Browse Source

Implemented XEmbed client support with GtkSharp usage example (#17446)

pull/17782/head
Nikita Tsukanov 1 year ago
committed by GitHub
parent
commit
0efe89e063
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      Avalonia.Desktop.slnf
  2. 6
      Avalonia.sln
  3. 66
      samples/XEmbedSample/HarfbuzzWorkaround.cs
  4. 63
      samples/XEmbedSample/Program.cs
  5. 64
      samples/XEmbedSample/SocketEx.cs
  6. 20
      samples/XEmbedSample/XEmbedSample.csproj
  7. 6
      src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
  8. 8
      src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs
  9. 4
      src/Avalonia.X11/Dispatching/X11PlatformThreading.cs
  10. 75
      src/Avalonia.X11/X11Window.cs
  11. 53
      src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs
  12. 55
      src/Avalonia.X11/X11WindowModes/InputProxyWindowMode.cs
  13. 60
      src/Avalonia.X11/X11WindowModes/WindowMode.cs
  14. 207
      src/Avalonia.X11/X11WindowModes/XEmbedClientWindowMode.cs
  15. 81
      src/Avalonia.X11/XEmbedPlug.cs

1
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",

6
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}

66
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);
});
}
}

63
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<App>()
.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();
}
}

64
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();
}
}

20
samples/XEmbedSample/XEmbedSample.csproj

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GtkSharp" Version="3.24.24.95" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>
</Project>

6
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 @@ internal class GlibDispatcherImpl :
}
}
}
public X11EventDispatcher EventDispatcher => _x11Events;
}

8
src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs

@ -0,0 +1,8 @@
using Avalonia.Threading;
namespace Avalonia.X11.Dispatching;
interface IX11PlatformDispatcher : IDispatcherImpl
{
X11EventDispatcher EventDispatcher { get; }
}

4
src/Avalonia.X11/Dispatching/X11PlatformThreading.cs

@ -5,11 +5,12 @@ using System.Runtime.InteropServices;
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 @@ namespace Avalonia.X11
public bool CanQueryPendingInput => true;
public bool HasPendingInput => _platform.EventGrouperDispatchQueue.HasJobs || _x11Events.IsPending;
public X11EventDispatcher EventDispatcher => _x11Events;
}
}

75
src/Avalonia.X11/X11Window.cs

@ -69,7 +69,7 @@ namespace Avalonia.X11
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 @@ namespace Avalonia.X11
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 @@ namespace Avalonia.X11
_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 @@ namespace Avalonia.X11
InitializeIme();
var data = new List<IntPtr> { _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 @@ namespace Avalonia.X11
{
if (_inputRoot is null)
return;
if(_mode.OnEvent(ref ev))
return;
if (ev.type == XEventName.MapNotify)
{
@ -588,6 +599,7 @@ namespace Avalonia.X11
else if (ev.type == XEventName.DestroyNotify
&& ev.DestroyWindowEvent.window == _handle)
{
_mode.OnDestroyNotify();
Cleanup(true);
}
else if (ev.type == XEventName.ClientMessage)
@ -605,11 +617,6 @@ namespace Avalonia.X11
_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 @@ namespace Avalonia.X11
_handle = IntPtr.Zero;
_mouse.Dispose();
_touch.Dispose();
if (!fromDestroyNotification)
if (!fromDestroyNotification)
XDestroyWindow(_x11.Display, handle);
}
@ -1014,8 +1021,6 @@ namespace Avalonia.X11
{
_renderHandle = IntPtr.Zero;
}
_focusProxy?.Cleanup();
}
private bool ActivateTransientChildIfNeeded()
@ -1039,18 +1044,14 @@ namespace Avalonia.X11
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 @@ namespace Avalonia.X11
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 @@ namespace Avalonia.X11
public bool NeedsManagedDecorations => false;
public bool IsEnabled => !_disabled;
public bool IsEnabled => !_disabled && !_mode.BlockInput;
public class SurfacePlatformHandle : INativePlatformHandleSurface
{

53
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));
}
}

55
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<IntPtr> data)
{
data.Add(X11.Atoms.WM_TAKE_FOCUS);
base.AppendWmProtocols(data);
}
}
}

60
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<IntPtr> 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()
{
}
}
}

207
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<IInputElement>? _savedFocus;
private IInputElement? SavedFocus
{
get => _savedFocus?.TryGetTarget(out var target) == true ? target : null;
set => _savedFocus = value == null ? null : new WeakReference<IInputElement>(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();
}
}

81
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<AvaloniaX11Platform>();
_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<IDispatcherImpl>();
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);
}
Loading…
Cancel
Save