Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Avalonia.Controls.WebView.Core/Gtk/GdkEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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)]
Expand Down
16 changes: 16 additions & 0 deletions src/Avalonia.Controls.WebView.Core/Gtk/GtkInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
Expand Down Expand Up @@ -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<WriteableBitmap, PixelSize>.IProducer producer)
{
if (_windowHandle == IntPtr.Zero)
Expand Down Expand Up @@ -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);
}
}
}
}
92 changes: 89 additions & 3 deletions src/Avalonia.Controls.WebView.Core/Gtk/GtkWebViewAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -185,13 +185,69 @@ public virtual Color DefaultBackground
public event EventHandler? GotFocus;
public event EventHandler<IWebViewAdapterWithFocus.LostFocusDirection>? LostFocus;

public void Focus() => RunOnWebView(static handle =>
/// <summary>
/// The GTK toplevel holding the web view, or zero when the adapter has none.
/// </summary>
protected virtual IntPtr ToplevelHandle => IntPtr.Zero;

/// <summary>
/// True when <see cref="ToplevelHandle"/> is a GtkOffscreenWindow, whose GdkWindow has no native window behind it.
/// </summary>
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()
{
Expand Down Expand Up @@ -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);
}
}
}
}
30 changes: 29 additions & 1 deletion src/Avalonia.Controls.WebView.Core/Gtk/GtkX11WebViewAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media;
Expand All @@ -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]<IntPtr, GdkEvent*, IntPtr, int>)&ButtonPressCallback);

private GtkSignal? _buttonPressSignal;

private readonly IntPtr _x11Window;
private IntPtr _windowHandle;
private IntPtr _currentParent;
Expand All @@ -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<GtkX11WebViewAdapter>(data, out var adapter))
{
adapter.Focus();
}

// Never handled here: WebKit still gets the click.
return False;
}

public static Task<WebViewAdapter.NativeWebViewAdapterBuilder> CreateBuilder(
Expand All @@ -48,7 +74,7 @@ private GtkX11WebViewAdapter(GtkWebViewEnvironmentRequestedEventArgs environment

return Task.FromResult(builder);
}

public override void SetParent(IPlatformHandle parent)
{
if (parent.HandleDescriptor != "XID")
Expand Down Expand Up @@ -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)
{
Expand Down
4 changes: 4 additions & 0 deletions src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down