From 6a61949bd7f660f36de1d3fdda3fc8ce2cc9fbd7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 7 Nov 2024 16:59:06 +0500 Subject: [PATCH 1/2] XEmbed client support draft --- Avalonia.Desktop.slnf | 1 + Avalonia.sln | 6 + samples/ControlCatalog.NetCore/Program.cs | 2 + samples/XEmbedSample/HarfbuzzWorkaround.cs | 66 +++++++ samples/XEmbedSample/Program.cs | 63 ++++++ samples/XEmbedSample/SocketEx.cs | 64 +++++++ samples/XEmbedSample/XEmbedSample.csproj | 20 ++ src/Avalonia.Base/Platform/IRenderTarget.cs | 22 ++- .../Server/ServerCompositionTarget.cs | 7 +- .../IGlPlatformSurfaceRenderTarget.cs | 8 + .../Dispatching/GLibDispatcherImpl.cs | 6 +- .../Dispatching/IX11PlatformDispatcher.cs | 8 + .../Dispatching/X11PlatformThreading.cs | 4 +- src/Avalonia.X11/Glx/Glx.cs | 7 +- src/Avalonia.X11/Glx/GlxDisplay.cs | 2 + src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs | 33 +++- src/Avalonia.X11/X11Platform.cs | 5 +- src/Avalonia.X11/X11Window.cs | 76 ++++---- .../X11WindowModes/DefaultWindowMode.cs | 45 +++++ .../X11WindowModes/InputProxyWindowMode.cs | 55 ++++++ src/Avalonia.X11/X11WindowModes/WindowMode.cs | 57 ++++++ .../X11WindowModes/XEmbedClientWindowMode.cs | 179 ++++++++++++++++++ src/Avalonia.X11/XEmbedPlug.cs | 81 ++++++++ .../Avalonia.Skia/FramebufferRenderTarget.cs | 13 +- .../Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs | 8 + .../Gpu/OpenGl/GlRenderTarget.cs | 13 +- .../Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs | 21 +- 27 files changed, 800 insertions(+), 72 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/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 036dd13f7a2..6e1046fb7ca 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -134,6 +134,8 @@ public static AppBuilder BuildAvaloniaApp() EnableMultiTouch = true, UseDBusMenu = true, EnableIme = true, + UseGLibMainLoop = true, + UseDBusFilePicker = false }) .With(new VulkanOptions 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.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 39504ac9fbe..73362fe205b 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -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; } /// /// Creates an for a rendering session. /// - /// Apply DPI reported by the render target as a hidden transform matrix + /// The pixel size of the surface /// Returns various properties about the returned drawing context - 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); } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 8afdb6a2cc1..fb1bda6ff0a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -141,7 +141,7 @@ 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 @@ -149,7 +149,8 @@ public void Render() || !(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)) { @@ -178,7 +179,7 @@ public void Render() using (var context = _layer.CreateDrawingContext(false)) RenderRootToContextWithClip(context, Root); - renderTargetContext.Clear(Colors.Transparent); + renderTargetContext.Clear(Colors.Red); renderTargetContext.Transform = Matrix.Identity; if (_layer.CanBlit) _layer.Blit(renderTargetContext); diff --git a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs index f89b6f04f5c..13e89e95508 100644 --- a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurfaceRenderTarget.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Metadata; namespace Avalonia.OpenGL.Surfaces { @@ -11,4 +12,11 @@ public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformS { bool IsCorrupted { get; } } + + [PrivateApi] + public interface IGlPlatformSurfaceRenderTarget2 : IGlPlatformSurfaceRenderTargetWithCorruptionInfo + { + IGlPlatformSurfaceRenderingSession BeginDraw(PixelSize expectedPixelSize); + } + } 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/Glx/Glx.cs b/src/Avalonia.X11/Glx/Glx.cs index fceea3718c0..ed65f943877 100644 --- a/src/Avalonia.X11/Glx/Glx.cs +++ b/src/Avalonia.X11/Glx/Glx.cs @@ -40,8 +40,13 @@ internal unsafe partial class GlxInterface [GetProcAddress("glXCreateContext")] public partial IntPtr CreateContext(IntPtr dpy, XVisualInfo* vis, IntPtr shareList, bool direct); - + [GetProcAddress("glXCreateWindow")] + public partial IntPtr CreateWindow(IntPtr dpy, IntPtr fbConfig, IntPtr window, int[] attrib_list); + + [GetProcAddress("glXDestroyWindow")] + public partial IntPtr DestroyWindow(IntPtr dpy, IntPtr fb); + [GetProcAddress("glXCreateContextAttribsARB")] public partial IntPtr CreateContextAttribsARB(IntPtr dpy, IntPtr fbconfig, IntPtr shareList, bool direct, int[] attribs); diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 190677074a8..9ce34ec7c41 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -18,6 +18,8 @@ internal unsafe class GlxDisplay public XVisualInfo* VisualInfo => _visual; public GlxContext DeferredContext { get; } public GlxInterface Glx { get; } = new GlxInterface(); + public X11Info X11Info => _x11; + public IntPtr FbConfig => _fbconfig; public GlxDisplay(X11Info x11, IList probeProfiles) { _x11 = x11; diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index 17c5909a39a..0c7c7d21659 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -21,10 +21,11 @@ public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) return new RenderTarget((GlxContext)context, _info); } - private class RenderTarget : IGlPlatformSurfaceRenderTarget + private class RenderTarget : IGlPlatformSurfaceRenderTarget2 { private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + private PixelSize? _lastSize; public RenderTarget(GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { @@ -36,29 +37,51 @@ public void Dispose() { // No-op } - - public IGlPlatformSurfaceRenderingSession BeginDraw() + + public bool IsCorrupted => false; + public IGlPlatformSurfaceRenderingSession BeginDraw(PixelSize size) => BeginDrawCore(size); + public IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDrawCore(null); + public IGlPlatformSurfaceRenderingSession BeginDrawCore(PixelSize? expectedSize) { + var size = expectedSize ?? _info.Size; + if (expectedSize.HasValue) + { + XLib.XConfigureResizeWindow(_context.Display.X11Info.DeferredDisplay, + _info.Handle, size.Width, size.Height); + XLib.XFlush(_context.Display.X11Info.DeferredDisplay); + + if (_lastSize != size) + { + XLib.XSync(_context.Display.X11Info.DeferredDisplay, true); + _lastSize = size; + } + _context.Glx.WaitX(); + } + + var oldContext = _context.MakeCurrent(_info.Handle); // Reset to default FBO first _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, 0); - return new Session(_context, _info, oldContext); + return new Session(_context, _info, size, oldContext); } private class Session : IGlPlatformSurfaceRenderingSession { private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + private readonly PixelSize? _size; private readonly IDisposable _clearContext; public IGlContext Context => _context; public Session(GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, + PixelSize? size, IDisposable clearContext) { _context = context; _info = info; + _size = size; _clearContext = clearContext; } @@ -71,7 +94,7 @@ public void Dispose() _clearContext.Dispose(); } - public PixelSize Size => _info.Size; + public PixelSize Size => _size ?? _info.Size; public double Scaling => _info.Scaling; public bool IsYFlipped { get; } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 4b1b3731b79..af59995adb8 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -129,14 +129,15 @@ public ITrayIconImpl CreateTrayIcon() public IWindowImpl CreateWindow() { - return new X11Window(this, null); + return new X11Window(this, null, + Options.EnableInputFocusProxy ? new X11Window.InputProxyWindowMode() : new X11Window.DefaultWindowMode()); } public ITopLevelImpl CreateEmbeddableTopLevel() => CreateEmbeddableWindow(); public IWindowImpl CreateEmbeddableWindow() { - throw new NotSupportedException(); + throw new PlatformNotSupportedException(); } private static bool EnableIme(X11PlatformOptions options) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 0a254b6b2b3..9b4889b6747 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -67,8 +67,9 @@ internal unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client, private TransparencyHelper? _transparencyHelper; private RawEventGrouper? _rawEventGrouper; private bool _useRenderWindow = false; + private bool _suppressRenderWindowResize = false; private bool _usePositioningFlags = false; - private X11FocusProxy? _focusProxy; + private X11WindowMode _mode; private enum XSyncState { @@ -76,10 +77,20 @@ private enum XSyncState WaitConfigure, WaitPaint } - + + // Do _not_ remove public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool overrideRedirect = false) + : this(platform, popupParent, new DefaultWindowMode(), 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; @@ -111,7 +122,10 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool ov var glx = glfeature as GlxPlatformGraphics; if (glx != null) + { visualInfo = *glx.Display.VisualInfo; + _suppressRenderWindowResize = true; + } else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; @@ -162,12 +176,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; @@ -224,9 +234,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); @@ -440,6 +449,9 @@ private void OnEvent(ref XEvent ev) { if (_inputRoot is null) return; + + if(_mode.OnEvent(ref ev)) + return; if (ev.type == XEventName.MapNotify) { @@ -569,7 +581,8 @@ private void OnEvent(ref XEvent ev) Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); }, DispatcherPriority.AsyncRenderTargetResize); - if (_useRenderWindow) + + if (_useRenderWindow && !_suppressRenderWindowResize) XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width, ev.ConfigureEvent.height); if (_xSyncState == XSyncState.WaitConfigure) @@ -581,6 +594,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) @@ -598,11 +612,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) @@ -989,6 +998,7 @@ private void Cleanup(bool fromDestroyNotification) _xSyncCounter = IntPtr.Zero; } + if (_handle != IntPtr.Zero) { _platform.Windows.Remove(_handle); @@ -997,7 +1007,7 @@ private void Cleanup(bool fromDestroyNotification) _handle = IntPtr.Zero; _mouse.Dispose(); _touch.Dispose(); - if (!fromDestroyNotification) + if (!fromDestroyNotification) XDestroyWindow(_x11.Display, handle); } @@ -1007,8 +1017,6 @@ private void Cleanup(bool fromDestroyNotification) { _renderHandle = IntPtr.Zero; } - - _focusProxy?.Cleanup(); } private bool ActivateTransientChildIfNeeded() @@ -1032,13 +1040,11 @@ 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 void Hide() => _mode.Hide(); + public Point PointToClient(PixelPoint point) => new Point((point.X - (_position ?? default).X) / RenderScaling, (point.Y - (_position ?? default).Y) / RenderScaling); public PixelPoint PointToScreen(Point point) => new PixelPoint( @@ -1081,7 +1087,7 @@ private void Resize(Size clientSize, bool force, WindowResizeReason reason) var pixelSize = ToPixelSize(clientSize); UpdateSizeHints(pixelSize); XConfigureResizeWindow(_x11.Display, _handle, pixelSize); - if (_useRenderWindow) + if (_useRenderWindow && !_suppressRenderWindowResize) XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize); XFlush(_x11.Display); @@ -1161,21 +1167,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(); @@ -1449,7 +1441,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..f02b2506149 --- /dev/null +++ b/src/Avalonia.X11/X11WindowModes/DefaultWindowMode.cs @@ -0,0 +1,45 @@ +using System; + +namespace Avalonia.X11; + +using static XLib; +partial class X11Window +{ + public class DefaultWindowMode : 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(); + } + } +} \ 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..fec18272abd --- /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 : DefaultWindowMode + { + 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..815e0d02289 --- /dev/null +++ b/src/Avalonia.X11/X11WindowModes/WindowMode.cs @@ -0,0 +1,57 @@ +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 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..8119e7677fa --- /dev/null +++ b/src/Avalonia.X11/X11WindowModes/XEmbedClientWindowMode.cs @@ -0,0 +1,179 @@ +#nullable enable +using System; +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 IInputElement? _savedFocus; + 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; + } + + 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); + if (_savedFocus != null) + KeyboardDevice.Instance?.SetFocusedElement(_savedFocus, NavigationMethod.Unspecified, KeyModifiers.None); + _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)); + } + } +} \ 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 diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 4b8f1791842..318cdac22c0 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -10,7 +10,7 @@ namespace Avalonia.Skia /// /// Skia render target that renders to a framebuffer surface. No gpu acceleration available. /// - internal class FramebufferRenderTarget : IRenderTargetWithProperties + internal class FramebufferRenderTarget : IRenderTarget2 { private SKImageInfo _currentImageInfo; private IntPtr _currentFramebufferAddress; @@ -50,10 +50,15 @@ public void Dispose() /// public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi) => - CreateDrawingContext(scaleDrawingToDpi, out _); + CreateDrawingContextCore(scaleDrawingToDpi, out _); /// - public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, out RenderTargetDrawingContextProperties properties) + public IDrawingContextImpl CreateDrawingContext(PixelSize expectedPixelSize, + out RenderTargetDrawingContextProperties properties) + => CreateDrawingContextCore(false, out properties); + + IDrawingContextImpl CreateDrawingContextCore(bool scaleDrawingToDpi, + out RenderTargetDrawingContextProperties properties) { if (_renderTarget == null) throw new ObjectDisposedException(nameof(FramebufferRenderTarget)); @@ -77,7 +82,7 @@ public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, out Rende { Surface = _framebufferSurface, Dpi = framebuffer.Dpi, - ScaleDrawingToDpi = useScaledDrawing + ScaleDrawingToDpi = scaleDrawingToDpi }; properties = new() diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs index bd5b170a688..f8be7dc66f0 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Metadata; using SkiaSharp; namespace Avalonia.Skia @@ -16,4 +17,11 @@ public interface ISkiaGpuRenderTarget : IDisposable bool IsCorrupted { get; } } + + [PrivateApi] + //TODO12: Merge with ISkiaGpuRenderTarget + public interface ISkiaGpuRenderTarget2 : ISkiaGpuRenderTarget + { + ISkiaGpuRenderSession BeginRenderingSession(PixelSize pixelSize); + } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 25e004f4efe..56ce4a71945 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -9,7 +9,7 @@ namespace Avalonia.Skia { - internal class GlRenderTarget : ISkiaGpuRenderTarget + internal class GlRenderTarget : ISkiaGpuRenderTarget2 { private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; @@ -59,9 +59,16 @@ public void Dispose() public double ScaleFactor => _glSession.Scaling; } - public ISkiaGpuRenderSession BeginRenderingSession() + public ISkiaGpuRenderSession BeginRenderingSession(PixelSize size) => BeginRenderingSessionCore(size); + public ISkiaGpuRenderSession BeginRenderingSession() => BeginRenderingSessionCore(null); + + ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) { - var glSession = _surface.BeginDraw(); + var glSession = + expectedSize != null && _surface is IGlPlatformSurfaceRenderTarget2 surface2 + ? surface2.BeginDraw(expectedSize.Value) + : _surface.BeginDraw(); + bool success = false; try { diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 8cc7ed68688..8d231a57849 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -5,7 +5,7 @@ namespace Avalonia.Skia /// /// Adapts to be used within our rendering pipeline. /// - internal class SkiaGpuRenderTarget : IRenderTarget + internal class SkiaGpuRenderTarget : IRenderTarget2 { private readonly ISkiaGpu _skiaGpu; private readonly ISkiaGpuRenderTarget _renderTarget; @@ -21,9 +21,23 @@ public void Dispose() _renderTarget.Dispose(); } + public IDrawingContextImpl CreateDrawingContext(PixelSize expectedPixelSize, + out RenderTargetDrawingContextProperties properties) => + CreateDrawingContextCore(expectedPixelSize, false, out properties); + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) + => CreateDrawingContextCore(null, useScaledDrawing, out _); + + + IDrawingContextImpl CreateDrawingContextCore(PixelSize? expectedPixelSize, + bool useScaledDrawing, + out RenderTargetDrawingContextProperties properties) { - var session = _renderTarget.BeginRenderingSession(); + properties = default; + var session = + expectedPixelSize.HasValue && _renderTarget is ISkiaGpuRenderTarget2 target2 + ? target2.BeginRenderingSession(expectedPixelSize.Value) + : _renderTarget.BeginRenderingSession(); var nfo = new DrawingContextImpl.CreateInfo { @@ -39,5 +53,8 @@ public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing) } public bool IsCorrupted => _renderTarget.IsCorrupted; + public RenderTargetProperties Properties { get; } + + } } From 51cc2524e96b88e4ee5d5ca8a50ed85b9bdff319 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 7 Nov 2024 18:11:20 +0500 Subject: [PATCH 2/2] removed test code --- .../Rendering/Composition/Server/ServerCompositionTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index fb1bda6ff0a..730f2356e4b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -179,7 +179,7 @@ public void Render() using (var context = _layer.CreateDrawingContext(false)) RenderRootToContextWithClip(context, Root); - renderTargetContext.Clear(Colors.Red); + renderTargetContext.Clear(Colors.Transparent); renderTargetContext.Transform = Matrix.Identity; if (_layer.CanBlit) _layer.Blit(renderTargetContext);