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 draft #17446

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions samples/ControlCatalog.NetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public static AppBuilder BuildAvaloniaApp()
EnableMultiTouch = true,
UseDBusMenu = true,
EnableIme = true,
UseGLibMainLoop = true,
UseDBusFilePicker = false
})

.With(new VulkanOptions
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>
22 changes: 15 additions & 7 deletions src/Avalonia.Base/Platform/IRenderTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,38 @@ public interface IRenderTarget : IDisposable
public bool IsCorrupted { get; }
}

[PrivateApi]
[PrivateApi, Obsolete("Use IRenderTarget2", true)]
// TODO12: Remove
public interface IRenderTargetWithProperties : IRenderTarget
{
}

[PrivateApi]
// TODO12: Merge into IRenderTarget
public interface IRenderTarget2 : IRenderTarget
{
RenderTargetProperties Properties { get; }

/// <summary>
/// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
/// </summary>
/// <param name="useScaledDrawing">Apply DPI reported by the render target as a hidden transform matrix</param>
/// <param name="expectedPixelSize">The pixel size of the surface</param>
/// <param name="properties">Returns various properties about the returned drawing context</param>
IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, out RenderTargetDrawingContextProperties properties);
IDrawingContextImpl CreateDrawingContext(PixelSize expectedPixelSize,
out RenderTargetDrawingContextProperties properties);
}

internal static class RenderTargetExtensions
{
public static IDrawingContextImpl CreateDrawingContextWithProperties(
this IRenderTarget renderTarget,
bool useScaledDrawing,
PixelSize expectedPixelSize,
out RenderTargetDrawingContextProperties properties)
{
if (renderTarget is IRenderTargetWithProperties target)
return target.CreateDrawingContext(useScaledDrawing, out properties);
if (renderTarget is IRenderTarget2 target)
return target.CreateDrawingContext(expectedPixelSize, out properties);
properties = default;
return renderTarget.CreateDrawingContext(useScaledDrawing);
return renderTarget.CreateDrawingContext(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,16 @@ public void Render()
if (!_redrawRequested)
return;

var renderTargetWithProperties = _renderTarget as IRenderTargetWithProperties;
var renderTargetWithProperties = _renderTarget as IRenderTarget2;


var needLayer = _overlays.RequireLayer // Check if we don't need overlays
// Check if render target can be rendered to directly and preserves the previous frame
|| !(renderTargetWithProperties?.Properties.RetainsPreviousFrameContents == true
&& renderTargetWithProperties?.Properties.IsSuitableForDirectRendering == true);

using (var renderTargetContext = _renderTarget.CreateDrawingContextWithProperties(false, out var properties))
using (var renderTargetContext = _renderTarget.CreateDrawingContextWithProperties(
this.PixelSize, out var properties))
{
if(needLayer && (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using Avalonia.Metadata;

namespace Avalonia.OpenGL.Surfaces
{
Expand All @@ -11,4 +12,11 @@ public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformS
{
bool IsCorrupted { get; }
}

[PrivateApi]
public interface IGlPlatformSurfaceRenderTarget2 : IGlPlatformSurfaceRenderTargetWithCorruptionInfo
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
{
IGlPlatformSurfaceRenderingSession BeginDraw(PixelSize expectedPixelSize);
}

}
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