diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/AvaloniaGtk.cs b/src/Avalonia.Controls.WebView.Core/Gtk/AvaloniaGtk.cs index 8d0825c..8c540f6 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/AvaloniaGtk.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/AvaloniaGtk.cs @@ -66,13 +66,11 @@ static AvaloniaGtk() public static bool HasSoup3 { get; } /// - /// Ensures GDK_BACKEND=x11 is in effect for the duration of the bridge call that - /// initializes GTK in Avalonia.X11.NativeDialogs.Gtk. The X11 GTK adapters in this - /// package use X11/GDK-X11-only entry points and require the X11 GDK backend; under - /// a Wayland session GDK_BACKEND is typically pre-set to "wayland" by the desktop, - /// which would otherwise cause `gtk_init` to fail with "Unable to initialize GTK". + /// Checks that GTK can initialize with the x11 GDK backend, which the GTK adapters require. + /// GDK_BACKEND replaces the backend list, while Avalonia only filters it with + /// gdk_set_allowed_backends("x11"), so any other explicit value makes gtk_init fail. /// - public static IDisposable EnsureX11GdkBackendForGtkInit() + public static IDisposable PrepareGdkBackendForGtkInit(bool forceX11) { if (!OperatingSystem.IsLinux()) return EmptyScope.Instance; @@ -81,11 +79,16 @@ public static IDisposable EnsureX11GdkBackendForGtkInit() { if (s_overrideCount == 0) { + // Unset is fine: GDK then falls back to the backends Avalonia allowed, i.e. x11 only. var current = Environment.GetEnvironmentVariable("GDK_BACKEND"); - if (string.Equals(current, "x11", StringComparison.Ordinal)) + if (current is not { Length: > 0 } || string.Equals(current, "x11", StringComparison.Ordinal)) return EmptyScope.Instance; - if (current is { Length: > 0 } && !string.Equals(current, "wayland", StringComparison.Ordinal)) + + if (!forceX11) + { + ReportUnsupportedBackend(current); return EmptyScope.Instance; + } int rc; try { rc = LibC.setenv("GDK_BACKEND", "x11", 1); } @@ -94,9 +97,10 @@ public static IDisposable EnsureX11GdkBackendForGtkInit() if (rc != 0) { Logger.TryGet(LogEventLevel.Warning, "WebView")?.Log(null, - "libc setenv(GDK_BACKEND, x11) returned {Rc}; gtk_init may still pick Wayland", rc); + "libc setenv(GDK_BACKEND, x11) returned {Rc}; gtk_init may still fail", rc); return EmptyScope.Instance; } + // Keep the managed cache in step, it doesn't share storage with libc's environ. Environment.SetEnvironmentVariable("GDK_BACKEND", "x11"); s_savedBackend = current; } @@ -105,9 +109,22 @@ public static IDisposable EnsureX11GdkBackendForGtkInit() return new RestoreGdkBackendScope(); } + private static void ReportUnsupportedBackend(string current) + { + if (s_reportedUnsupportedBackend) + return; + s_reportedUnsupportedBackend = true; + + Logger.TryGet(LogEventLevel.Error, "WebView")?.Log(null, + "GDK_BACKEND is set to {Backend}, but the GTK web view requires the x11 GDK backend, " + + "so GTK initialization is expected to fail. Either run with GDK_BACKEND=x11, or set " + + "ForceX11GdkBackend on GtkWebViewEnvironmentRequestedEventArgs.", current); + } + private static readonly object s_overrideLock = new(); private static int s_overrideCount; private static string? s_savedBackend; + private static bool s_reportedUnsupportedBackend; private sealed class EmptyScope : IDisposable { @@ -126,15 +143,13 @@ public void Dispose() { if (--s_overrideCount > 0) return; + // Only ever set when GDK_BACKEND already had a value, so there is nothing to unset. var previous = s_savedBackend; s_savedBackend = null; - try - { - if (previous is null) - LibC.unsetenv("GDK_BACKEND"); - else - LibC.setenv("GDK_BACKEND", previous, 1); - } + if (previous is null) + return; + + try { LibC.setenv("GDK_BACKEND", previous, 1); } catch (DllNotFoundException) { } catch (EntryPointNotFoundException) { } Environment.SetEnvironmentVariable("GDK_BACKEND", previous); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs index 59068d4..fade712 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs @@ -339,6 +339,10 @@ internal static partial IntPtr webkit_website_data_manager_new( [DllImport(LibGdk)] internal static extern IntPtr gdk_x11_window_get_xid(IntPtr window); + [DllImport(LibGdk)] + internal static extern void gdk_x11_window_set_frame_sync_enabled(IntPtr window, + [MarshalAs(UnmanagedType.Bool)] bool frameSyncEnabled); + [DllImport(LibGdk)] internal static extern IntPtr gdk_display_get_default(); [DllImport(LibGdk)] diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenAvaloniaWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenAvaloniaWebViewAdapter.cs index 196205b..c764b92 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenAvaloniaWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenAvaloniaWebViewAdapter.cs @@ -38,7 +38,7 @@ private GtkOffscreenAvaloniaWebViewAdapter(GtkWebViewEnvironmentRequestedEventAr // detached, so handing out a cached instance leaves a dead web view after a re-attach. WebViewAdapter.OffscreenWebViewAdapterBuilder builder = async parent => { - using var backendScope = EnsureX11GdkBackendForGtkInit(); + using var backendScope = PrepareGdkBackendForGtkInit(environmentArgs.ForceX11GdkBackend); var adapter = await RunOnGlibThreadAsync(() => new GtkOffscreenAvaloniaWebViewAdapter(environmentArgs)); adapter.Parent = parent; return adapter; diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs index d33e80a..a6ac807 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs @@ -23,7 +23,14 @@ private GtkX11WebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs environment gtk_container_add(_windowHandle, WebViewHandle); gtk_widget_show_all(WebViewHandle); gtk_widget_realize(_windowHandle); - _x11Window = gdk_x11_window_get_xid(gtk_widget_get_window(_windowHandle)); + + var gdkWindow = gtk_widget_get_window(_windowHandle); + _x11Window = gdk_x11_window_get_xid(gdkWindow); + + // Reparenting this window into the Avalonia window takes it away from the window manager, + // so the _NET_WM_FRAME_DRAWN replies GDK throttles drawing on never arrive. + // Without this GTK paints one frame and then waits forever. + gdk_x11_window_set_frame_sync_enabled(gdkWindow, false); } public static Task CreateBuilder( @@ -32,7 +39,7 @@ private GtkX11WebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs environment WebViewAdapter.NativeWebViewAdapterBuilder builder = (parent, _) => { WebViewDispatcher.VerifyAccess(); - using var backendScope = EnsureX11GdkBackendForGtkInit(); + using var backendScope = PrepareGdkBackendForGtkInit(environmentArgs.ForceX11GdkBackend); var adapter = RunOnGlibThread(() => new GtkX11WebViewAdapter(environmentArgs)); adapter.SetParent(parent); return new WebViewAdapter.AdapterWrapper(adapter, Task.FromResult(adapter)); diff --git a/src/Avalonia.Controls.WebView.Core/Linux/Interop/LibC.cs b/src/Avalonia.Controls.WebView.Core/Linux/Interop/LibC.cs index a461f7d..397dac0 100644 --- a/src/Avalonia.Controls.WebView.Core/Linux/Interop/LibC.cs +++ b/src/Avalonia.Controls.WebView.Core/Linux/Interop/LibC.cs @@ -10,12 +10,6 @@ internal static unsafe partial class LibC [LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)] public static partial int setenv(string name, string value, int overwrite); - [LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)] - public static partial int unsetenv(string name); - - [LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)] - public static partial nint getenv(string name); - [LibraryImport("libc")] public static partial int poll(GPollFD* fds, nint nfds, int timeout); diff --git a/src/Avalonia.Controls.WebView.Core/Platform/GtkWebViewEnvironmentRequestedEventArgs.cs b/src/Avalonia.Controls.WebView.Core/Platform/GtkWebViewEnvironmentRequestedEventArgs.cs index 3bade75..28723d8 100644 --- a/src/Avalonia.Controls.WebView.Core/Platform/GtkWebViewEnvironmentRequestedEventArgs.cs +++ b/src/Avalonia.Controls.WebView.Core/Platform/GtkWebViewEnvironmentRequestedEventArgs.cs @@ -20,6 +20,15 @@ internal GtkWebViewEnvironmentRequestedEventArgs(DeferralManager deferralManager /// public bool ExperimentalOffscreen { get; set; } + /// + /// Gets or sets a value indicating whether GDK_BACKEND should be forced to "x11" while GTK is initialized. + /// The GTK adapters require the x11 GDK backend, and a Wayland desktop usually pre-sets GDK_BACKEND=wayland, which makes gtk_init fail. + /// + /// + /// Disabled by default, as it temporarily mutates the process environment. The previous value is restored once GTK is initialized. + /// + public bool ForceX11GdkBackend { get; set; } + /// /// Gets or sets a value indicating whether the webview should use an ephemeral data manager, handling all website data as non-persistent and not writing anything to client storage. ///