Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XEmbed client support #17446

Merged
merged 1 commit into from
Dec 16, 2024
Merged
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
1 change: 1 addition & 0 deletions Avalonia.Desktop.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions Avalonia.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
66 changes: 66 additions & 0 deletions samples/XEmbedSample/HarfbuzzWorkaround.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 63 additions & 0 deletions samples/XEmbedSample/Program.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 64 additions & 0 deletions samples/XEmbedSample/SocketEx.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions samples/XEmbedSample/XEmbedSample.csproj
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 2 deletions src/Avalonia.X11/Dispatching/GLibDispatcherImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -309,5 +310,6 @@ public void Dispose()
}
}
}


public X11EventDispatcher EventDispatcher => _x11Events;
}
8 changes: 8 additions & 0 deletions src/Avalonia.X11/Dispatching/IX11PlatformDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Avalonia.Threading;

namespace Avalonia.X11.Dispatching;

interface IX11PlatformDispatcher : IDispatcherImpl
{
X11EventDispatcher EventDispatcher { get; }
}
4 changes: 3 additions & 1 deletion src/Avalonia.X11/Dispatching/X11PlatformThreading.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Loading
Loading