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..389cb5d 100644 --- a/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs +++ b/src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs @@ -358,6 +358,22 @@ 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(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); + [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..471848f 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; @@ -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) @@ -344,33 +347,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 c1958e3..495a52e 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; @@ -185,13 +185,69 @@ public virtual Color DefaultBackground public event EventHandler? GotFocus; public event EventHandler? LostFocus; - public 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; + + /// + /// True when is a GtkOffscreenWindow, whose GdkWindow has no native window behind it. + /// + protected virtual bool ToplevelIsOffscreen => false; + + public void Focus() => RunOnWebView(handle => { gtk_widget_grab_focus(handle); gtk_widget_has_focus(handle); + SendToplevelFocusChange(true); }); - public void ResignFocus() { } + public 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; + + // 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) + { + gdk_x11_display_error_trap_push(trapped); + } + + 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() + { + 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; + } + } public bool GoBack() { @@ -661,4 +717,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 6971dcb..578d9c9 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,25 @@ 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); + } + + protected override IntPtr ToplevelHandle => _windowHandle; + + [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( @@ -48,7 +74,7 @@ private GtkX11WebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs environment return Task.FromResult(builder); } - + 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) { 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) {