From b8321a69f66b60f7e3dd93e11cedb5fe3cbbff49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Duguay-L=C3=A9tourneau?= Date: Wed, 23 Sep 2026 14:41:12 -0400 Subject: [PATCH 1/6] webview: send focus changes to the offscreen GTK toplevel --- .../Gtk/GdkEvent.cs | 11 +++++ .../Gtk/GtkInterop.cs | 3 ++ .../Gtk/GtkOffscreenWebViewAdapter.cs | 45 ++++++++++++++++++- .../Gtk/GtkWebViewAdapter.cs | 4 +- .../NativeWebViewCompositorHost.cs | 4 ++ 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GdkEvent.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GdkEvent.cs index 9912fac..0bd00f0 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GdkEvent.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GdkEvent.cs @@ -23,6 +23,7 @@ internal enum GdkEventType GDK_KEY_RELEASE = 9, GDK_ENTER_NOTIFY = 10, GDK_LEAVE_NOTIFY = 11, + GDK_FOCUS_CHANGE = 12, GDK_SCROLL = 31, } @@ -37,6 +38,16 @@ internal struct GdkEvent [FieldOffset(0)] public GdkEventScroll scroll; [FieldOffset(0)] public GdkEventCrossing crossing; [FieldOffset(0)] public GdkEventKey key; + [FieldOffset(0)] public GdkEventFocus focus_change; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct GdkEventFocus +{ + public GdkEventType type; + public IntPtr window; + public SByte send_event; + public Int16 @in; } [StructLayout(LayoutKind.Sequential)] diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs index 04bd8a1..00130f5 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs @@ -358,6 +358,9 @@ internal static extern void gdk_x11_window_set_frame_sync_enabled(IntPtr window, [DllImport(LibGtk)] public static extern bool gtk_widget_event(IntPtr widget, IntPtr gdkEvent); + [DllImport(LibGtk)] + public static extern bool gtk_widget_send_focus_change(IntPtr widget, IntPtr gdkEvent); + [DllImport(LibGtk)] public static extern void gtk_main_do_event(IntPtr gdkEvent); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs index 9e79a94..a78f823 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -151,6 +151,49 @@ public override void SizeChanged(PixelSize containerSize) }); } + public override void Focus() + { + if (!_experimentalOffscreen) + { + base.Focus(); + return; + } + + RunOnGlibThreadAsync(() => + { + if (_windowHandle == IntPtr.Zero) + return; + + gtk_widget_grab_focus(WebViewHandle); + SendToplevelFocusChange(true); + }); + } + + public override void ResignFocus() + { + if (!_experimentalOffscreen) + { + base.ResignFocus(); + return; + } + + RunOnGlibThreadAsync(() => + { + if (_windowHandle != IntPtr.Zero) + SendToplevelFocusChange(false); + }); + } + + private void SendToplevelFocusChange(bool focusIn) + { + if (gtk_widget_get_window(_windowHandle) == IntPtr.Zero) + return; + + using var state = new EventSendState(GdkEventType.GDK_FOCUS_CHANGE, _windowHandle); + state.Event->focus_change.@in = (short)(focusIn ? 1 : 0); + gtk_widget_send_focus_change(_windowHandle, new IntPtr(state.Event)); + } + public bool KeyInput(bool press, PhysicalKey physical, string? _, KeyModifiers modifiers) { var keycode = KeyTransform.ScanCodeFromPhysicalKey(physical); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs index c1958e3..661ea59 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs @@ -185,13 +185,13 @@ public virtual Color DefaultBackground public event EventHandler? GotFocus; public event EventHandler? LostFocus; - public void Focus() => RunOnWebView(static handle => + public virtual void Focus() => RunOnWebView(static handle => { gtk_widget_grab_focus(handle); gtk_widget_has_focus(handle); }); - public void ResignFocus() { } + public virtual void ResignFocus() { } public bool GoBack() { diff --git a/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs b/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs index 3962886..2f41591 100644 --- a/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs +++ b/src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs @@ -172,6 +172,10 @@ private async void OffscreenAdapter_OnDrawRequested() await adapter.UpdateWriteableBitmap(adapterSize, _frameChain.Producer); _customVisual?.SendHandlerMessage(VisualHandler.DrawRequested); + + // Invalidate() on the handler marks the visual dirty without scheduling a compositor frame, so a + // sparse update would sit there until the next one drove one. + InvalidateVisual(); } catch (Exception ex) { From b1ffb29a5e1d54a7a5d0d4cc03a7f2aeb12cf1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Duguay-L=C3=A9tourneau?= Date: Wed, 23 Sep 2026 16:07:07 -0400 Subject: [PATCH 2/6] webview: give the X11 GTK adapter focus as well The toplevel focus change only ran on the offscreen adapter, and GtkX11WebViewAdapter is a sibling of it rather than a subclass, so X11 never got it and still drew no caret. Move ToplevelHandle, the focus change and EventSendState up into GtkWebViewAdapter so both adapters share them, and drop the ExperimentalOffscreen guard: the condition was never offscreen-ness but a toplevel that no window manager owns, which both adapters have. That alone is not enough on X11. Reparenting the toplevel into the host window leaves it without a window manager and without an XEmbed handshake, so nothing ever hands it the X input focus: clicking the page raises no GTK focus-in at all and Focus() was never called. Take the button press as the focus gesture instead and drive the same path from there, returning false so WebKit still handles the click. GTK then emits its own focus-in, which pulls the host's focus across too. Co-Authored-By: Claude Opus 5 --- .../Gtk/GtkOffscreenWebViewAdapter.cs | 72 +------------------ .../Gtk/GtkWebViewAdapter.cs | 51 ++++++++++++- .../Gtk/GtkX11WebViewAdapter.cs | 28 ++++++++ 3 files changed, 78 insertions(+), 73 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs index a78f823..22e2538 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs @@ -151,48 +151,7 @@ public override void SizeChanged(PixelSize containerSize) }); } - public override void Focus() - { - if (!_experimentalOffscreen) - { - base.Focus(); - return; - } - - RunOnGlibThreadAsync(() => - { - if (_windowHandle == IntPtr.Zero) - return; - - gtk_widget_grab_focus(WebViewHandle); - SendToplevelFocusChange(true); - }); - } - - public override void ResignFocus() - { - if (!_experimentalOffscreen) - { - base.ResignFocus(); - return; - } - - RunOnGlibThreadAsync(() => - { - if (_windowHandle != IntPtr.Zero) - SendToplevelFocusChange(false); - }); - } - - private void SendToplevelFocusChange(bool focusIn) - { - if (gtk_widget_get_window(_windowHandle) == IntPtr.Zero) - return; - - using var state = new EventSendState(GdkEventType.GDK_FOCUS_CHANGE, _windowHandle); - state.Event->focus_change.@in = (short)(focusIn ? 1 : 0); - gtk_widget_send_focus_change(_windowHandle, new IntPtr(state.Event)); - } + protected override IntPtr ToplevelHandle => _windowHandle; public bool KeyInput(bool press, PhysicalKey physical, string? _, KeyModifiers modifiers) { @@ -387,33 +346,4 @@ private static unsafe int DrawCallback(IntPtr widget, IntPtr* cairoTex, IntPtr d return False; } - private readonly ref struct EventSendState : IDisposable - { - private readonly IntPtr _evPtr; - - public EventSendState(GdkEventType eventType, IntPtr handle) - { - _evPtr = gdk_event_new(eventType); - var ev = (GdkEvent*)_evPtr.ToPointer(); - ev->any.window = gtk_widget_get_window(handle); // gdk window - ev->any.send_event = 1; - g_object_ref(ev->any.window); - } - - public GdkEvent* Event => (GdkEvent*)_evPtr.ToPointer(); - - public bool Send() - { - gdk_event_put(_evPtr); - return true; - } - - public void Dispose() - { - if (_evPtr != IntPtr.Zero) - { - gdk_event_free(_evPtr); - } - } - } } diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs index 661ea59..42caced 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs @@ -185,13 +185,60 @@ public virtual Color DefaultBackground public event EventHandler? GotFocus; public event EventHandler? LostFocus; - public virtual void Focus() => RunOnWebView(static handle => + /// + /// The GTK toplevel holding the web view, or zero when the adapter has none. + /// + protected virtual IntPtr ToplevelHandle => IntPtr.Zero; + + public virtual void Focus() => RunOnWebView(handle => { gtk_widget_grab_focus(handle); gtk_widget_has_focus(handle); + SendToplevelFocusChange(true); }); - public virtual void ResignFocus() { } + public virtual void ResignFocus() => RunOnWebView(_ => SendToplevelFocusChange(false)); + + private unsafe void SendToplevelFocusChange(bool focusIn) + { + var toplevel = ToplevelHandle; + if (toplevel == IntPtr.Zero || gtk_widget_get_window(toplevel) == IntPtr.Zero) + return; + + using var state = new EventSendState(GdkEventType.GDK_FOCUS_CHANGE, toplevel); + state.Event->focus_change.@in = (short)(focusIn ? 1 : 0); + gtk_widget_send_focus_change(toplevel, new IntPtr(state.Event)); + } + + protected readonly unsafe ref struct EventSendState : IDisposable + { + private readonly IntPtr _evPtr; + + public EventSendState(GdkEventType eventType, IntPtr handle) + { + _evPtr = gdk_event_new(eventType); + var ev = (GdkEvent*)_evPtr.ToPointer(); + ev->any.window = gtk_widget_get_window(handle); // gdk window + ev->any.send_event = 1; + g_object_ref(ev->any.window); + } + + public GdkEvent* Event => (GdkEvent*)_evPtr.ToPointer(); + + public bool Send() + { + gdk_event_put(_evPtr); + return true; + } + + public void Dispose() + { + if (_evPtr != IntPtr.Zero) + { + gdk_event_free(_evPtr); + } + } + } public bool GoBack() { diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs index 6971dcb..4144e9e 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs @@ -1,4 +1,6 @@ using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Media; @@ -13,6 +15,11 @@ internal sealed class GtkX11WebViewAdapter : GtkWebViewAdapter, IPlatformHandle { private static readonly IntPtr s_display = XOpenDisplay(IntPtr.Zero); + private static readonly unsafe IntPtr s_buttonPressCallback = + new((delegate* unmanaged[Cdecl])&ButtonPressCallback); + + private GtkSignal? _buttonPressSignal; + private readonly IntPtr _x11Window; private IntPtr _windowHandle; private IntPtr _currentParent; @@ -32,6 +39,23 @@ private GtkX11WebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs environment // 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); + + // Reparenting also means no window manager and no XEmbed handshake, so nothing ever hands this toplevel the + // X input focus: clicking the page raises no GTK focus-in, the page stays inactive. Take + // the click as the focus gesture the window manager would otherwise have turned into one. + _buttonPressSignal = new GtkSignal(WebViewHandle, "button-press-event", s_buttonPressCallback, this); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe int ButtonPressCallback(IntPtr widget, GdkEvent* gdkEvent, IntPtr data) + { + if (GtkSignal.TryGetState(data, out var adapter)) + { + adapter.Focus(); + } + + // Never handled here: WebKit still gets the click. + return False; } public static Task CreateBuilder( @@ -49,6 +73,8 @@ private GtkX11WebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs environment return Task.FromResult(builder); } + protected override IntPtr ToplevelHandle => _windowHandle; + public override void SetParent(IPlatformHandle parent) { if (parent.HandleDescriptor != "XID") @@ -107,6 +133,8 @@ public override Color DefaultBackground protected override void DisposeSafe(bool disposing) { + Interlocked.Exchange(ref _buttonPressSignal, null)?.Dispose(); + var window = Interlocked.Exchange(ref _windowHandle, IntPtr.Zero); if (window != IntPtr.Zero) { From 24259bc92edc513f1a3e97cab2ff45a0b5393747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Duguay-L=C3=A9tourneau?= Date: Wed, 23 Sep 2026 16:59:33 -0400 Subject: [PATCH 3/6] webview: skip the toplevel focus change for offscreen on x11 An offscreen GdkWindow has no X window behind it. Activating one on the x11 backend makes GTK and WebKit query the pointer against it, and XIQueryPointer on a window that does not exist fails with a BadWindow that takes the whole process down. Reproducible without any pointer input: focusing the web view is enough. Leave the toplevel alone in that combination and log once to say why there is no caret. Wayland is unaffected, since offscreen windows there never reach XInput, and so is the native x11 adapter, whose toplevel is a real X window. Co-Authored-By: Claude Opus 5 --- .../Gtk/GtkInterop.cs | 7 +++++ .../Gtk/GtkOffscreenWebViewAdapter.cs | 2 ++ .../Gtk/GtkWebViewAdapter.cs | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs index 00130f5..ffa980b 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs @@ -361,6 +361,13 @@ internal static extern void gdk_x11_window_set_frame_sync_enabled(IntPtr window, [DllImport(LibGtk)] public static extern bool gtk_widget_send_focus_change(IntPtr widget, IntPtr gdkEvent); + [DllImport(LibGdk)] + internal static extern IntPtr gdk_x11_display_get_type(); + + [DllImport(LibGObject)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool g_type_check_instance_is_a(IntPtr instance, IntPtr ifaceType); + [DllImport(LibGtk)] public static extern void gtk_main_do_event(IntPtr gdkEvent); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs index 22e2538..18bed4b 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs @@ -153,6 +153,8 @@ public override void SizeChanged(PixelSize containerSize) protected override IntPtr ToplevelHandle => _windowHandle; + protected override bool ToplevelIsOffscreen => _experimentalOffscreen; + public bool KeyInput(bool press, PhysicalKey physical, string? _, KeyModifiers modifiers) { var keycode = KeyTransform.ScanCodeFromPhysicalKey(physical); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs index 42caced..0dbad9a 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs @@ -190,6 +190,11 @@ public virtual Color DefaultBackground /// protected virtual IntPtr ToplevelHandle => IntPtr.Zero; + /// + /// True when is a GtkOffscreenWindow, whose GdkWindow has no native window behind it. + /// + protected virtual bool ToplevelIsOffscreen => false; + public virtual void Focus() => RunOnWebView(handle => { gtk_widget_grab_focus(handle); @@ -205,11 +210,32 @@ private unsafe void SendToplevelFocusChange(bool focusIn) if (toplevel == IntPtr.Zero || gtk_widget_get_window(toplevel) == IntPtr.Zero) return; + if (ToplevelIsOffscreen && IsX11Display()) + { + return; + } + using var state = new EventSendState(GdkEventType.GDK_FOCUS_CHANGE, toplevel); state.Event->focus_change.@in = (short)(focusIn ? 1 : 0); gtk_widget_send_focus_change(toplevel, new IntPtr(state.Event)); } + private static bool IsX11Display() + { + var display = gdk_display_get_default(); + if (display == IntPtr.Zero) + return false; + + try + { + return g_type_check_instance_is_a(display, gdk_x11_display_get_type()); + } + catch (EntryPointNotFoundException) + { + return false; + } + } + protected readonly unsafe ref struct EventSendState : IDisposable { private readonly IntPtr _evPtr; From 31b3e8ebba2f9c3a22e47d8bd57423ec2284c946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Duguay-L=C3=A9tourneau?= Date: Wed, 23 Sep 2026 17:40:46 -0400 Subject: [PATCH 4/6] remove virtual --- src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs index 0dbad9a..3b1248a 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs @@ -195,14 +195,14 @@ public virtual Color DefaultBackground /// protected virtual bool ToplevelIsOffscreen => false; - public virtual void Focus() => RunOnWebView(handle => + public void Focus() => RunOnWebView(handle => { gtk_widget_grab_focus(handle); gtk_widget_has_focus(handle); SendToplevelFocusChange(true); }); - public virtual void ResignFocus() => RunOnWebView(_ => SendToplevelFocusChange(false)); + public void ResignFocus() => RunOnWebView(_ => SendToplevelFocusChange(false)); private unsafe void SendToplevelFocusChange(bool focusIn) { From 7757de774f8ae757643b5e664806c0b80177a476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Duguay-L=C3=A9tourneau?= Date: Wed, 23 Sep 2026 17:52:02 -0400 Subject: [PATCH 5/6] reorder --- .../Gtk/GtkOffscreenWebViewAdapter.cs | 9 ++- .../Gtk/GtkWebViewAdapter.cs | 62 +++++++++---------- .../Gtk/GtkX11WebViewAdapter.cs | 6 +- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs index 18bed4b..471848f 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkOffscreenWebViewAdapter.cs @@ -41,7 +41,10 @@ protected GtkOffscreenWebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs arg public PixelFormat BufferPixelFormat => PixelFormats.Rgba8888; public AlphaFormat BufferAlphaFormat => AlphaFormat.Unpremul; - + + protected override IntPtr ToplevelHandle => _windowHandle; + protected override bool ToplevelIsOffscreen => _experimentalOffscreen; + public Task UpdateWriteableBitmap(PixelSize _, FrameChainBase.IProducer producer) { if (_windowHandle == IntPtr.Zero) @@ -151,10 +154,6 @@ public override void SizeChanged(PixelSize containerSize) }); } - protected override IntPtr ToplevelHandle => _windowHandle; - - protected override bool ToplevelIsOffscreen => _experimentalOffscreen; - public bool KeyInput(bool press, PhysicalKey physical, string? _, KeyModifiers modifiers) { var keycode = KeyTransform.ScanCodeFromPhysicalKey(physical); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs index 3b1248a..9ae4428 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Net.Http; using System.Runtime.CompilerServices; @@ -235,36 +235,6 @@ private static bool IsX11Display() return false; } } - - protected readonly unsafe ref struct EventSendState : IDisposable - { - private readonly IntPtr _evPtr; - - public EventSendState(GdkEventType eventType, IntPtr handle) - { - _evPtr = gdk_event_new(eventType); - var ev = (GdkEvent*)_evPtr.ToPointer(); - ev->any.window = gtk_widget_get_window(handle); // gdk window - ev->any.send_event = 1; - g_object_ref(ev->any.window); - } - - public GdkEvent* Event => (GdkEvent*)_evPtr.ToPointer(); - - public bool Send() - { - gdk_event_put(_evPtr); - return true; - } - - public void Dispose() - { - if (_evPtr != IntPtr.Zero) - { - gdk_event_free(_evPtr); - } - } - } public bool GoBack() { @@ -734,4 +704,34 @@ internal static DetailedWebViewAdapterInfo GetWebKitGtkInfo(WebViewEmbeddingScen UnavailableReason: version is not null ? null : "WebKitGtk library is not installed. Install webkit2gtk 4.0+ package.", SupportedScenarios: version is not null ? scenarios : WebViewEmbeddingScenario.None); } + + protected readonly unsafe ref struct EventSendState : IDisposable + { + private readonly IntPtr _evPtr; + + public EventSendState(GdkEventType eventType, IntPtr handle) + { + _evPtr = gdk_event_new(eventType); + var ev = (GdkEvent*)_evPtr.ToPointer(); + ev->any.window = gtk_widget_get_window(handle); // gdk window + ev->any.send_event = 1; + g_object_ref(ev->any.window); + } + + public GdkEvent* Event => (GdkEvent*)_evPtr.ToPointer(); + + public bool Send() + { + gdk_event_put(_evPtr); + return true; + } + + public void Dispose() + { + if (_evPtr != IntPtr.Zero) + { + gdk_event_free(_evPtr); + } + } + } } diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs index 4144e9e..578d9c9 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs @@ -46,6 +46,8 @@ private GtkX11WebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs environment _buttonPressSignal = new GtkSignal(WebViewHandle, "button-press-event", s_buttonPressCallback, this); } + protected override IntPtr ToplevelHandle => _windowHandle; + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] private static unsafe int ButtonPressCallback(IntPtr widget, GdkEvent* gdkEvent, IntPtr data) { @@ -72,9 +74,7 @@ private static unsafe int ButtonPressCallback(IntPtr widget, GdkEvent* gdkEvent, return Task.FromResult(builder); } - - protected override IntPtr ToplevelHandle => _windowHandle; - + public override void SetParent(IPlatformHandle parent) { if (parent.HandleDescriptor != "XID") From c7786c2ee4ac685aa9abe7be6ee8041ea23d1afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Duguay-L=C3=A9tourneau?= Date: Fri, 25 Sep 2026 16:58:04 -0400 Subject: [PATCH 6/6] fix BadWindow error when activating gtk off screen window under x11 --- .../Gtk/GtkInterop.cs | 6 +++++ .../Gtk/GtkWebViewAdapter.cs | 23 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs index ffa980b..389cb5d 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs @@ -364,6 +364,12 @@ internal static extern void gdk_x11_window_set_frame_sync_enabled(IntPtr window, [DllImport(LibGdk)] internal static extern IntPtr gdk_x11_display_get_type(); + [DllImport(LibGdk)] + internal static extern void gdk_x11_display_error_trap_push(IntPtr display); + + [DllImport(LibGdk)] + internal static extern int gdk_x11_display_error_trap_pop(IntPtr display); + [DllImport(LibGObject)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool g_type_check_instance_is_a(IntPtr instance, IntPtr ifaceType); diff --git a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs index 9ae4428..495a52e 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs @@ -210,14 +210,27 @@ private unsafe void SendToplevelFocusChange(bool focusIn) if (toplevel == IntPtr.Zero || gtk_widget_get_window(toplevel) == IntPtr.Zero) return; - if (ToplevelIsOffscreen && IsX11Display()) + // Activating an offscreen toplevel on x11 queries the pointer against a window that has no X counterpart. The + // activation still lands; only the resulting BadWindow needs swallowing, and it is fatal untrapped. + var trapped = ToplevelIsOffscreen && IsX11Display() ? gdk_display_get_default() : IntPtr.Zero; + if (trapped != IntPtr.Zero) { - return; + gdk_x11_display_error_trap_push(trapped); } - using var state = new EventSendState(GdkEventType.GDK_FOCUS_CHANGE, toplevel); - state.Event->focus_change.@in = (short)(focusIn ? 1 : 0); - gtk_widget_send_focus_change(toplevel, new IntPtr(state.Event)); + try + { + using var state = new EventSendState(GdkEventType.GDK_FOCUS_CHANGE, toplevel); + state.Event->focus_change.@in = (short)(focusIn ? 1 : 0); + gtk_widget_send_focus_change(toplevel, new IntPtr(state.Event)); + } + finally + { + if (trapped != IntPtr.Zero) + { + gdk_x11_display_error_trap_pop(trapped); + } + } } private static bool IsX11Display()