diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index c87d9fdead..9398987837 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -9,6 +9,7 @@
+
diff --git a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
index b1fef7c013..9d698716ee 100644
--- a/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
+++ b/samples/ControlCatalog.NetCore/NativeControls/Gtk/GtkHelper.cs
@@ -5,7 +5,7 @@ using Avalonia.Platform.Interop;
using Avalonia.X11.Interop;
using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.NativeDialogs.Gtk;
-using static Avalonia.X11.NativeDialogs.Glib;
+using static Avalonia.X11.Interop.Glib;
namespace ControlCatalog.NetCore;
diff --git a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
new file mode 100644
index 0000000000..29f2836594
--- /dev/null
+++ b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
@@ -0,0 +1,313 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using Avalonia.Logging;
+using Avalonia.Threading;
+using static Avalonia.X11.Interop.Glib;
+namespace Avalonia.X11.Dispatching;
+
+internal class GlibDispatcherImpl :
+ IDispatcherImplWithExplicitBackgroundProcessing,
+ IControlledDispatcherImpl
+{
+ /*
+ GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there
+ are "background" and "foreground" priority groups. Foreground jobs are executed before any user input processing,
+ background jobs are executed strictly after user input processing.
+
+ GLib has numeric priorities that are used in the following way:
+ -100 G_PRIORITY_HIGH - "high" priority sources, not really used by GLib/GTK
+ 0 G_PRIORITY_DEFAULT - polling X11 events (GTK) and default value for g_timeout_add
+ 100 G_PRIORITY_HIGH_IDLE without a clear definition, used as an anchor value of sorts
+ 110 Resize/layout operations (GTK)
+ 120 Render operations (GTK)
+ 200 G_PRIORITY_DEFAULT_IDLE - "idle" priority sources
+
+ So, unlike Avalonia, GTK puts way higher priority on input processing, then does resize/layout/render
+
+ So, to map our model to GLib we do the following:
+ - foreground jobs (including grouped user events) are executed with (-1) priority (_before_ any normal GLib jobs)
+ - X11 socket is polled with G_PRIORITY_DEFAULT, all X11 events are read until socket is empty,
+ we also group input events at that stage (this matches our epoll-based dispatcher)
+ - background jobs are executed with G_PRIORITY_DEFAULT_IDLE, so they would have lower priority than GTK
+ foreground jobs
+
+ Unfortunately we can't detect if there are pending _non-idle_ GLib jobs using g_main_context_pending, since
+ - g_main_context_pending doesn't accept max_priority argument
+ - even if it did, that would still involve a syscall to the kernel to poll for fds anyway
+
+ So we just report that we don't support pending input query and let the dispatcher to
+ call RequestBackgroundProcessing every time, which results in g_idle_add call for every background job.
+ Background jobs are expected to be relatively expensive to execute since on Windows
+ MsgWaitForMultipleObjectsEx results isn't really free too.
+
+ For signaling (aka waking up dispatcher for processing _high_ priority jobs we are using
+ g_idle_add_full with (-1) priority. While the naming suggests that it would enqueue an idle job,
+ it actually adds an always-triggered source that would be called before other sources with lower priority.
+
+ For timers we are using a simple g_timeout_add_full and discard the previous one when dispatcher requests
+ an update
+
+ Since GLib dispatches event sources in batches, we force-check for "signaled" flag to run high-prio jobs
+ whenever we get control back from GLib. We can still occasionally get GTK code to run before high-prio
+ Avalonia-jobs, but that should be fine since the point is to keep Avalonia-based jobs ordered properly
+ and to not have our low-priority jobs to prevent GLib-based code from running its own "foreground" jobs
+
+ Another implementation note here is that GLib (just as any other C library) is NOT aware of C# exceptions,
+ so we are NOT allowed to have exceptions to escape native->managed call boundary. So we have exception handlers
+ that try to propagate those to the nearest run loop frame that was initiated by Avalonia.
+
+ If there is no such frame, we have no choice but to log/swallow those
+ */
+
+ private readonly AvaloniaX11Platform _platform;
+
+ // Note that we can't use g_main_context_is_owner outside a run loop, since context doesn't really have an
+ // inherent owner when run loop is not running and the context isn't explicitly "locked", so we just assume that
+ // the app author is initializing Avalonia on the intended UI thread and won't migrate the default run loop
+ // to a different thread
+ private readonly Thread _mainThread = Thread.CurrentThread;
+
+ private readonly X11EventDispatcher _x11Events;
+ private bool _signaled;
+ private bool _signaledSourceAdded;
+ private readonly object _signalLock = new();
+ private readonly Stack _runLoopStack = new();
+
+ private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+ private uint? _glibTimerSourceTag;
+
+ public GlibDispatcherImpl(AvaloniaX11Platform platform)
+ {
+ _platform = platform;
+ _x11Events = new X11EventDispatcher(platform);
+ var unixFdId = g_unix_fd_add_full(G_PRIORITY_DEFAULT, _x11Events.Fd, GIOCondition.G_IO_IN,
+ X11SourceCallback);
+ // We can trigger a nested event loop when handling X11 events, so we need to mark the source as recursive
+ var unixFdSource = g_main_context_find_source_by_id(IntPtr.Zero, unixFdId);
+ g_source_set_can_recurse(unixFdSource, 1);
+ }
+
+ public bool CurrentThreadIsLoopThread => _mainThread == Thread.CurrentThread;
+
+ public event Action? Signaled;
+ public void Signal()
+ {
+ lock (_signalLock)
+ {
+ if(_signaled)
+ return;
+ _signaled = true;
+ if(_signaledSourceAdded)
+ return;
+ _signaledSourceAdded = true;
+ }
+ g_idle_add_full(G_PRIORITY_DEFAULT - 1, SignalSourceCallback);
+ }
+
+ private void CheckSignaled()
+ {
+ lock (_signalLock)
+ {
+ if (!_signaled)
+ return;
+ _signaled = false;
+ }
+
+ try
+ {
+ Signaled?.Invoke();
+ }
+ catch (Exception e)
+ {
+ HandleException(e);
+ }
+ _x11Events.Flush();
+ }
+
+ private bool SignalSourceCallback()
+ {
+ lock (_signalLock)
+ {
+ _signaledSourceAdded = false;
+ }
+ CheckSignaled();
+ return false;
+ }
+
+ public event Action? Timer;
+ public long Now => _stopwatch.ElapsedMilliseconds;
+
+ public void UpdateTimer(long? dueTimeInMs)
+ {
+ if (_glibTimerSourceTag.HasValue)
+ {
+ g_source_remove(_glibTimerSourceTag.Value);
+ _glibTimerSourceTag = null;
+ }
+
+ if (dueTimeInMs == null)
+ return;
+
+ var interval = (uint)Math.Max(0, (int)Math.Min(int.MaxValue, dueTimeInMs.Value - Now));
+ _glibTimerSourceTag = g_timeout_add_once(interval, TimerCallback);
+ }
+
+ private void TimerCallback()
+ {
+ try
+ {
+ Timer?.Invoke();
+ }
+ catch (Exception e)
+ {
+ HandleException(e);
+ }
+ _x11Events.Flush();
+ }
+
+ public event Action? ReadyForBackgroundProcessing;
+
+ public void RequestBackgroundProcessing() =>
+ g_idle_add_once(() => ReadyForBackgroundProcessing?.Invoke());
+
+ public bool CanQueryPendingInput => false;
+ public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
+
+ private bool X11SourceCallback(int i, GIOCondition gioCondition)
+ {
+ CheckSignaled();
+ var token = _runLoopStack.Count > 0 ? _runLoopStack.Peek().Cancelled : CancellationToken.None;
+ try
+ {
+ // Completely drain X11 socket while we are at it
+ while (_x11Events.IsPending)
+ {
+ // If we don't actually drain our X11 socket, GLib _will_ call us again even if
+ // we request the run loop to quit
+ _x11Events.DispatchX11Events(CancellationToken.None);
+ if (!token.IsCancellationRequested)
+ {
+ while (_platform.EventGrouperDispatchQueue.HasJobs)
+ {
+ CheckSignaled();
+ _platform.EventGrouperDispatchQueue.DispatchNext();
+ }
+
+ _x11Events.Flush();
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ HandleException(e);
+ }
+
+ return true;
+ }
+
+ public void RunLoop(CancellationToken token)
+ {
+ if(token.IsCancellationRequested)
+ return;
+
+ using var loop = new ManagedLoopFrame(token);
+ _runLoopStack.Push(loop);
+ loop.Run();
+ _runLoopStack.Pop();
+
+ // Propagate any managed exceptions that we've captured from this frame
+ if(loop.Exceptions.Count == 1)
+ loop.Exceptions[0].Throw();
+ else if (loop.Exceptions.Count > 1)
+ throw new AggregateException(loop.Exceptions.Select(x => x.SourceException));
+ }
+
+ void HandleException(Exception e)
+ {
+ if (_runLoopStack.Count > 0)
+ {
+ var frame = _runLoopStack.Peek();
+ frame.Exceptions.Add(ExceptionDispatchInfo.Capture(e));
+ frame.Stop();
+ }
+ else
+ {
+ var externalLogger = _platform.Options.ExterinalGLibMainLoopExceptionLogger;
+ if (externalLogger != null)
+ externalLogger.Invoke(e);
+ else
+ Logger.TryGet(LogEventLevel.Error, LogArea.Control)
+ ?.Log("Dispatcher", "Unhandled exception: {exception}", e);
+ }
+ }
+
+ private class ManagedLoopFrame : IDisposable
+ {
+ private readonly CancellationToken _externalToken;
+ private CancellationTokenSource? _internalTokenSource;
+ public CancellationToken Cancelled { get; private set; }
+
+ private readonly IntPtr _loop = g_main_loop_new(IntPtr.Zero, 1);
+ public List Exceptions { get; } = new();
+ private readonly object _destroyLock = new();
+ private bool _disposed;
+
+ public ManagedLoopFrame(CancellationToken token)
+ {
+ _externalToken = token;
+ }
+
+ public void Stop()
+ {
+ try
+ {
+ _internalTokenSource?.Cancel();
+ }
+ catch
+ {
+ // Ignore
+ }
+ }
+
+ public void Run()
+ {
+ if (_externalToken.IsCancellationRequested)
+ return;
+ using (_internalTokenSource = new())
+ using (var composite =
+ CancellationTokenSource.CreateLinkedTokenSource(_externalToken, _internalTokenSource.Token))
+ {
+ Cancelled = composite.Token;
+ using (Cancelled.Register(() =>
+ {
+ lock (_destroyLock)
+ {
+ if (_disposed)
+ return;
+ g_main_loop_quit(_loop);
+ }
+ }))
+ {
+ g_main_loop_run(_loop);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_destroyLock)
+ {
+ if(_disposed)
+ return;
+ _disposed = true;
+ g_main_loop_unref(_loop);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs b/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs
new file mode 100644
index 0000000000..36625a61cd
--- /dev/null
+++ b/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using static Avalonia.X11.XLib;
+namespace Avalonia.X11;
+
+internal class X11EventDispatcher
+{
+ private readonly AvaloniaX11Platform _platform;
+ private readonly IntPtr _display;
+
+ public delegate void EventHandler(ref XEvent xev);
+ public int Fd { get; }
+ private readonly Dictionary _eventHandlers;
+
+ public X11EventDispatcher(AvaloniaX11Platform platform)
+ {
+ _platform = platform;
+ _display = platform.Display;
+ _eventHandlers = platform.Windows;
+ Fd = XLib.XConnectionNumber(_display);
+ }
+
+ public bool IsPending => XPending(_display) != 0;
+
+ public unsafe void DispatchX11Events(CancellationToken cancellationToken)
+ {
+ while (IsPending)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ XNextEvent(_display, out var xev);
+ if(XFilterEvent(ref xev, IntPtr.Zero))
+ continue;
+
+ if (xev.type == XEventName.GenericEvent)
+ XGetEventData(_display, &xev.GenericEventCookie);
+ try
+ {
+ if (xev.type == XEventName.GenericEvent)
+ {
+ if (_platform.XI2 != null && _platform.Info.XInputOpcode ==
+ xev.GenericEventCookie.extension)
+ {
+ _platform.XI2.OnEvent((XIEvent*)xev.GenericEventCookie.data);
+ }
+ }
+ else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler))
+ handler(ref xev);
+ }
+ finally
+ {
+ if (xev.type == XEventName.GenericEvent && xev.GenericEventCookie.data != null)
+ XFreeEventData(_display, &xev.GenericEventCookie);
+ }
+ }
+ Flush();
+ }
+
+ public void Flush() => XFlush(_display);
+}
\ No newline at end of file
diff --git a/src/Avalonia.X11/X11PlatformThreading.cs b/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs
similarity index 74%
rename from src/Avalonia.X11/X11PlatformThreading.cs
rename to src/Avalonia.X11/Dispatching/X11PlatformThreading.cs
index de494eb059..9cbfbe8a87 100644
--- a/src/Avalonia.X11/X11PlatformThreading.cs
+++ b/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs
@@ -12,11 +12,7 @@ namespace Avalonia.X11
internal unsafe class X11PlatformThreading : IControlledDispatcherImpl
{
private readonly AvaloniaX11Platform _platform;
- private readonly IntPtr _display;
-
- public delegate void EventHandler(ref XEvent xev);
- private readonly Dictionary _eventHandlers;
- private Thread _mainThread;
+ private Thread _mainThread = Thread.CurrentThread;
[StructLayout(LayoutKind.Explicit)]
private struct epoll_data
@@ -72,14 +68,12 @@ namespace Avalonia.X11
private long? _nextTimer;
private int _epoll;
private Stopwatch _clock = Stopwatch.StartNew();
+ private readonly X11EventDispatcher _x11Events;
public X11PlatformThreading(AvaloniaX11Platform platform)
{
_platform = platform;
- _display = platform.Display;
- _eventHandlers = platform.Windows;
- _mainThread = Thread.CurrentThread;
- var fd = XLib.XConnectionNumber(_display);
+ _x11Events = new X11EventDispatcher(platform);
var ev = new epoll_event()
{
events = EPOLLIN,
@@ -89,7 +83,7 @@ namespace Avalonia.X11
if (_epoll == -1)
throw new X11Exception("epoll_create1 failed");
- if (epoll_ctl(_epoll, EPOLL_CTL_ADD, fd, ref ev) == -1)
+ if (epoll_ctl(_epoll, EPOLL_CTL_ADD, _x11Events.Fd, ref ev) == -1)
throw new X11Exception("Unable to attach X11 connection handle to epoll");
var fds = stackalloc int[2];
@@ -117,40 +111,7 @@ namespace Avalonia.X11
Signaled?.Invoke();
}
-
- private unsafe void HandleX11(CancellationToken cancellationToken)
- {
- while (XPending(_display) != 0)
- {
- if (cancellationToken.IsCancellationRequested)
- return;
-
- XNextEvent(_display, out var xev);
- if(XFilterEvent(ref xev, IntPtr.Zero))
- continue;
-
- if (xev.type == XEventName.GenericEvent)
- XGetEventData(_display, &xev.GenericEventCookie);
- try
- {
- if (xev.type == XEventName.GenericEvent)
- {
- if (_platform.XI2 != null && _platform.Info.XInputOpcode ==
- xev.GenericEventCookie.extension)
- {
- _platform.XI2.OnEvent((XIEvent*)xev.GenericEventCookie.data);
- }
- }
- else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler))
- handler(ref xev);
- }
- finally
- {
- if (xev.type == XEventName.GenericEvent && xev.GenericEventCookie.data != null)
- XFreeEventData(_display, &xev.GenericEventCookie);
- }
- }
- }
+
public void RunLoop(CancellationToken cancellationToken)
{
@@ -166,9 +127,9 @@ namespace Avalonia.X11
return;
//Flush whatever requests were made to XServer
- XFlush(_display);
+ _x11Events.Flush();
epoll_event ev;
- if (XPending(_display) == 0)
+ if (!_x11Events.IsPending)
{
now = _clock.ElapsedMilliseconds;
if (_nextTimer < now)
@@ -190,7 +151,7 @@ namespace Avalonia.X11
if (cancellationToken.IsCancellationRequested)
return;
CheckSignaled();
- HandleX11(cancellationToken);
+ _x11Events.DispatchX11Events(cancellationToken);
while (_platform.EventGrouperDispatchQueue.HasJobs)
{
CheckSignaled();
@@ -238,6 +199,6 @@ namespace Avalonia.X11
public long Now => _clock.ElapsedMilliseconds;
public bool CanQueryPendingInput => true;
- public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || XPending(_display) != 0;
+ public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
}
}
diff --git a/src/Avalonia.X11/Interop/Glib.cs b/src/Avalonia.X11/Interop/Glib.cs
new file mode 100644
index 0000000000..fbc92960cd
--- /dev/null
+++ b/src/Avalonia.X11/Interop/Glib.cs
@@ -0,0 +1,192 @@
+#nullable enable
+using System;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Avalonia.Platform.Interop;
+using static Avalonia.X11.Interop.Glib;
+namespace Avalonia.X11.Interop;
+
+internal static unsafe class Glib
+{
+ private const string GlibName = "libglib-2.0.so.0";
+ private const string GObjectName = "libgobject-2.0.so.0";
+
+ [DllImport(GlibName)]
+ public static extern void g_slist_free(GSList* data);
+
+ [DllImport(GObjectName)]
+ private static extern void g_object_ref(IntPtr instance);
+
+ [DllImport(GObjectName)]
+ private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal,
+ IntPtr handler, IntPtr userData, int flags);
+
+ [DllImport(GObjectName)]
+ private static extern void g_object_unref(IntPtr instance);
+
+ [DllImport(GObjectName)]
+ private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId);
+
+ public const int G_PRIORITY_HIGH = -100;
+ public const int G_PRIORITY_DEFAULT = 0;
+ public const int G_PRIORITY_HIGH_IDLE = 100;
+ public const int G_PRIORITY_DEFAULT_IDLE = 200;
+
+ [DllImport(GlibName)]
+ public static extern IntPtr g_main_loop_new(IntPtr context, int is_running);
+
+ [DllImport(GlibName)]
+ public static extern void g_main_loop_quit(IntPtr loop);
+
+ [DllImport(GlibName)]
+ public static extern void g_main_loop_run(IntPtr loop);
+
+ [DllImport(GlibName)]
+ public static extern void g_main_loop_unref(IntPtr loop);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate void GOnceSourceFunc(IntPtr userData);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate int GSourceFunc(IntPtr userData);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate void GDestroyNotify(IntPtr userData);
+
+ [DllImport(GlibName)]
+ private static extern int g_idle_add_once(GOnceSourceFunc cb, IntPtr userData);
+
+ private static readonly GOnceSourceFunc s_onceSourceCb = (userData) =>
+ {
+ var h = GCHandle.FromIntPtr(userData);
+ var cb = (Action)h.Target!;
+
+ h.Free();
+ cb();
+ };
+
+ public static void g_idle_add_once(Action cb) =>
+ g_idle_add_once(s_onceSourceCb, GCHandle.ToIntPtr(GCHandle.Alloc(cb)));
+
+ [DllImport(GlibName)]
+ private static extern uint g_timeout_add_once(uint interval, GOnceSourceFunc cb, IntPtr userData);
+
+ public static uint g_timeout_add_once(uint interval, Action cb) =>
+ g_timeout_add_once(interval, s_onceSourceCb, GCHandle.ToIntPtr(GCHandle.Alloc(cb)));
+
+ private static readonly GDestroyNotify s_gcHandleDestroyNotify = handle => GCHandle.FromIntPtr(handle).Free();
+
+ private static readonly GSourceFunc s_sourceFuncDispatchCallback =
+ handle => ((Func)GCHandle.FromIntPtr(handle).Target)() ? 1 : 0;
+
+ [DllImport(GlibName)]
+ private static extern uint g_idle_add_full (int priority, GSourceFunc function, IntPtr data, GDestroyNotify notify);
+
+ public static uint g_idle_add_full(int priority, Func callback)
+ => g_idle_add_full(priority, s_sourceFuncDispatchCallback, GCHandle.ToIntPtr(GCHandle.Alloc(callback)),
+ s_gcHandleDestroyNotify);
+
+ [DllImport(GlibName)]
+ public static extern int g_source_get_can_recurse (IntPtr source);
+
+ [DllImport(GlibName)]
+ public static extern void g_source_set_can_recurse (IntPtr source, int can_recurse);
+
+ [DllImport(GlibName)]
+ public static extern IntPtr g_main_context_find_source_by_id (IntPtr context, uint source_id);
+
+ [Flags]
+ public enum GIOCondition
+ {
+ G_IO_IN = 1,
+ G_IO_OUT = 4,
+ G_IO_PRI = 2,
+ G_IO_ERR = 8,
+ G_IO_HUP = 16,
+ G_IO_NVAL = 32
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate int GUnixFDSourceFunc(int fd, GIOCondition condition, IntPtr user_data);
+
+ private static readonly GUnixFDSourceFunc s_unixFdSourceCallback = (fd, cond, handle) =>
+ ((Func)GCHandle.FromIntPtr(handle).Target)(fd, cond) ? 1 : 0;
+
+ [DllImport(GlibName)]
+ public static extern uint g_unix_fd_add_full (int priority,
+ int fd,
+ GIOCondition condition,
+ GUnixFDSourceFunc function,
+ IntPtr user_data,
+ GDestroyNotify notify);
+
+ public static uint g_unix_fd_add_full(int priority, int fd, GIOCondition condition,
+ Func cb) =>
+ g_unix_fd_add_full(priority, fd, condition, s_unixFdSourceCallback, GCHandle.ToIntPtr(GCHandle.Alloc(cb)),
+ s_gcHandleDestroyNotify);
+
+ [DllImport(GlibName)]
+ public static extern int g_source_remove (uint tag);
+
+ private class ConnectedSignal : IDisposable
+ {
+ private readonly IntPtr _instance;
+ private GCHandle _handle;
+ private readonly ulong _id;
+
+ public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id)
+ {
+ _instance = instance;
+ g_object_ref(instance);
+ _handle = handle;
+ _id = id;
+ }
+
+ public void Dispose()
+ {
+ if (_handle.IsAllocated)
+ {
+ g_signal_handler_disconnect(_instance, _id);
+ g_object_unref(_instance);
+ _handle.Free();
+ }
+ }
+ }
+
+ public static IDisposable ConnectSignal(IntPtr obj, string name, T handler)
+ {
+ var handle = GCHandle.Alloc(handler);
+ var ptr = Marshal.GetFunctionPointerForDelegate(handler);
+ using (var utf = new Utf8Buffer(name))
+ {
+ var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0);
+ if (id == 0)
+ throw new ArgumentException("Unable to connect to signal " + name);
+ return new ConnectedSignal(obj, handle, id);
+ }
+ }
+
+ public static Task RunOnGlibThread(Func action)
+ {
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ g_timeout_add_once(0, () =>
+ {
+ try
+ {
+ tcs.SetResult(action());
+ }
+ catch (Exception e)
+ {
+ tcs.TrySetException(e);
+ }
+ });
+ return tcs.Task;
+ }
+}
+
+[StructLayout(LayoutKind.Sequential)]
+internal unsafe struct GSList
+{
+ public readonly IntPtr Data;
+ public readonly GSList* Next;
+}
\ No newline at end of file
diff --git a/src/Avalonia.X11/Interop/GtkInteropHelper.cs b/src/Avalonia.X11/Interop/GtkInteropHelper.cs
index de0b755832..3560b20b7e 100644
--- a/src/Avalonia.X11/Interop/GtkInteropHelper.cs
+++ b/src/Avalonia.X11/Interop/GtkInteropHelper.cs
@@ -10,6 +10,6 @@ public class GtkInteropHelper
{
if (!await NativeDialogs.Gtk.StartGtk().ConfigureAwait(false))
throw new Win32Exception("Unable to initialize GTK");
- return await NativeDialogs.Glib.RunOnGlibThread(cb).ConfigureAwait(false);
+ return await Glib.RunOnGlibThread(cb).ConfigureAwait(false);
}
}
\ No newline at end of file
diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs
index 14bd1142dd..97874d71f0 100644
--- a/src/Avalonia.X11/NativeDialogs/Gtk.cs
+++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs
@@ -3,131 +3,13 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Platform.Interop;
+using Avalonia.Threading;
+using Avalonia.X11.Dispatching;
+using Avalonia.X11.Interop;
// ReSharper disable IdentifierTypo
namespace Avalonia.X11.NativeDialogs
{
- internal static unsafe class Glib
- {
- private const string GlibName = "libglib-2.0.so.0";
- private const string GObjectName = "libgobject-2.0.so.0";
-
- [DllImport(GlibName)]
- public static extern void g_slist_free(GSList* data);
-
- [DllImport(GObjectName)]
- private static extern void g_object_ref(IntPtr instance);
-
- [DllImport(GObjectName)]
- private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal,
- IntPtr handler, IntPtr userData, int flags);
-
- [DllImport(GObjectName)]
- private static extern void g_object_unref(IntPtr instance);
-
- [DllImport(GObjectName)]
- private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId);
-
- private delegate bool timeout_callback(IntPtr data);
-
- [DllImport(GlibName)]
- private static extern ulong g_timeout_add_full(int prio, uint interval, timeout_callback callback, IntPtr data,
- IntPtr destroy);
-
-
- private class ConnectedSignal : IDisposable
- {
- private readonly IntPtr _instance;
- private GCHandle _handle;
- private readonly ulong _id;
-
- public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id)
- {
- _instance = instance;
- g_object_ref(instance);
- _handle = handle;
- _id = id;
- }
-
- public void Dispose()
- {
- if (_handle.IsAllocated)
- {
- g_signal_handler_disconnect(_instance, _id);
- g_object_unref(_instance);
- _handle.Free();
- }
- }
- }
-
- public static IDisposable ConnectSignal(IntPtr obj, string name, T handler)
- {
- var handle = GCHandle.Alloc(handler);
- var ptr = Marshal.GetFunctionPointerForDelegate(handler);
- using (var utf = new Utf8Buffer(name))
- {
- var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0);
- if (id == 0)
- throw new ArgumentException("Unable to connect to signal " + name);
- return new ConnectedSignal(obj, handle, id);
- }
- }
-
-
- private static bool TimeoutHandler(IntPtr data)
- {
- var handle = GCHandle.FromIntPtr(data);
- var cb = (Func)handle.Target;
- if (!cb())
- {
- handle.Free();
- return false;
- }
-
- return true;
- }
-
- private static readonly timeout_callback s_pinnedHandler;
-
- static Glib()
- {
- s_pinnedHandler = TimeoutHandler;
- }
-
- private static void AddTimeout(int priority, uint interval, Func callback)
- {
- var handle = GCHandle.Alloc(callback);
- g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero);
- }
-
- public static Task RunOnGlibThread(Func action)
- {
- var tcs = new TaskCompletionSource();
- AddTimeout(0, 0, () =>
- {
-
- try
- {
- tcs.SetResult(action());
- }
- catch (Exception e)
- {
- tcs.TrySetException(e);
- }
-
- return false;
- });
- return tcs.Task;
- }
- }
-
- [StructLayout(LayoutKind.Sequential)]
- internal unsafe struct GSList
- {
- public readonly IntPtr Data;
- public readonly GSList* Next;
- }
-
internal enum GtkFileChooserAction
{
Open,
@@ -247,6 +129,9 @@ namespace Avalonia.X11.NativeDialogs
[DllImport(GdkName)]
private static extern IntPtr gdk_display_get_default();
+
+ [DllImport(GdkName)]
+ private static extern IntPtr gdk_x11_display_get_xdisplay(IntPtr display);
[DllImport(GtkName)]
private static extern IntPtr gtk_application_new(Utf8Buffer appId, int flags);
@@ -265,10 +150,21 @@ namespace Avalonia.X11.NativeDialogs
return s_startGtkTask ??= StartGtkCore();
}
- private static void GtkThread(TaskCompletionSource tcs)
+ static bool InitializeGtk()
{
try
{
+ // Check if GTK was already initialized
+ var existingDisplay = gdk_display_get_default();
+ if (existingDisplay != IntPtr.Zero)
+ {
+ if (gdk_x11_display_get_xdisplay(existingDisplay) == IntPtr.Zero)
+ return false;
+ s_display = existingDisplay;
+ return true;
+
+ }
+
try
{
using (var backends = new Utf8Buffer("x11"))
@@ -284,8 +180,7 @@ namespace Avalonia.X11.NativeDialogs
if (!gtk_init_check(0, IntPtr.Zero))
{
- tcs.SetResult(false);
- return;
+ return false;
}
IntPtr app;
@@ -293,26 +188,51 @@ namespace Avalonia.X11.NativeDialogs
app = gtk_application_new(utf, 0);
if (app == IntPtr.Zero)
{
- tcs.SetResult(false);
- return;
+ return false;
}
s_display = gdk_display_get_default();
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static void GtkThread(TaskCompletionSource tcs)
+ {
+ try
+ {
+ if (!InitializeGtk())
+ {
+ tcs.SetResult(false);
+ return;
+ }
+
tcs.SetResult(true);
while (true)
gtk_main_iteration();
}
catch
{
- tcs.SetResult(false);
+ tcs.TrySetResult(false);
}
}
private static Task StartGtkCore()
{
- var tcs = new TaskCompletionSource();
- new Thread(() => GtkThread(tcs)) {Name = "GTK3THREAD", IsBackground = true}.Start();
- return tcs.Task;
+ if (AvaloniaLocator.Current.GetService()?.UseGLibMainLoop == true)
+ {
+ return Task.FromResult(InitializeGtk());
+ }
+ else
+ {
+ var tcs = new TaskCompletionSource();
+ new Thread(() => GtkThread(tcs)) { Name = "GTK3THREAD", IsBackground = true }.Start();
+ return tcs.Task;
+ }
}
}
}
diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
index e7ff04a2dd..568875c507 100644
--- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
+++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
@@ -10,7 +10,7 @@ using Avalonia.Platform;
using Avalonia.Platform.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
-using static Avalonia.X11.NativeDialogs.Glib;
+using static Avalonia.X11.Interop.Glib;
using static Avalonia.X11.NativeDialogs.Gtk;
namespace Avalonia.X11.NativeDialogs
diff --git a/src/Avalonia.X11/X11FocusProxy.cs b/src/Avalonia.X11/X11FocusProxy.cs
index 4fdaf643ab..4856dbef65 100644
--- a/src/Avalonia.X11/X11FocusProxy.cs
+++ b/src/Avalonia.X11/X11FocusProxy.cs
@@ -26,7 +26,7 @@ namespace Avalonia.X11
internal IntPtr _handle;
private readonly AvaloniaX11Platform _platform;
- private readonly X11PlatformThreading.EventHandler _ownerEventHandler;
+ private readonly X11EventDispatcher.EventHandler _ownerEventHandler;
///
/// Initializes instance and creates the underlying X window.
@@ -36,7 +36,7 @@ namespace Avalonia.X11
/// The parent window to proxy the focus for.
/// An event handler that will handle X events that come to the proxy.
internal X11FocusProxy(AvaloniaX11Platform platform, IntPtr parent,
- X11PlatformThreading.EventHandler eventHandler)
+ X11EventDispatcher.EventHandler eventHandler)
{
_handle = PrepareXWindow(platform.Info.Display, parent);
_platform = platform;
diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs
index 64ff98e7c7..4b1b3731b7 100644
--- a/src/Avalonia.X11/X11Platform.cs
+++ b/src/Avalonia.X11/X11Platform.cs
@@ -15,6 +15,7 @@ using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Vulkan;
using Avalonia.X11;
+using Avalonia.X11.Dispatching;
using Avalonia.X11.Glx;
using Avalonia.X11.Vulkan;
using Avalonia.X11.Screens;
@@ -26,8 +27,7 @@ namespace Avalonia.X11
{
private Lazy _keyboardDevice = new Lazy(() => new KeyboardDevice());
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
- public Dictionary Windows { get; } =
- new Dictionary();
+ public Dictionary Windows { get; } = new ();
public XI2Manager XI2 { get; private set; }
public X11Info Info { get; private set; }
public X11Screens X11Screens { get; private set; }
@@ -73,7 +73,9 @@ namespace Avalonia.X11
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind().ToConstant(this)
- .Bind().ToConstant(new X11PlatformThreading(this))
+ .Bind().ToConstant(options.UseGLibMainLoop
+ ? new GlibDispatcherImpl(this)
+ : new X11PlatformThreading(this))
.Bind().ToConstant(timer)
.Bind().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Control))
.Bind().ToConstant(new KeyGestureFormatInfo(new Dictionary() { }, meta: "Super"))
@@ -370,6 +372,22 @@ namespace Avalonia
///
public bool? UseRetainedFramebuffer { get; set; }
+ ///
+ /// If this option is set to true, GMainLoop and GSource based dispatcher implementation will be used instead
+ /// of epoll-based one.
+ /// Use this if you need to use GLib-based libraries on the main thread
+ ///
+ public bool UseGLibMainLoop { get; set; }
+
+ ///
+ /// If Avalonia is in control of a run loop, we propagate exceptions by stopping the run loop frame
+ /// and rethrowing an exception. However, if there is no Avalonia-controlled run loop frame,
+ /// there is no way to report such exceptions, since allowing those to escape native->managed call boundary
+ /// will likely brick GLib machinery since it's not aware of managed Exceptions
+ /// This property allows to inspect such exceptions before they will be ignored
+ ///
+ public Action? ExterinalGLibMainLoopExceptionLogger { get; set; }
+
public X11PlatformOptions()
{
try
diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs
index 03dad8e0e2..33daacf936 100644
--- a/src/Avalonia.X11/XLib.cs
+++ b/src/Avalonia.X11/XLib.cs
@@ -733,7 +733,7 @@ namespace Avalonia.X11
}
}
- public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11PlatformThreading.EventHandler handler)
+ public static IntPtr CreateEventWindow(AvaloniaX11Platform plat, X11EventDispatcher.EventHandler handler)
{
var win = XCreateSimpleWindow(plat.Display, plat.Info.DefaultRootWindow,
0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);