From 4733e99c82f2ee5356c1e1fd22ce7d82b7225b7f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 16 Dec 2024 12:14:40 +0500 Subject: [PATCH] Implemented XEmbed client support with GtkSharp usage example --- Avalonia.Desktop.slnf | 1 + Avalonia.sln | 6 + samples/XEmbedSample/HarfbuzzWorkaround.cs | 66 ++++++ samples/XEmbedSample/Program.cs | 63 ++++++ samples/XEmbedSample/SocketEx.cs | 64 ++++++ samples/XEmbedSample/XEmbedSample.csproj | 20 ++ .../Dispatching/GLibDispatcherImpl.cs | 6 +- .../Dispatching/IX11PlatformDispatcher.cs | 8 + .../Dispatching/X11PlatformThreading.cs | 4 +- src/Avalonia.X11/X11Window.cs | 75 +++---- .../X11WindowModes/DefaultWindowMode.cs | 53 +++++ .../X11WindowModes/InputProxyWindowMode.cs | 55 +++++ src/Avalonia.X11/X11WindowModes/WindowMode.cs | 60 +++++ .../X11WindowModes/XEmbedClientWindowMode.cs | 207 ++++++++++++++++++ src/Avalonia.X11/XEmbedPlug.cs | 81 +++++++ 15 files changed, 722 insertions(+), 47 deletions(-) create mode 100644 samples/XEmbedSample/HarfbuzzWorkaround.cs create mode 100644 samples/XEmbedSample/Program.cs create mode 100644 samples/XEmbedSample/SocketEx.cs create mode 100644 samples/XEmbedSample/XEmbedSample.csproj create mode 100644 src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs create mode 100644 src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs create mode 100644 src/Avalonia.X11/X11WindowModes/InputProxyWindowMode.cs create mode 100644 src/Avalonia.X11/X11WindowModes/WindowMode.cs create mode 100644 src/Avalonia.X11/X11WindowModes/XEmbedClientWindowMode.cs create mode 100644 src/Avalonia.X11/XEmbedPlug.cs diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 06a7261b1c2..59cc49543ae 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -15,6 +15,7 @@ "samples\\Sandbox\\Sandbox.csproj", "samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContextPlug\\UnloadableAssemblyLoadContextPlug.csproj", "samples\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext\\UnloadableAssemblyLoadContext.csproj", + "samples\\XEmbedSample\\XEmbedSample.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index 98436f68968..cd7e56e9d44 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -302,6 +302,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{0097673D-DBCE-476E-82FE-E78A56E58AA2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -707,6 +708,10 @@ Global {0097673D-DBCE-476E-82FE-E78A56E58AA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.ActiveCfg = Release|Any CPU {0097673D-DBCE-476E-82FE-E78A56E58AA2}.Release|Any CPU.Build.0 = Release|Any CPU + {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -795,6 +800,7 @@ Global {DA5F1FF9-4259-4C54-B443-85CFA226EE6A} = {9CCA131B-DE95-4D44-8788-C3CAE28574CD} {9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {0097673D-DBCE-476E-82FE-E78A56E58AA2} = {B39A8919-9F95-48FE-AD7B-76E08B509888} + {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/XEmbedSample/HarfbuzzWorkaround.cs b/samples/XEmbedSample/HarfbuzzWorkaround.cs new file mode 100644 index 00000000000..f0ecf0699a3 --- /dev/null +++ b/samples/XEmbedSample/HarfbuzzWorkaround.cs @@ -0,0 +1,66 @@ +using System.Runtime.InteropServices; + +namespace XEmbedSample; + +/* + This is needed specifically for GtkSharp: + https://github.com/mono/SkiaSharp/issues/3038 + https://github.com/GtkSharp/GtkSharp/issues/443 + + Instead of using plain DllImport they are manually calling dlopen with RTLD_GLOBAL and RTLD_LAZY flags: + https://github.com/GtkSharp/GtkSharp/blob/b7303616129ab5a0ca64def45649ab522d83fa4a/Source/Libs/Shared/FuncLoader.cs#L80-L92 + + Which causes libHarfBuzzSharp.so from HarfBuzzSharp to resolve some of the symbols from the system libharfbuzz.so.0 + which is a _different_ harfbuzz version. + + That results in a segfault. + + Previously there was a workaround - https://github.com/mono/SkiaSharp/pull/2247 but it got + disabled for .NET Core / .NET 5+. + + Why linux linker builds shared libraries in a way that makes it possible for them to resolve their own symbols from + elsewhere escapes me. + + Here we are loading libHarfBuzzSharp.so from the .NET-resolved location, saving it, unloading the library + and then defining a custom resolver that would call dlopen with RTLD_NOW + RTLD_DEEPBIND + + */ + +public unsafe class HarfbuzzWorkaround +{ + [DllImport("libc")] + static extern int dlinfo(IntPtr handle, int request, IntPtr info); + + [DllImport("libc")] + static extern IntPtr dlopen(string filename, int flags); + + private const int RTLD_DI_ORIGIN = 6; + private const int RTLD_NOW = 2; + private const int RTLD_DEEPBIND = 8; + + public static void Apply() + { + if (RuntimeInformation.RuntimeIdentifier.Contains("musl")) + throw new PlatformNotSupportedException("musl doesn't support RTLD_DEEPBIND"); + + var libraryPathBytes = Marshal.AllocHGlobal(4096); + var handle = NativeLibrary.Load("libHarfBuzzSharp", typeof(HarfBuzzSharp.Blob).Assembly, null); + dlinfo(handle, RTLD_DI_ORIGIN, libraryPathBytes); + var libraryOrigin = Marshal.PtrToStringUTF8(libraryPathBytes); + Marshal.FreeHGlobal(libraryPathBytes); + var libraryPath = Path.Combine(libraryOrigin, "libHarfBuzzSharp.so"); + + NativeLibrary.Free(handle); + var forceLoadedHandle = dlopen(libraryPath, RTLD_NOW | RTLD_DEEPBIND); + if (forceLoadedHandle == IntPtr.Zero) + throw new DllNotFoundException($"Unable to load {libraryPath} via dlopen"); + + NativeLibrary.SetDllImportResolver(typeof(HarfBuzzSharp.Blob).Assembly, (name, assembly, searchPath) => + { + if (name.Contains("HarfBuzzSharp")) + return dlopen(libraryPath, RTLD_NOW | RTLD_DEEPBIND); + return NativeLibrary.Load(name, assembly, searchPath); + }); + + } +} \ No newline at end of file diff --git a/samples/XEmbedSample/Program.cs b/samples/XEmbedSample/Program.cs new file mode 100644 index 00000000000..d5cf8be0398 --- /dev/null +++ b/samples/XEmbedSample/Program.cs @@ -0,0 +1,63 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using ControlCatalog; +using ControlCatalog.Models; +using Gtk; + +namespace XEmbedSample; + +class Program +{ + static void Main(string[] args) + { + HarfbuzzWorkaround.Apply(); + AppBuilder.Configure() + .UseSkia() + .With(new X11PlatformOptions() + { + UseGLibMainLoop = true, + ExterinalGLibMainLoopExceptionLogger = e => Console.WriteLine(e.ToString()) + }) + .UseX11() + .SetupWithoutStarting(); + App.SetCatalogThemes(CatalogTheme.Fluent); + Gdk.Global.AllowedBackends = "x11"; + Gtk.Application.Init("myapp", ref args); + + + + + + var w = new Gtk.Window("XEmbed Test Window"); + var socket = new AvaloniaXEmbedGtkSocket(w.StyleContext.GetBackgroundColor(StateFlags.Normal)) + { + Content = new ScrollViewer() + { + Content = new ControlCatalog.Pages.TextBoxPage(), + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto + } + }; + var vbox = new Gtk.Box(Gtk.Orientation.Vertical, 5); + var label = new Gtk.Label("Those are GTK controls"); + vbox.Add(label); + vbox.Add(new Gtk.Entry()); + vbox.Add(new Gtk.Button(new Gtk.Label("Do nothing"))); + vbox.PackEnd(socket, true, true, 0); + socket.HeightRequest = 400; + socket.WidthRequest = 400; + w.Add(vbox); + socket.Realize(); + + + w.AddSignalHandler("destroy", new EventHandler((_, __) => + { + Gtk.Application.Quit(); + socket.Destroy(); + })); + w.ShowAll(); + Gtk.Application.Run(); + + } +} \ No newline at end of file diff --git a/samples/XEmbedSample/SocketEx.cs b/samples/XEmbedSample/SocketEx.cs new file mode 100644 index 00000000000..3a4d820978b --- /dev/null +++ b/samples/XEmbedSample/SocketEx.cs @@ -0,0 +1,64 @@ +using Avalonia; +using Avalonia.X11; +using Gdk; +using Color = Cairo.Color; + +namespace XEmbedSample; + +public class AvaloniaXEmbedGtkSocket : Gtk.Socket +{ + private readonly RGBA _backgroundColor; + private XEmbedPlug? _avaloniaPlug; + public AvaloniaXEmbedGtkSocket(RGBA backgroundColor) + { + _backgroundColor = backgroundColor; + } + + private object _content; + public object Content + { + get => _content; + set + { + _content = value; + if (_avaloniaPlug != null) + _avaloniaPlug.Content = _content; + } + } + + protected override void OnRealized() + { + base.OnRealized(); + _avaloniaPlug ??= XEmbedPlug.Create(); + _avaloniaPlug.ScaleFactor = ScaleFactor; + _avaloniaPlug.BackgroundColor = Avalonia.Media.Color.FromRgb((byte)(_backgroundColor.Red * 255), + (byte)(_backgroundColor.Green * 255), + (byte)(_backgroundColor.Blue * 255) + ); + _avaloniaPlug.Content = _content; + ApplyInteractiveResize(); + AddId((ulong)_avaloniaPlug.Handle); + } + + void ApplyInteractiveResize() + { + // This is _NOT_ a part of XEmbed, but allows us to have smooth resize + GetAllocatedSize(out var rect, out _); + var scale = ScaleFactor; + _avaloniaPlug?.ProcessInteractiveResize(new PixelSize(rect.Width * scale, rect.Height * scale)); + } + + protected override void OnSizeAllocated(Rectangle allocation) + { + base.OnSizeAllocated(allocation); + Display.Default.Sync(); + ApplyInteractiveResize(); + } + + protected override void OnDestroyed() + { + _avaloniaPlug?.Dispose(); + _avaloniaPlug = null; + base.OnDestroyed(); + } +} \ No newline at end of file diff --git a/samples/XEmbedSample/XEmbedSample.csproj b/samples/XEmbedSample/XEmbedSample.csproj new file mode 100644 index 00000000000..01ea7432db0 --- /dev/null +++ b/samples/XEmbedSample/XEmbedSample.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + true + + + + + + + + + + + + diff --git a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs index 29f2836594b..bf5ffa370d1 100644 --- a/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs +++ b/src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs @@ -12,7 +12,8 @@ namespace Avalonia.X11.Dispatching; internal class GlibDispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing, - IControlledDispatcherImpl + IControlledDispatcherImpl, + IX11PlatformDispatcher { /* GLib priorities and Avalonia priorities are a bit different. Avalonia follows the WPF model when there @@ -309,5 +310,6 @@ public void Dispose() } } } - + + public X11EventDispatcher EventDispatcher => _x11Events; } \ No newline at end of file diff --git a/src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs b/src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs new file mode 100644 index 00000000000..81d1c7128a3 --- /dev/null +++ b/src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs @@ -0,0 +1,8 @@ +using Avalonia.Threading; + +namespace Avalonia.X11.Dispatching; + +interface IX11PlatformDispatcher : IDispatcherImpl +{ + X11EventDispatcher EventDispatcher { get; } +} \ No newline at end of file diff --git a/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs b/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs index 9cbfbe8a878..529e2cd8dc5 100644 --- a/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs +++ b/src/Avalonia.X11/Dispatching/X11PlatformThreading.cs @@ -5,11 +5,12 @@ using System.Threading; using Avalonia.Platform; using Avalonia.Threading; +using Avalonia.X11.Dispatching; using static Avalonia.X11.XLib; namespace Avalonia.X11 { - internal unsafe class X11PlatformThreading : IControlledDispatcherImpl + internal unsafe class X11PlatformThreading : IControlledDispatcherImpl, IX11PlatformDispatcher { private readonly AvaloniaX11Platform _platform; private Thread _mainThread = Thread.CurrentThread; @@ -200,5 +201,6 @@ public void UpdateTimer(long? dueTimeInMs) public bool CanQueryPendingInput => true; public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending; + public X11EventDispatcher EventDispatcher => _x11Events; } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 7ad0cd44615..c68c55e0421 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -69,7 +69,7 @@ internal unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client, private bool _useRenderWindow = false; private bool _useCompositorDrivenRenderWindowResize = false; private bool _usePositioningFlags = false; - private X11FocusProxy? _focusProxy; + private X11WindowMode _mode; private enum XSyncState { @@ -77,10 +77,23 @@ private enum XSyncState WaitConfigure, WaitPaint } - + public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool overrideRedirect = false) + : this(platform, popupParent, + platform.Options.EnableInputFocusProxy + ? new InputProxyWindowMode() + : new DefaultTopLevelWindowMode(), + overrideRedirect) + { + + } + + public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, X11WindowMode mode, + bool overrideRedirect = false) { _platform = platform; + _mode = mode; + _mode.Init(this); _popup = popupParent != null; _overrideRedirect = _popup || overrideRedirect; _x11 = platform.Info; @@ -168,12 +181,8 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov _renderHandle = _handle; Handle = new PlatformHandle(_handle, "XID"); - - if (platform.Options.EnableInputFocusProxy) - { - _focusProxy = new X11FocusProxy(platform, _handle, OnEvent); - SetWmClass(_focusProxy._handle, "FocusProxy"); - } + + _mode.OnHandleCreated(_handle); _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; @@ -230,9 +239,8 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov InitializeIme(); var data = new List { _x11.Atoms.WM_DELETE_WINDOW, _x11.Atoms._NET_WM_SYNC_REQUEST }; - - if(platform.Options.EnableInputFocusProxy) - data.Add(_x11.Atoms.WM_TAKE_FOCUS); + + _mode.AppendWmProtocols(data); XChangeProperty(_x11.Display, _handle, _x11.Atoms.WM_PROTOCOLS, _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, data.ToArray(), data.Count); @@ -446,6 +454,9 @@ private void OnEvent(ref XEvent ev) { if (_inputRoot is null) return; + + if(_mode.OnEvent(ref ev)) + return; if (ev.type == XEventName.MapNotify) { @@ -588,6 +599,7 @@ private void OnEvent(ref XEvent ev) else if (ev.type == XEventName.DestroyNotify && ev.DestroyWindowEvent.window == _handle) { + _mode.OnDestroyNotify(); Cleanup(true); } else if (ev.type == XEventName.ClientMessage) @@ -605,11 +617,6 @@ private void OnEvent(ref XEvent ev) _xSyncValue.Hi = ev.ClientMessageEvent.ptr4.ToInt32(); _xSyncState = XSyncState.WaitConfigure; } - else if (ev.ClientMessageEvent.ptr1 == _x11.Atoms.WM_TAKE_FOCUS && _platform.Options.EnableInputFocusProxy) - { - IntPtr time = ev.ClientMessageEvent.ptr2; - XSetInputFocus(_x11.Display, _focusProxy!._handle, RevertTo.Parent, time); - } } } else if (ev.type == XEventName.KeyPress || ev.type == XEventName.KeyRelease) @@ -1004,7 +1011,7 @@ private void Cleanup(bool fromDestroyNotification) _handle = IntPtr.Zero; _mouse.Dispose(); _touch.Dispose(); - if (!fromDestroyNotification) + if (!fromDestroyNotification) XDestroyWindow(_x11.Display, handle); } @@ -1014,8 +1021,6 @@ private void Cleanup(bool fromDestroyNotification) { _renderHandle = IntPtr.Zero; } - - _focusProxy?.Cleanup(); } private bool ActivateTransientChildIfNeeded() @@ -1039,18 +1044,14 @@ public void SetParent(IWindowImpl? parent) public void Show(bool activate, bool isDialog) { - _wasMappedAtLeastOnce = true; - XMapWindow(_x11.Display, _handle); - XFlush(_x11.Display); + _mode.Show(activate, isDialog); } - public void Hide() => XUnmapWindow(_x11.Display, _handle); - - public Point PointToClient(PixelPoint point) => new Point((point.X - (_position ?? default).X) / RenderScaling, (point.Y - (_position ?? default).Y) / RenderScaling); + public void Hide() => _mode.Hide(); + + public Point PointToClient(PixelPoint point) => _mode.PointToClient(point); - public PixelPoint PointToScreen(Point point) => new PixelPoint( - (int)(point.X * RenderScaling + (_position ?? default).X), - (int)(point.Y * RenderScaling + (_position ?? default).Y)); + public PixelPoint PointToScreen(Point point) => _mode.PointToScreen(point); public void SetSystemDecorations(SystemDecorations enabled) { @@ -1168,21 +1169,7 @@ public PixelPoint Position public IPopupImpl? CreatePopup() => _platform.Options.OverlayPopups ? null : new X11Window(_platform, this); - public void Activate() - { - if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero) - { - SendNetWMMessage(_x11.Atoms._NET_ACTIVE_WINDOW, (IntPtr)1, _x11.LastActivityTimestamp, - IntPtr.Zero); - } - else - { - XRaiseWindow(_x11.Display, _handle); - - if (_focusProxy is not null) - XSetInputFocus(_x11.Display, _focusProxy._handle, 0, IntPtr.Zero); - } - } + public void Activate() => _mode.Activate(); public Size MaxAutoSizeHint => _platform.X11Screens.AllScreens.Select(s => s.Bounds.Size.ToSize(s.Scaling)) .OrderByDescending(x => x.Width + x.Height).FirstOrDefault(); @@ -1456,7 +1443,7 @@ public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } public bool NeedsManagedDecorations => false; - public bool IsEnabled => !_disabled; + public bool IsEnabled => !_disabled && !_mode.BlockInput; public class SurfacePlatformHandle : INativePlatformHandleSurface { diff --git a/src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs b/src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs new file mode 100644 index 00000000000..d155a7f2c15 --- /dev/null +++ b/src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs @@ -0,0 +1,53 @@ +using System; + +namespace Avalonia.X11; + +using static XLib; +partial class X11Window +{ + public class DefaultTopLevelWindowMode : X11WindowMode + { + public override void Activate() + { + if (X11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero) + { + Window.SendNetWMMessage(X11.Atoms._NET_ACTIVE_WINDOW, (IntPtr)1, X11.LastActivityTimestamp, + IntPtr.Zero); + } + else + { + XRaiseWindow(X11.Display, Handle); + OnManualXRaiseWindow(); + } + + base.Activate(); + } + + protected virtual void OnManualXRaiseWindow() + { + + } + + public override void Show(bool activate, bool isDialog) + { + Window._wasMappedAtLeastOnce = true; + XMapWindow(X11.Display, Handle); + XFlush(X11.Display); + base.Show(activate, isDialog); + } + + public override void Hide() + { + XUnmapWindow(X11.Display, Handle); + base.Hide(); + } + + public override Point PointToClient(PixelPoint point) => new Point( + (point.X - (Window._position ?? default).X) / Window.RenderScaling, + (point.Y - (Window._position ?? default).Y) / Window.RenderScaling); + + public override PixelPoint PointToScreen(Point point) => new PixelPoint( + (int)(point.X * Window.RenderScaling + (Window._position ?? default).X), + (int)(point.Y * Window.RenderScaling + (Window._position ?? default).Y)); + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11WindowModes/InputProxyWindowMode.cs b/src/Avalonia.X11/X11WindowModes/InputProxyWindowMode.cs new file mode 100644 index 00000000000..7716cd1464e --- /dev/null +++ b/src/Avalonia.X11/X11WindowModes/InputProxyWindowMode.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.X11; +using static XLib; + +partial class X11Window +{ + public class InputProxyWindowMode : DefaultTopLevelWindowMode + { + private X11FocusProxy _focusProxy; + + public override void OnHandleCreated(IntPtr handle) + { + _focusProxy = new X11FocusProxy(Platform, handle, OnFocusProxyEvent); + Window.SetWmClass(_focusProxy._handle, "FocusProxy"); + base.OnHandleCreated(handle); + } + + public override bool OnEvent(ref XEvent ev) + { + if (ev.type == XEventName.ClientMessage && ev.ClientMessageEvent.ptr1 == X11.Atoms.WM_TAKE_FOCUS) + { + XSetInputFocus(X11.Display, _focusProxy!._handle, RevertTo.Parent, ev.ClientMessageEvent.ptr2); + } + return base.OnEvent(ref ev); + } + + void OnFocusProxyEvent(ref XEvent xev) + { + + } + + protected override void OnManualXRaiseWindow() + { + if (_focusProxy is not null) + XSetInputFocus(X11.Display, _focusProxy._handle, 0, IntPtr.Zero); + base.OnManualXRaiseWindow(); + } + + + public override void OnDestroyNotify() + { + _focusProxy?.Cleanup(); + _focusProxy = null; + base.OnDestroyNotify(); + } + + public override void AppendWmProtocols(List data) + { + data.Add(X11.Atoms.WM_TAKE_FOCUS); + base.AppendWmProtocols(data); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11WindowModes/WindowMode.cs b/src/Avalonia.X11/X11WindowModes/WindowMode.cs new file mode 100644 index 00000000000..67dff91131e --- /dev/null +++ b/src/Avalonia.X11/X11WindowModes/WindowMode.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.X11; + +partial class X11Window +{ + public abstract class X11WindowMode + { + public X11Window Window { get; private set; } + protected IntPtr Display; + protected X11Info X11; + protected AvaloniaX11Platform Platform; + protected IntPtr Handle => Window._handle; + protected IntPtr RenderHandle => Window._renderHandle; + public virtual bool BlockInput => false; + + public void Init(X11Window window) + { + Platform = window._platform; + Display = window._platform.Display; + X11 = window._platform.Info; + Window = window; + } + + public virtual bool OnEvent(ref XEvent ev) + { + return false; + } + + public virtual void Activate() + { + + } + + public virtual void OnHandleCreated(IntPtr handle) + { + } + + public virtual void OnDestroyNotify() + { + } + + public virtual void AppendWmProtocols(List data) + { + } + + public virtual void Show(bool activate, bool isDialog) + { + + } + + public abstract PixelPoint PointToScreen(Point pt); + public abstract Point PointToClient(PixelPoint pt); + + public virtual void Hide() + { + } + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/X11WindowModes/XEmbedClientWindowMode.cs b/src/Avalonia.X11/X11WindowModes/XEmbedClientWindowMode.cs new file mode 100644 index 00000000000..1c805fd8438 --- /dev/null +++ b/src/Avalonia.X11/X11WindowModes/XEmbedClientWindowMode.cs @@ -0,0 +1,207 @@ +#nullable enable +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Controls.Embedding; +using Avalonia.Input; + +namespace Avalonia.X11; +using static XLib; +partial class X11Window +{ + public class XEmbedClientWindowMode : X11WindowMode + { + EmbeddableControlRoot? Root => Window._inputRoot as EmbeddableControlRoot; + private bool _focusedInEmbedder; + private bool _embedderActivated; + private bool _disabled; + private IntPtr _currentEmbedder; + private bool _suppressConfigureEvents; + + public override bool BlockInput => _disabled; + public double Scaling + { + get => Window._scalingOverride ?? 1; + set => Window._scalingOverride = value; + } + + private WeakReference? _savedFocus; + + private IInputElement? SavedFocus + { + get => _savedFocus?.TryGetTarget(out var target) == true ? target : null; + set => _savedFocus = value == null ? null : new WeakReference(value); + } + + public override void OnHandleCreated(IntPtr handle) + { + var data = new[] + { + IntPtr.Zero, new(1) /* XEMBED_MAPPED */ + }; + + XChangeProperty(Display, handle, X11.Atoms._XEMBED_INFO, X11.Atoms._XEMBED_INFO, 32, PropertyMode.Replace, + data, data.Length); + Scaling = 1; + + base.OnHandleCreated(handle); + } + + void SendXEmbedMessage(XEmbedMessage message, IntPtr detail = default, IntPtr data1 = default, IntPtr data2 = default) + { + if (_currentEmbedder == IntPtr.Zero) + return; + var xev = new XEvent + { + ClientMessageEvent = + { + type = XEventName.ClientMessage, + send_event = 1, + window = _currentEmbedder, + message_type = X11.Atoms._XEMBED, + format = 32, + ptr1 = default, + ptr2 = new ((int)message), + ptr3 = detail, + ptr4 = data1, + ptr5 = data2 + } + }; + XSendEvent(X11.Display, _currentEmbedder, false, + new IntPtr((int)(EventMask.NoEventMask)), ref xev); + } + + static XEmbedClientWindowMode() + { + KeyboardDevice.Instance.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement)) + { + if (KeyboardDevice.Instance.FocusedElement is Visual visual + && visual.VisualRoot is EmbeddableControlRoot root + && root.PlatformImpl is X11Window window + && window._mode is XEmbedClientWindowMode xembedMode + && xembedMode._currentEmbedder != IntPtr.Zero) + { + xembedMode.SavedFocus = KeyboardDevice.Instance.FocusedElement; + xembedMode.SendXEmbedMessage(XEmbedMessage.RequestFocus); + } + } + }; + } + + void Reset() + { + _embedderActivated = false; + _focusedInEmbedder = false; + _disabled = false; + UpdateActivation(); + } + + void OnXEmbedMessage(IntPtr time, XEmbedMessage message, IntPtr detail, IntPtr data1, IntPtr data2) + { + if (message == XEmbedMessage.EmbeddedNotify) + { + Reset(); + _currentEmbedder = data1; + } + else if (message == XEmbedMessage.FocusIn) + { + _focusedInEmbedder = true; + UpdateActivation(); + } + else if (message == XEmbedMessage.FocusOut) + { + _focusedInEmbedder = false; + UpdateActivation(); + } + else if (message == XEmbedMessage.WindowActivate) + { + _embedderActivated = true; + UpdateActivation(); + } + else if (message == XEmbedMessage.WindowDeactivate) + { + _embedderActivated = false; + UpdateActivation(); + } + else if (message == XEmbedMessage.ModalityOn) + _disabled = true; + else if (message == XEmbedMessage.ModalityOff) + _disabled = false; + } + + private void UpdateActivation() + { + var active = _focusedInEmbedder && _embedderActivated; + + if (active) + { + ((FocusManager?)Root?.FocusManager)?.SetFocusScope(Root); + SavedFocus?.Focus(); + SavedFocus = null; + } + else + { + SavedFocus = Root?.IsKeyboardFocusWithin == true ? Root.FocusManager?.GetFocusedElement() : null; + Window.LostFocus?.Invoke(); + } + } + + public override bool OnEvent(ref XEvent ev) + { + // In this mode we are getting the expected size directly from the embedder + if (_suppressConfigureEvents && ev.type == XEventName.ConfigureNotify) + return true; + if(ev.type == XEventName.MapNotify) + Root?.StartRendering(); + else if (ev.type == XEventName.UnmapNotify) + Root?.StopRendering(); + else if (ev.type == XEventName.ReparentNotify) + { + Root?.StopRendering(); + _currentEmbedder = IntPtr.Zero; + Reset(); + } + else if (ev.type == XEventName.ClientMessage && ev.ClientMessageEvent.message_type == X11.Atoms._XEMBED) + { + OnXEmbedMessage(ev.ClientMessageEvent.ptr1, + (XEmbedMessage)ev.ClientMessageEvent.ptr2.ToInt32(), + ev.ClientMessageEvent.ptr3, + ev.ClientMessageEvent.ptr4, ev.ClientMessageEvent.ptr5); + return true; + } + + return base.OnEvent(ref ev); + } + + public void ProcessInteractiveResize(PixelSize size) + { + _suppressConfigureEvents = true; + Window._realSize = size; + Window.Resized?.Invoke(Window.ClientSize, WindowResizeReason.User); + Window.Paint?.Invoke(new(Window.ClientSize)); + } + + PixelVector GetWindowOffset() + { + XTranslateCoordinates(Display, Handle, X11.DefaultRootWindow, + 0, 0, out var offsetX, out var offsetY, out _); + return new PixelVector(offsetX, offsetY); + } + + public override Point PointToClient(PixelPoint point) + { + var pos = GetWindowOffset(); + return new Point( + (point.X - pos.X) / Window.RenderScaling, + (point.Y - pos.Y) / Window.RenderScaling); + } + + public override PixelPoint PointToScreen(Point point) => + new PixelPoint( + (int)(point.X * Window.RenderScaling), + (int)(point.Y * Window.RenderScaling)) + + GetWindowOffset(); + } +} \ No newline at end of file diff --git a/src/Avalonia.X11/XEmbedPlug.cs b/src/Avalonia.X11/XEmbedPlug.cs new file mode 100644 index 00000000000..b42daed34b7 --- /dev/null +++ b/src/Avalonia.X11/XEmbedPlug.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading; +using Avalonia.Controls.Embedding; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.X11.Dispatching; + +namespace Avalonia.X11; + +public class XEmbedPlug : IDisposable +{ + private EmbeddableControlRoot _root; + private Color _backgroundColor; + private readonly X11Info _x11; + private readonly X11Window.XEmbedClientWindowMode _mode; + + private XEmbedPlug(IntPtr? parentXid) + { + var platform = AvaloniaLocator.Current.GetService(); + _mode = new X11Window.XEmbedClientWindowMode(); + _root = new EmbeddableControlRoot(new X11Window(platform, null, _mode)); + _root.Prepare(); + _x11 = platform.Info; + if (parentXid.HasValue) + XLib.XReparentWindow(platform.Display, Handle, parentXid.Value, 0, 0); + + // Make sure that the newly created XID is visible for other clients + XLib.XSync(platform.Display, false); + } + + public IntPtr Handle => + _root?.PlatformImpl!.Handle!.Handle ?? throw new ObjectDisposedException(nameof(XEmbedPlug)); + + public object Content + { + get => _root.Content; + set => _root.Content = value; + } + + public Color BackgroundColor + { + get => _backgroundColor; + set + { + _backgroundColor = value; + XLib.XSetWindowBackground(_x11.Display, Handle, new IntPtr( + (int)(value.ToUInt32() | 0xff000000))); + XLib.XFlush(_x11.Display); + } + } + + public double ScaleFactor + { + get => _mode.Scaling; + set => _mode.Scaling = value; + } + + public void ProcessInteractiveResize(PixelSize size) + { + + var events = (IX11PlatformDispatcher)AvaloniaLocator.Current.GetService(); + events.EventDispatcher.DispatchX11Events(CancellationToken.None); + _mode.ProcessInteractiveResize(size); + Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender); + } + + public void Dispose() + { + if (_root != null) + { + _root.StopRendering(); + _root.Dispose(); + _root = null; + } + } + + public static XEmbedPlug Create() => new(null); + + public static XEmbedPlug Create(IntPtr embedderXid) => + embedderXid == IntPtr.Zero ? throw new ArgumentException() : new XEmbedPlug(embedderXid); +} \ No newline at end of file