From c5f0e18d55a3485960cbacaa2d88fdfa50e32b6a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 28 Dec 2022 14:47:29 -0800 Subject: [PATCH 001/326] Support TransformOperations animations in Brush animator --- .../Animators/GradientBrushAnimator.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs index 068c190fa1..7da1b68051 100644 --- a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Avalonia.Data; using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.Media.Transformation; #nullable enable @@ -30,7 +31,7 @@ namespace Avalonia.Animation.Animators return new ImmutableRadialGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), - oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, + InterpolateTransform(progress, oldValue.Transform, newValue.Transform), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), @@ -41,7 +42,7 @@ namespace Avalonia.Animation.Animators return new ImmutableConicGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), - oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, + InterpolateTransform(progress, oldValue.Transform, newValue.Transform), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), @@ -51,7 +52,7 @@ namespace Avalonia.Animation.Animators return new ImmutableLinearGradientBrush( InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), - oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, + InterpolateTransform(progress, oldValue.Transform, newValue.Transform), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), oldValue.SpreadMethod, s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), @@ -72,6 +73,25 @@ namespace Avalonia.Animation.Animators return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation); } + private static ImmutableTransform? InterpolateTransform(double progress, + ITransform? oldTransform, ITransform? newTransform) + { + if (oldTransform is TransformOperations oldTransformOperations + && newTransform is TransformOperations newTransformOperations) + { + + return new ImmutableTransform(TransformOperations + .Interpolate(oldTransformOperations, newTransformOperations, progress).Value); + } + + if (oldTransform is { }) + { + return new ImmutableTransform(oldTransform.Value); + } + + return null; + } + private static IReadOnlyList InterpolateStops(double progress, IReadOnlyList oldValue, IReadOnlyList newValue) { var resultCount = Math.Max(oldValue.Count, newValue.Count); From a4d94f62f4c61f674e65f9be4613596f630d4c4b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 28 Dec 2022 18:30:21 -0800 Subject: [PATCH 002/326] Update src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs --- src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs index 7da1b68051..801f247754 100644 --- a/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs @@ -84,7 +84,7 @@ namespace Avalonia.Animation.Animators .Interpolate(oldTransformOperations, newTransformOperations, progress).Value); } - if (oldTransform is { }) + if (oldTransform is not null) { return new ImmutableTransform(oldTransform.Value); } From d7904b34cd7995935b4dce91bc057c5480973031 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Thu, 29 Dec 2022 20:55:14 +0100 Subject: [PATCH 003/326] Use Tmds.DBus.Protocol --- .gitmodules | 3 + Avalonia.Desktop.slnf | 7 +- Avalonia.sln | 7 + NuGet.Config | 1 + .../AppMenuRegistrar.DBus.cs | 176 ++++ .../Avalonia.FreeDesktop.csproj | 3 +- src/Avalonia.FreeDesktop/DBus.DBus.cs | 643 ++++++++++++ src/Avalonia.FreeDesktop/DBusFileChooser.cs | 32 - src/Avalonia.FreeDesktop/DBusHelper.cs | 54 +- .../DBusIme/DBusTextInputMethodBase.cs | 58 +- .../DBusIme/Fcitx/Fcitx.DBus.cs | 627 ++++++++++++ .../DBusIme/Fcitx/FcitxDBus.cs | 69 -- .../DBusIme/Fcitx/FcitxICWrapper.cs | 32 +- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 47 +- .../DBusIme/IBus/IBus.DBus.cs | 513 ++++++++++ .../DBusIme/IBus/IBusDBus.cs | 52 - .../DBusIme/IBus/IBusEnums.cs | 2 +- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 44 +- .../DBusIme/X11DBusImeHelper.cs | 25 +- src/Avalonia.FreeDesktop/DBusMenu.DBus.cs | 463 +++++++++ src/Avalonia.FreeDesktop/DBusMenu.cs | 56 -- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 432 ++++---- src/Avalonia.FreeDesktop/DBusRequest.cs | 16 - src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 107 +- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 415 ++++---- .../FreeDesktopPortalDesktop.DBus.cs | 937 ++++++++++++++++++ .../StatusNotifierWatcher.DBus.cs | 351 +++++++ src/Avalonia.X11/X11Window.cs | 6 +- src/Linux/Tmds.DBus | 1 + 29 files changed, 4272 insertions(+), 907 deletions(-) create mode 100644 src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBus.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusFileChooser.cs create mode 100644 src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBusMenu.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusMenu.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusRequest.cs create mode 100644 src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs create mode 100644 src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs create mode 160000 src/Linux/Tmds.DBus diff --git a/.gitmodules b/.gitmodules index 032bc879cc..16bc977251 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "nukebuild/il-repack"] path = nukebuild/il-repack url = https://github.com/Gillibald/il-repack +[submodule "Tmds.DBus"] + path = src/Linux/Tmds.DBus + url = https://github.com/affederaffe/Tmds.DBus diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3fa8e969c8..72eb13d0a9 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -7,9 +7,9 @@ "samples\\ControlCatalog\\ControlCatalog.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", + "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "samples\\SampleControls\\ControlSamples.csproj", "samples\\Sandbox\\Sandbox.csproj", - "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", @@ -31,15 +31,16 @@ "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", + "src\\Linux\\Tmds.DBus\\src\\Tmds.DBus.Protocol\\Tmds.DBus.Protocol.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj", + "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", + "src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", - "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", - "src\\tools\\DevGenerators\\DevGenerators.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index fc42a5d63b..e399483896 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,6 +231,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.Protocol", "src\Linux\Tmds.DBus\src\Tmds.DBus.Protocol\Tmds.DBus.Protocol.csproj", "{29E25263-3CC3-4D55-A042-00BA136867D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -542,6 +544,10 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -606,6 +612,7 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {29E25263-3CC3-4D55-A042-00BA136867D4} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/NuGet.Config b/NuGet.Config index 7d2bd8abd2..7a9dd7993c 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,5 +6,6 @@ + diff --git a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs new file mode 100644 index 0000000000..a45d1642fc --- /dev/null +++ b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs @@ -0,0 +1,176 @@ +using System; +using System.Threading.Tasks; + +using Tmds.DBus.Protocol; + + +namespace Avalonia.FreeDesktop +{ + internal class Registrar : AppMenuRegistrarObject + { + private const string Interface = "com.canonical.AppMenu.Registrar"; + + public Registrar(RegistrarService service, ObjectPath path) : base(service, path) { } + + public Task RegisterWindowAsync(uint windowId, ObjectPath menuObjectPath) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uo", + member: "RegisterWindow", + flags: MessageFlags.NoReplyExpected); + writer.WriteUInt32(windowId); + writer.WriteObjectPath(menuObjectPath); + return writer.CreateMessage(); + } + } + + public Task UnregisterWindowAsync(uint windowId) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "UnregisterWindow"); + writer.WriteUInt32(windowId); + return writer.CreateMessage(); + } + } + + public Task<(string Service, ObjectPath MenuObjectPath)> GetMenuForWindowAsync(uint windowId) + { + return Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "GetMenuForWindow"); + writer.WriteUInt32(windowId); + return writer.CreateMessage(); + } + } + } + + internal class RegistrarService + { + public RegistrarService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public Registrar CreateRegistrar(string path) => new Registrar(this, path); + } + + internal class AppMenuRegistrarObject + { + protected AppMenuRegistrarObject(RegistrarService service, ObjectPath path) + => (Service, Path) = (service, path); + + public RegistrarService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (Exception? ex, PropertyChanges changes, object? rs, object? hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (Exception? ex, TArg arg, object? rs, object? hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, (Message message, object? state) => null!, + (Exception? ex, object v, object? rs, object? hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static (string, ObjectPath) ReadMessage_so(Message message, AppMenuRegistrarObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadObjectPath(); + return (arg0, arg1); + } + } +} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 3b1c6cc7b1..a33fa7b32d 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -12,7 +12,8 @@ - + + diff --git a/src/Avalonia.FreeDesktop/DBus.DBus.cs b/src/Avalonia.FreeDesktop/DBus.DBus.cs new file mode 100644 index 0000000000..4cf2b76b45 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBus.DBus.cs @@ -0,0 +1,643 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Tmds.DBus.Protocol; + + +namespace Avalonia.FreeDesktop +{ + internal record DBusProperties + { + public string[] Features { get; set; } = default!; + public string[] Interfaces { get; set; } = default!; + } + + internal class DBus : DBusObject + { + private const string Interface = "org.freedesktop.DBus"; + + public DBus(DBusService service, ObjectPath path) : base(service, path) { } + + public Task HelloAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Hello"); + return writer.CreateMessage(); + } + } + + public Task RequestNameAsync(string a0, uint a1) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "su", + member: "RequestName"); + writer.WriteString(a0); + writer.WriteUInt32(a1); + return writer.CreateMessage(); + } + } + + public Task ReleaseNameAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "ReleaseName"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task StartServiceByNameAsync(string a0, uint a1) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "su", + member: "StartServiceByName"); + writer.WriteString(a0); + writer.WriteUInt32(a1); + return writer.CreateMessage(); + } + } + + public Task UpdateActivationEnvironmentAsync(Dictionary a0) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "a{ss}", + member: "UpdateActivationEnvironment"); + writer.WriteDictionary(a0); + return writer.CreateMessage(); + } + } + + public Task NameHasOwnerAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "NameHasOwner"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task ListNamesAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "ListNames"); + return writer.CreateMessage(); + } + } + + public Task ListActivatableNamesAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "ListActivatableNames"); + return writer.CreateMessage(); + } + } + + public Task AddMatchAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "AddMatch"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task RemoveMatchAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RemoveMatch"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetNameOwnerAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetNameOwner"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task ListQueuedOwnersAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "ListQueuedOwners"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetConnectionUnixUserAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionUnixUser"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetConnectionUnixProcessIDAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionUnixProcessID"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetAdtAuditSessionDataAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetAdtAuditSessionData"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task GetConnectionSELinuxSecurityContextAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionSELinuxSecurityContext"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public Task ReloadConfigAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "ReloadConfig"); + return writer.CreateMessage(); + } + } + + public Task GetIdAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "GetId"); + return writer.CreateMessage(); + } + } + + public Task> GetConnectionCredentialsAsync(string a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_aesv(m, (DBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetConnectionCredentials"); + writer.WriteString(a0); + return writer.CreateMessage(); + } + } + + public ValueTask WatchNameOwnerChangedAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "NameOwnerChanged", static (m, s) => + ReadMessage_sss(m, (DBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchNameLostAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "NameLost", static (m, s) => + ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchNameAcquiredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "NameAcquired", static (m, s) => + ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); + + public Task SetFeaturesAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Features"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + + public Task SetInterfacesAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Interfaces"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + + public Task GetFeaturesAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Features"), static (m, s) => + ReadMessage_v_as(m, (DBusObject)s!), this); + + public Task GetInterfacesAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Interfaces"), static (m, s) => + ReadMessage_v_as(m, (DBusObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) => + ReadMessage(m, (DBusObject)s!), this); + + static DBusProperties ReadMessage(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, static (m, s) => + ReadMessage(m, (DBusObject)s!), handler, emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + var headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new List(); + var property = reader.ReadString(); + switch (property) + { + case "Features": + invalidated.Add("Features"); + break; + case "Interfaces": + invalidated.Add("Interfaces"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static DBusProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new DBusProperties(); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Features": + reader.ReadSignature("as"); + props.Features = reader.ReadArray(); + changedList?.Add("Features"); + break; + case "Interfaces": + reader.ReadSignature("as"); + props.Interfaces = reader.ReadArray(); + changedList?.Add("Interfaces"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal class DBusService + { + public DBusService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public DBus CreateDBus(string path) => new(this, path); + } + + internal class DBusObject + { + protected DBusObject(DBusService service, ObjectPath path) + { + Service = service; + Path = path; + } + + public DBusService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => + ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static string ReadMessage_s(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static uint ReadMessage_u(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadUInt32(); + } + + protected static bool ReadMessage_b(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + + protected static string[] ReadMessage_as(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray(); + } + + protected static byte[] ReadMessage_ay(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray(); + } + + protected static Dictionary ReadMessage_aesv(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadDictionary(); + } + + protected static (string, string, string) ReadMessage_sss(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadString(); + return (arg0, arg1, arg2); + } + + protected static string[] ReadMessage_v_as(Message message, DBusObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("as"); + return reader.ReadArray(); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusFileChooser.cs b/src/Avalonia.FreeDesktop/DBusFileChooser.cs deleted file mode 100644 index 24db614a02..0000000000 --- a/src/Avalonia.FreeDesktop/DBusFileChooser.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.FileChooser")] - internal interface IFileChooser : IDBusObject - { - Task OpenFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFileAsync(string ParentWindow, string Title, IDictionary Options); - Task SaveFilesAsync(string ParentWindow, string Title, IDictionary Options); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [Dictionary] - internal class FileChooserProperties - { - public uint Version { get; set; } - } - - internal static class FileChooserExtensions - { - public static Task GetVersionAsync(this IFileChooser o) => o.GetAsync("version"); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index ef99838208..e0445cc95c 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -1,51 +1,12 @@ using System; using System.Threading; using Avalonia.Logging; -using Avalonia.Threading; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { public static class DBusHelper { - /// - /// This class uses synchronous execution at DBus connection establishment stage - /// then switches to using AvaloniaSynchronizationContext - /// - private class DBusSyncContext : SynchronizationContext - { - private readonly object _lock = new(); - private SynchronizationContext? _ctx; - - public override void Post(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Post(d, state); - else - d(state); - } - } - - public override void Send(SendOrPostCallback d, object? state) - { - lock (_lock) - { - if (_ctx is not null) - _ctx?.Send(d, state); - else - d(state); - } - } - - public void Initialized() - { - lock (_lock) - _ctx = new AvaloniaSynchronizationContext(); - } - } - public static Connection? Connection { get; private set; } public static Connection? TryInitialize(string? dbusAddress = null) @@ -56,19 +17,14 @@ namespace Avalonia.FreeDesktop var oldContext = SynchronizationContext.Current; try { - - var dbusContext = new DBusSyncContext(); - SynchronizationContext.SetSynchronizationContext(dbusContext); - var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session) + var conn = new Connection(new ClientConnectionOptions(dbusAddress ?? Address.Session!) { - AutoConnect = false, - SynchronizationContext = dbusContext + AutoConnect = false }); + // Connect synchronously - conn.ConnectAsync().Wait(); + conn.ConnectAsync().GetAwaiter().GetResult(); - // Initialize a brand new sync-context - dbusContext.Initialized(); Connection = conn; } catch (Exception e) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 899288b4a8..c7bf530cfd 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Logging; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme { @@ -46,7 +46,7 @@ namespace Avalonia.FreeDesktop.DBusIme public DBusTextInputMethodBase(Connection connection, params string[] knownNames) { - _queue = new DBusCallQueue(QueueOnError); + _queue = new DBusCallQueue(QueueOnErrorAsync); Connection = connection; _knownNames = knownNames; Watch(); @@ -54,12 +54,17 @@ namespace Avalonia.FreeDesktop.DBusIme public ITextInputMethodClient Client => _client; - public bool IsActive => _client != null; + public bool IsActive => _client is not null; - async void Watch() + private async void Watch() { foreach (var name in _knownNames) - _disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); + { + var dbus = new DBusService(Connection, name).CreateDBus("/org/freedesktop/DBus"); + _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); + var nameOwner = await dbus.GetNameOwnerAsync(name); + OnNameChange(null, (name, null, nameOwner)); + } } protected abstract Task Connect(string name); @@ -67,9 +72,9 @@ namespace Avalonia.FreeDesktop.DBusIme protected string GetAppName() => Application.Current?.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; - private async void OnNameChange(ServiceOwnerChangedEventArgs args) + private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args) { - if (args.NewOwner != null && _currentName == null) + if (args.NewOwner is not null && _currentName is null) { _onlineNamesQueue.Enqueue(args.ServiceName); if (!_connecting) @@ -89,10 +94,10 @@ namespace Avalonia.FreeDesktop.DBusIme return; } } - catch (Exception e) + catch (Exception ex) { Logger.TryGet(LogEventLevel.Error, "IME") - ?.Log(this, "Unable to create IME input context:\n" + e); + ?.Log(this, "Unable to create IME input context:\n" + ex); } } } @@ -105,7 +110,7 @@ namespace Avalonia.FreeDesktop.DBusIme } // IME has crashed - if (args.NewOwner == null && args.ServiceName == _currentName) + if (args.NewOwner is null && args.ServiceName == _currentName) { _currentName = null; foreach (var s in _disposables) @@ -120,7 +125,7 @@ namespace Avalonia.FreeDesktop.DBusIme } } - protected virtual Task Disconnect() + protected virtual Task DisconnectAsync() { return Task.CompletedTask; } @@ -136,13 +141,13 @@ namespace Avalonia.FreeDesktop.DBusIme _imeActive = null; } - async Task QueueOnError(Exception e) + private async Task QueueOnErrorAsync(Exception e) { Logger.TryGet(LogEventLevel.Error, "IME") ?.Log(this, "Error:\n" + e); try { - await Disconnect(); + await DisconnectAsync(); } catch (Exception ex) { @@ -160,20 +165,13 @@ namespace Avalonia.FreeDesktop.DBusIme if(d is { }) _disposables.Add(d); } - + public void Dispose() { foreach(var d in _disposables) d.Dispose(); _disposables.Clear(); - try - { - Disconnect().ContinueWith(_ => { }); - } - catch - { - // fire and forget - } + DisconnectAsync(); _currentName = null; } @@ -182,13 +180,13 @@ namespace Avalonia.FreeDesktop.DBusIme protected abstract Task ResetContextCore(); protected abstract Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); - void UpdateActive() + private void UpdateActive() { _queue.Enqueue(async () => { if(!IsConnected) return; - + var active = _windowActive && _controlActive; if (active != _imeActive) { @@ -204,7 +202,7 @@ namespace Avalonia.FreeDesktop.DBusIme _windowActive = active; UpdateActive(); } - + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { _client = client; @@ -227,7 +225,7 @@ namespace Avalonia.FreeDesktop.DBusIme // Error, disconnect catch (Exception e) { - await QueueOnError(e); + await QueueOnErrorAsync(e); return false; } } @@ -240,7 +238,7 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireCommit(string s) => _onCommit?.Invoke(s); - + private Action? _onForward; event Action IX11InputMethodControl.ForwardKey { @@ -249,8 +247,8 @@ namespace Avalonia.FreeDesktop.DBusIme } protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k); - - void UpdateCursorRect() + + private void UpdateCursorRect() { _queue.Enqueue(async () => { @@ -265,7 +263,7 @@ namespace Avalonia.FreeDesktop.DBusIme } }); } - + void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling) { _windowPosition = position; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs new file mode 100644 index 0000000000..f9e4497892 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs @@ -0,0 +1,627 @@ +using System; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop.DBusIme.Fcitx +{ + internal class InputContext : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputContext"; + + public InputContext(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task FocusInAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusIn"); + return writer.CreateMessage(); + } + } + + public Task FocusOutAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusOut"); + return writer.CreateMessage(); + } + } + + public Task ResetAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Reset"); + return writer.CreateMessage(); + } + } + + public Task SetCursorRectAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorRect"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task SetCapacityAsync(uint caps) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "SetCapacity"); + writer.WriteUInt32(caps); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "suu", + member: "SetSurroundingText"); + writer.WriteString(text); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uu", + member: "SetSurroundingTextPosition"); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task DestroyICAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "DestroyIC"); + return writer.CreateMessage(); + } + } + + public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, int type, uint time) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_i(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uuuiu", + member: "ProcessKeyEvent"); + writer.WriteUInt32(keyval); + writer.WriteUInt32(keycode); + writer.WriteUInt32(state); + writer.WriteInt32(type); + writer.WriteUInt32(time); + return writer.CreateMessage(); + } + } + + public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => + ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchCurrentIMAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => + ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => + ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchForwardKeyAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => + ReadMessage_uui(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchDeleteSurroundingTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => + ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); + } + + internal class InputMethod : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputMethod"; + + public InputMethod(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task<(int Icid, bool Enable, uint Keyval1, uint State1, uint Keyval2, uint State2)> CreateICv3Async(string appname, int pid) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ibuuuu(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "si", + member: "CreateICv3"); + writer.WriteString(appname); + writer.WriteInt32(pid); + return writer.CreateMessage(); + } + } + } + + internal class InputContext1 : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputContext1"; + + public InputContext1(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task FocusInAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusIn"); + return writer.CreateMessage(); + } + } + + public Task FocusOutAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusOut"); + return writer.CreateMessage(); + } + } + + public Task ResetAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Reset"); + return writer.CreateMessage(); + } + } + + public Task SetCursorRectAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorRect"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task SetCapabilityAsync(ulong caps) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "t", + member: "SetCapability"); + writer.WriteUInt64(caps); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "suu", + member: "SetSurroundingText"); + writer.WriteString(text); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uu", + member: "SetSurroundingTextPosition"); + writer.WriteUInt32(cursor); + writer.WriteUInt32(anchor); + return writer.CreateMessage(); + } + } + + public Task DestroyICAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "DestroyIC"); + return writer.CreateMessage(); + } + } + + public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, bool type, uint time) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uuubu", + member: "ProcessKeyEvent"); + writer.WriteUInt32(keyval); + writer.WriteUInt32(keycode); + writer.WriteUInt32(state); + writer.WriteBool(type); + writer.WriteUInt32(time); + return writer.CreateMessage(); + } + } + + public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => + ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchCurrentIMAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => + ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => + ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchForwardKeyAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => + ReadMessage_uub(m, (FcitxObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchDeleteSurroundingTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => + ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); + } + + internal class InputMethod1 : FcitxObject + { + private const string Interface = "org.fcitx.Fcitx.InputMethod1"; + + public InputMethod1(FcitxService service, ObjectPath path) : base(service, path) { } + + public Task<(ObjectPath A0, byte[] A1)> CreateInputContextAsync((string, string)[] a0) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_oay(m, (FcitxObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "a(ss)", + member: "CreateInputContext"); + writer.WriteArray(a0); + return writer.CreateMessage(); + } + } + } + + internal class FcitxService + { + public FcitxService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public InputContext CreateInputContext(string path) => new(this, path); + public InputMethod CreateInputMethod(string path) => new(this, path); + public InputContext1 CreateInputContext1(string path) => new(this, path); + public InputMethod1 CreateInputMethod1(string path) => new(this, path); + } + + internal class FcitxObject + { + protected FcitxObject(FcitxService service, ObjectPath path) + => (Service, Path) = (service, path); + + public FcitxService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) => + null!, static (ex, _, _, hs) => + ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static int ReadMessage_i(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadInt32(); + } + + protected static string ReadMessage_s(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static (string, string, string) ReadMessage_sss(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadString(); + return (arg0, arg1, arg2); + } + + protected static ((string, int)[], int) ReadMessage_arsizi(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadArray<(string, int)>(); + var arg1 = reader.ReadInt32(); + return (arg0, arg1); + } + + protected static (uint, uint, int) ReadMessage_uui(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadInt32(); + return (arg0, arg1, arg2); + } + + protected static (int, uint) ReadMessage_iu(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadInt32(); + var arg1 = reader.ReadUInt32(); + return (arg0, arg1); + } + + protected static (int, bool, uint, uint, uint, uint) ReadMessage_ibuuuu(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadInt32(); + var arg1 = reader.ReadBool(); + var arg2 = reader.ReadUInt32(); + var arg3 = reader.ReadUInt32(); + var arg4 = reader.ReadUInt32(); + var arg5 = reader.ReadUInt32(); + return (arg0, arg1, arg2, arg3, arg4, arg5); + } + + protected static bool ReadMessage_b(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + + protected static (uint, uint, bool) ReadMessage_uub(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadBool(); + return (arg0, arg1, arg2); + } + + protected static (ObjectPath, byte[]) ReadMessage_oay(Message message, FcitxObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadObjectPath(); + var arg1 = reader.ReadArray(); + return (arg0, arg1); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs deleted file mode 100644 index 06afacaa29..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxDBus.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.Fcitx -{ - [DBusInterface("org.fcitx.Fcitx.InputMethod")] - interface IFcitxInputMethod : IDBusObject - { - Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async( - string Appname, int Pid); - } - - - [DBusInterface("org.fcitx.Fcitx.InputContext")] - interface IFcitxInputContext : IDBusObject - { - Task EnableICAsync(); - Task CloseICAsync(); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task MouseEventAsync(int X); - Task SetCursorLocationAsync(int X, int Y); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapacityAsync(uint Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); - Task WatchEnableIMAsync(Action handler, Action? onError = null); - Task WatchCloseIMAsync(Action handler, Action? onError = null); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputContext1")] - interface IFcitxInputContext1 : IDBusObject - { - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCursorRectAsync(int X, int Y, int W, int H); - Task SetCapabilityAsync(ulong Caps); - Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); - Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); - Task DestroyICAsync(); - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); - Task WatchCommitStringAsync(Action handler, Action? onError = null); - Task WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action? onError = null); - Task WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action? onError = null); - Task WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action? onError = null); - } - - [DBusInterface("org.fcitx.Fcitx.InputMethod1")] - interface IFcitxInputMethod1 : IDBusObject - { - Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index 6c503edb41..b71ca55cf8 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -5,15 +5,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly IFcitxInputContext1? _modern; - private readonly IFcitxInputContext? _old; + private readonly InputContext1? _modern; + private readonly InputContext? _old; - public FcitxICWrapper(IFcitxInputContext old) + public FcitxICWrapper(InputContext old) { _old = old; } - public FcitxICWrapper(IFcitxInputContext1 modern) + public FcitxICWrapper(InputContext1 modern) { _modern = modern; } @@ -21,32 +21,30 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern?.FocusInAsync() ?? Task.CompletedTask; public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern?.FocusOutAsync() ?? Task.CompletedTask; - + public Task ResetAsync() => _old?.ResetAsync() ?? _modern?.ResetAsync() ?? Task.CompletedTask; public Task SetCursorRectAsync(int x, int y, int w, int h) => _old?.SetCursorRectAsync(x, y, w, h) ?? _modern?.SetCursorRectAsync(x, y, w, h) ?? Task.CompletedTask; + public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern?.DestroyICAsync() ?? Task.CompletedTask; public async Task ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) { - if(_old!=null) + if (_old is not null) return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; return await (_modern?.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time) ?? Task.FromResult(false)); } - public Task WatchCommitStringAsync(Action handler) => - _old?.WatchCommitStringAsync(handler) - ?? _modern?.WatchCommitStringAsync(handler) - ?? Task.FromResult(default(IDisposable?)); + public ValueTask WatchCommitStringAsync(Action handler) => + _old?.WatchCommitStringAsync(handler) + ?? _modern?.WatchCommitStringAsync(handler) + ?? new ValueTask(default(IDisposable?)); - public Task WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) - { - return _old?.WatchForwardKeyAsync(handler) - ?? _modern?.WatchForwardKeyAsync(ev => - handler((ev.keyval, ev.state, ev.type ? 1 : 0))) - ?? Task.FromResult(default(IDisposable?)); - } + public ValueTask WatchForwardKeyAsync(Action handler) => + _old?.WatchForwardKeyAsync(handler) + ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0))) + ?? new ValueTask(default(IDisposable?)); public Task SetCapacityAsync(uint flags) => _old?.SetCapacityAsync(flags) ?? _modern?.SetCapabilityAsync(flags) ?? Task.CompletedTask; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 791431dfa7..2a5260b1cf 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -1,12 +1,10 @@ using System; using System.Diagnostics; -using System.Reactive.Concurrency; -using System.Reflection; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { @@ -15,32 +13,25 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx private FcitxICWrapper? _context; private FcitxCapabilityFlags? _lastReportedFlags; - public FcitxX11TextInputMethod(Connection connection) : base(connection, - "org.fcitx.Fcitx", - "org.freedesktop.portal.Fcitx" - ) - { - - } + public FcitxX11TextInputMethod(Connection connection) : base(connection, "org.fcitx.Fcitx", "org.freedesktop.portal.Fcitx") { } protected override async Task Connect(string name) { + var service = new FcitxService(Connection, name); if (name == "org.fcitx.Fcitx") { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = service.CreateInputMethod("/inputmethod"); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = Connection.CreateProxy(name, - "/inputcontext_" + resp.icid); - + var proxy = service.CreateInputContext($"/inputcontext_{resp.Icid}"); _context = new FcitxICWrapper(proxy); } else { - var method = Connection.CreateProxy(name, "/inputmethod"); + var method = service.CreateInputMethod1("/inputmethod"); var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); - var proxy = Connection.CreateProxy(name, resp.path); + var proxy = service.CreateInputContext1(resp.A0); _context = new FcitxICWrapper(proxy); } @@ -49,7 +40,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx return true; } - protected override Task Disconnect() => _context?.DestroyICAsync() ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _context?.DestroyICAsync() ?? Task.CompletedTask; protected override void OnDisconnected() => _context = null; @@ -64,14 +55,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx Math.Max(1, cursorRect.Height)) ?? Task.CompletedTask; - protected override Task SetActiveCore(bool active)=> (active + protected override Task SetActiveCore(bool active)=> (active ? _context?.FocusInAsync() : _context?.FocusOutAsync()) ?? Task.CompletedTask; - - protected override Task ResetContextCore() => _context?.ResetAsync() - ?? Task.CompletedTask; + protected override Task ResetContextCore() => _context?.ResetAsync() ?? Task.CompletedTask; protected override async Task HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) { @@ -88,17 +77,13 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx var type = args.Type == RawKeyEventType.KeyDown ? FcitxKeyEventType.FCITX_PRESS_KEY : FcitxKeyEventType.FCITX_RELEASE_KEY; - if (_context is { }) - { + if (_context is not null) return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, (uint)args.Timestamp).ConfigureAwait(false); - } - else - { - return false; - } + + return false; } - + public override void SetOptions(TextInputOptions options) => Enqueue(async () => { @@ -128,7 +113,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx } }); - private void OnForward((uint keyval, uint state, int type) ev) + private void OnForward(Exception? e, (uint keyval, uint state, int type) ev) { var state = (FcitxKeyState)ev.state; KeyModifiers mods = default; @@ -150,6 +135,6 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx }); } - private void OnCommitString(string s) => FireCommit(s); + private void OnCommitString(Exception? e, string s) => FireCommit(s); } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs new file mode 100644 index 0000000000..e0f9681766 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs @@ -0,0 +1,513 @@ +using System; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop.DBusIme.IBus +{ + internal class Portal : IBusObject + { + private const string Interface = "org.freedesktop.IBus.Portal"; + + public Portal(IBusService service, ObjectPath path) : base(service, path) { } + + public Task CreateInputContextAsync(string clientName) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (IBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "CreateInputContext"); + writer.WriteString(clientName); + return writer.CreateMessage(); + } + } + } + + internal class InputContext : IBusObject + { + private const string Interface = "org.freedesktop.IBus.InputContext"; + + public InputContext(IBusService service, ObjectPath path) : base(service, path) { } + + public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state) + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (IBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "uuu", + member: "ProcessKeyEvent"); + writer.WriteUInt32(keyval); + writer.WriteUInt32(keycode); + writer.WriteUInt32(state); + return writer.CreateMessage(); + } + } + + public Task SetCursorLocationAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorLocation"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task SetCursorLocationRelativeAsync(int x, int y, int w, int h) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iiii", + member: "SetCursorLocationRelative"); + writer.WriteInt32(x); + writer.WriteInt32(y); + writer.WriteInt32(w); + writer.WriteInt32(h); + return writer.CreateMessage(); + } + } + + public Task ProcessHandWritingEventAsync(double[] coordinates) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ad", + member: "ProcessHandWritingEvent"); + writer.WriteArray(coordinates); + return writer.CreateMessage(); + } + } + + public Task CancelHandWritingAsync(uint nStrokes) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "CancelHandWriting"); + writer.WriteUInt32(nStrokes); + return writer.CreateMessage(); + } + } + + public Task FocusInAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusIn"); + return writer.CreateMessage(); + } + } + + public Task FocusOutAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "FocusOut"); + return writer.CreateMessage(); + } + } + + public Task ResetAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Reset"); + return writer.CreateMessage(); + } + } + + public Task SetCapabilitiesAsync(uint caps) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "u", + member: "SetCapabilities"); + writer.WriteUInt32(caps); + return writer.CreateMessage(); + } + } + + public Task PropertyActivateAsync(string name, uint state) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "su", + member: "PropertyActivate"); + writer.WriteString(name); + writer.WriteUInt32(state); + return writer.CreateMessage(); + } + } + + public Task SetEngineAsync(string name) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "SetEngine"); + writer.WriteString(name); + return writer.CreateMessage(); + } + } + + public Task GetEngineAsync() + { + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_v(m, (IBusObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "GetEngine"); + return writer.CreateMessage(); + } + } + + public Task SetSurroundingTextAsync(object text, uint cursorPos, uint anchorPos) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "vuu", + member: "SetSurroundingText"); + writer.WriteVariant(text); + writer.WriteUInt32(cursorPos); + writer.WriteUInt32(anchorPos); + return writer.CreateMessage(); + } + } + + public ValueTask WatchCommitTextAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "CommitText", static (m, s) => + ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchForwardKeyEventAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKeyEvent", static (m, s) => + ReadMessage_uuu(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdatePreeditTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditText", static (m, s) => + ReadMessage_vub(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdatePreeditTextWithModeAsync( + Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditTextWithMode", static (m, s) => + ReadMessage_vubu(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchShowPreeditTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ShowPreeditText", handler, emitOnCapturedContext); + + public ValueTask WatchHidePreeditTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "HidePreeditText", handler, emitOnCapturedContext); + + public ValueTask WatchUpdateAuxiliaryTextAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateAuxiliaryText", static (m, s) => + ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchShowAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ShowAuxiliaryText", handler, emitOnCapturedContext); + + public ValueTask WatchHideAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "HideAuxiliaryText", handler, emitOnCapturedContext); + + public ValueTask WatchUpdateLookupTableAsync(Action handler, + bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "UpdateLookupTable", static (m, s) => + ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchShowLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ShowLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchHideLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "HideLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchPageUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "PageUpLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchPageDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "PageDownLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchCursorUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "CursorUpLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchCursorDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "CursorDownLookupTable", handler, emitOnCapturedContext); + + public ValueTask WatchRegisterPropertiesAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "RegisterProperties", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchUpdatePropertyAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "UpdateProperty", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); + } + + internal class Service : IBusObject + { + private const string Interface = "org.freedesktop.IBus.Service"; + + public Service(IBusService service, ObjectPath path) : base(service, path) { } + + public Task DestroyAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Destroy"); + return writer.CreateMessage(); + } + } + } + + internal class IBusService + { + public IBusService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public Portal CreatePortal(string path) => new(this, path); + public InputContext CreateInputContext(string path) => new(this, path); + public Service CreateService(string path) => new(this, path); + } + + internal class IBusObject + { + protected IBusObject(IBusService service, ObjectPath path) + => (Service, Path) = (service, path); + + public IBusService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static ObjectPath ReadMessage_o(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadObjectPath(); + } + + protected static bool ReadMessage_b(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + + protected static object ReadMessage_v(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadVariant(); + } + + protected static (uint, uint, uint) ReadMessage_uuu(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadUInt32(); + return (arg0, arg1, arg2); + } + + protected static (object, uint, bool) ReadMessage_vub(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadBool(); + return (arg0, arg1, arg2); + } + + protected static (object, uint, bool, uint) ReadMessage_vubu(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadUInt32(); + var arg2 = reader.ReadBool(); + var arg3 = reader.ReadUInt32(); + return (arg0, arg1, arg2, arg3); + } + + protected static (object, bool) ReadMessage_vb(Message message, IBusObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadBool(); + return (arg0, arg1); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs deleted file mode 100644 index 4ef034adb9..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusDBus.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusIme.IBus -{ - [DBusInterface("org.freedesktop.IBus.InputContext")] - interface IIBusInputContext : IDBusObject - { - Task ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); - Task SetCursorLocationAsync(int X, int Y, int W, int H); - Task FocusInAsync(); - Task FocusOutAsync(); - Task ResetAsync(); - Task SetCapabilitiesAsync(uint Caps); - Task PropertyActivateAsync(string Name, int State); - Task SetEngineAsync(string Name); - Task GetEngineAsync(); - Task DestroyAsync(); - Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); - Task WatchCommitTextAsync(Action cb, Action? onError = null); - Task WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action? onError = null); - Task WatchRequireSurroundingTextAsync(Action handler, Action? onError = null); - Task WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action? onError = null); - Task WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action? onError = null); - Task WatchShowPreeditTextAsync(Action handler, Action? onError = null); - Task WatchHidePreeditTextAsync(Action handler, Action? onError = null); - Task WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action? onError = null); - Task WatchShowAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchHideAuxiliaryTextAsync(Action handler, Action? onError = null); - Task WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action? onError = null); - Task WatchShowLookupTableAsync(Action handler, Action? onError = null); - Task WatchHideLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchPageDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorUpLookupTableAsync(Action handler, Action? onError = null); - Task WatchCursorDownLookupTableAsync(Action handler, Action? onError = null); - Task WatchRegisterPropertiesAsync(Action handler, Action? onError = null); - Task WatchUpdatePropertyAsync(Action handler, Action? onError = null); - } - - - [DBusInterface("org.freedesktop.IBus.Portal")] - interface IIBusPortal : IDBusObject - { - Task CreateInputContextAsync(string Name); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs index 3070f51a8e..b8ac816694 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -18,7 +18,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus Button3Mask = 1 << 10, Button4Mask = 1 << 11, Button5Mask = 1 << 12, - + HandledMask = 1 << 24, ForwardMask = 1 << 25, IgnoredMask = ForwardMask, diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 2324ca44a7..f3ab8617a4 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private IIBusInputContext? _context; + private Service? _service; + private InputContext? _context; - public IBusX11TextInputMethod(Connection connection) : base(connection, - "org.freedesktop.portal.IBus") - { - } + public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } protected override async Task Connect(string name) { - var path = - await Connection.CreateProxy(name, "/org/freedesktop/IBus") - .CreateInputContextAsync(GetAppName()); - - _context = Connection.CreateProxy(name, path); + var service = new IBusService(Connection, name); + var path = await service.CreatePortal("/org/freedesktop/IBus").CreateInputContextAsync(GetAppName()); + _context = service.CreateInputContext(path); + _service = service.CreateService(path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); return true; } - private void OnForwardKey((uint keyval, uint keycode, uint state) k) + private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k) { var state = (IBusModifierMask)k.state; KeyModifiers mods = default; @@ -49,8 +46,7 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus }); } - - private void OnCommitText(object wtf) + private void OnCommitText(Exception? e, object wtf) { // Hello darkness, my old friend if (wtf.GetType().GetField("Item3") is { } prop) @@ -61,16 +57,16 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus } } - protected override Task Disconnect() => _context?.DestroyAsync() - ?? Task.CompletedTask; + protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask; protected override void OnDisconnected() { + _service = null; _context = null; base.OnDisconnected(); } - protected override Task SetCursorRectCore(PixelRect rect) + protected override Task SetCursorRectCore(PixelRect rect) => _context?.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height) ?? Task.CompletedTask; @@ -96,20 +92,12 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus if (args.Type == RawKeyEventType.KeyUp) state |= IBusModifierMask.ReleaseMask; - if(_context is { }) - { - return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); - } - else - { - return Task.FromResult(false); - } - + return _context is not null ? _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state) : Task.FromResult(false); } public override void SetOptions(TextInputOptions options) { - // No-op, because ibus + // No-op, because ibus } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs index 86978c8b60..c68e10043b 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/X11DBusImeHelper.cs @@ -2,44 +2,43 @@ using System; using System.Collections.Generic; using Avalonia.FreeDesktop.DBusIme.Fcitx; using Avalonia.FreeDesktop.DBusIme.IBus; -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop.DBusIme { public class X11DBusImeHelper { - private static readonly Dictionary> KnownMethods = - new Dictionary> + private static readonly Dictionary> KnownMethods = new() { - ["fcitx"] = conn => + ["fcitx"] = static conn => new DBusInputMethodFactory(_ => new FcitxX11TextInputMethod(conn)), - ["ibus"] = conn => + ["ibus"] = static conn => new DBusInputMethodFactory(_ => new IBusX11TextInputMethod(conn)) }; - - static Func? DetectInputMethod() + + private static Func? DetectInputMethod() { foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) { var value = Environment.GetEnvironmentVariable(name); - + if (value == "none") return null; - - if (value != null && KnownMethods.TryGetValue(value, out var factory)) + + if (value is not null && KnownMethods.TryGetValue(value, out var factory)) return factory; } return null; } - + public static bool DetectAndRegister() { var factory = DetectInputMethod(); - if (factory != null) + if (factory is not null) { var conn = DBusHelper.TryInitialize(); - if (conn != null) + if (conn is not null) { AvaloniaLocator.CurrentMutable.Bind().ToConstant(factory(conn)); return true; diff --git a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs new file mode 100644 index 0000000000..d9016d8644 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs @@ -0,0 +1,463 @@ +using System; +using Tmds.DBus.Protocol; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.FreeDesktop +{ + internal record DBusMenuProperties + { + public uint Version { get; set; } + public string TextDirection { get; set; } = default!; + public string Status { get; set; } = default!; + public string[] IconThemePath { get; set; } = default!; + } + + internal class DBusMenu : DBusMenuObject + { + private const string Interface = "com.canonical.dbusmenu"; + public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) + { } + public Task<(uint Revision, (int, Dictionary, object[]) Layout)> GetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_uriaesvavz(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "iias", + member: "GetLayout"); + writer.WriteInt32(parentId); + writer.WriteInt32(recursionDepth); + writer.WriteArray(propertyNames); + return writer.CreateMessage(); + } + } + public Task<(int, Dictionary)[]> GetGroupPropertiesAsync(int[] ids, string[] propertyNames) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ariaesvz(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "aias", + member: "GetGroupProperties"); + writer.WriteArray(ids); + writer.WriteArray(propertyNames); + return writer.CreateMessage(); + } + } + public Task GetPropertyAsync(int id, string name) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_v(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "is", + member: "GetProperty"); + writer.WriteInt32(id); + writer.WriteString(name); + return writer.CreateMessage(); + } + } + public Task EventAsync(int id, string eventId, object data, uint timestamp) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "isvu", + member: "Event"); + writer.WriteInt32(id); + writer.WriteString(eventId); + writer.WriteVariant(data); + writer.WriteUInt32(timestamp); + return writer.CreateMessage(); + } + } + public Task EventGroupAsync((int, string, object, uint)[] events) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ai(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "a(isvu)", + member: "EventGroup"); + writer.WriteArray(events); + return writer.CreateMessage(); + } + } + public Task AboutToShowAsync(int id) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_b(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "i", + member: "AboutToShow"); + writer.WriteInt32(id); + return writer.CreateMessage(); + } + } + public Task<(int[] UpdatesNeeded, int[] IdErrors)> AboutToShowGroupAsync(int[] ids) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_aiai(m, (DBusMenuObject)s!), this); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ai", + member: "AboutToShowGroup"); + writer.WriteArray(ids); + return writer.CreateMessage(); + } + } + public ValueTask WatchItemsPropertiesUpdatedAsync(Action)[] UpdatedProps, (int, string[])[] RemovedProps)> handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", (m, s) => ReadMessage_ariaesvzariasz(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + public ValueTask WatchLayoutUpdatedAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", (m, s) => ReadMessage_ui(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + public ValueTask WatchItemActivationRequestedAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", (m, s) => ReadMessage_iu(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + public Task SetTextDirectionAsync(string value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("TextDirection"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetStatusAsync(string value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("Status"); + writer.WriteSignature("s"); + writer.WriteString(value); + return writer.CreateMessage(); + } + } + public Task SetIconThemePathAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("IconThemePath"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), (m, s) => ReadMessage_v_u(m, (DBusMenuObject)s!), this); + public Task GetTextDirectionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + public Task GetStatusAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + public Task GetIconThemePathAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), (m, s) => ReadMessage_v_as(m, (DBusMenuObject)s!), this); + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), (m, s) => ReadMessage(m, (DBusMenuObject)s!), this); + static DBusMenuProperties ReadMessage(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + static PropertyChanges ReadMessage(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(), invalidated = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "Version": invalidated.Add("Version"); break; + case "TextDirection": invalidated.Add("TextDirection"); break; + case "Status": invalidated.Add("Status"); break; + case "IconThemePath": invalidated.Add("IconThemePath"); break; + } + } + return invalidated?.ToArray() ?? Array.Empty(); + } + } + private static DBusMenuProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new DBusMenuProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "Version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + case "TextDirection": + reader.ReadSignature("s"); + props.TextDirection = reader.ReadString(); + changedList?.Add("TextDirection"); + break; + case "Status": + reader.ReadSignature("s"); + props.Status = reader.ReadString(); + changedList?.Add("Status"); + break; + case "IconThemePath": + reader.ReadSignature("as"); + props.IconThemePath = reader.ReadArray(); + changedList?.Add("IconThemePath"); + break; + default: + reader.ReadVariant(); + break; + } + } + return props; + } + } + + internal class DBusMenuService + { + public Connection Connection { get; } + public string Destination { get; } + public DBusMenuService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + public DBusMenu CreateDbusmenu(string path) => new DBusMenu(this, path); + } + + internal class DBusMenuObject + { + public DBusMenuService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + protected DBusMenuObject(DBusMenuService service, ObjectPath path) + => (Service, Path) = (service, path); + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, changes, rs, hs) => ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, (message, state) => null!, + (ex, v, rs, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadStruct, object[]>(); + return (arg0, arg1); + } + protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray<(int, Dictionary)>(); + } + protected static object ReadMessage_v(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadVariant(); + } + protected static int[] ReadMessage_ai(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadArray(); + } + protected static bool ReadMessage_b(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadBool(); + } + protected static (int[], int[]) ReadMessage_aiai(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadArray(); + var arg1 = reader.ReadArray(); + return (arg0, arg1); + } + protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadArray<(int, Dictionary)>(); + var arg1 = reader.ReadArray<(int, string[])>(); + return (arg0, arg1); + } + protected static (uint, int) ReadMessage_ui(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadInt32(); + return (arg0, arg1); + } + protected static (int, uint) ReadMessage_iu(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadInt32(); + var arg1 = reader.ReadUInt32(); + return (arg0, arg1); + } + protected static uint ReadMessage_v_u(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("u"); + return reader.ReadUInt32(); + } + protected static string ReadMessage_v_s(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("s"); + return reader.ReadString(); + } + protected static string[] ReadMessage_v_as(Message message, DBusMenuObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("as"); + return reader.ReadArray(); + } + } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs deleted file mode 100644 index 3a1c65e7c9..0000000000 --- a/src/Avalonia.FreeDesktop/DBusMenu.cs +++ /dev/null @@ -1,56 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop.DBusMenu -{ - - [DBusInterface("org.freedesktop.DBus.Properties")] - interface IFreeDesktopDBusProperties : IDBusObject - { - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - [DBusInterface("com.canonical.dbusmenu")] - interface IDBusMenu : IFreeDesktopDBusProperties - { - Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync(int ParentId, int RecursionDepth, string[] PropertyNames); - Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames); - Task GetPropertyAsync(int Id, string Name); - Task EventAsync(int Id, string EventId, object Data, uint Timestamp); - Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] events); - Task AboutToShowAsync(int Id); - Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids); - Task WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError = null); - Task WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError = null); - Task WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError = null); - } - - [Dictionary] - class DBusMenuProperties - { - public uint Version { get; set; } = default (uint); - public string? TextDirection { get; set; } = default (string); - public string? Status { get; set; } = default (string); - public string[]? IconThemePath { get; set; } = default (string[]); - } - - - [DBusInterface("com.canonical.AppMenu.Registrar")] - interface IRegistrar : IDBusObject - { - Task RegisterWindowAsync(uint WindowId, ObjectPath MenuObjectPath); - Task UnregisterWindowAsync(uint WindowId); - Task<(string service, ObjectPath menuObjectPath)> GetMenuForWindowAsync(uint WindowId); - Task<(uint, string, ObjectPath)[]> GetMenusAsync(); - Task WatchWindowRegisteredAsync(Action<(uint windowId, string service, ObjectPath menuObjectPath)> handler, Action? onError = null); - Task WatchWindowUnregisteredAsync(Action handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 657e324010..682434bc8d 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,98 +2,81 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using System.Reactive.Disposables; +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.FreeDesktop.DBusMenu; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Threading; -using Tmds.DBus; +using Tmds.DBus.Protocol; + #pragma warning disable 1998 namespace Avalonia.FreeDesktop { public class DBusMenuExporter { - public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) - { - if (DBusHelper.Connection == null) - return null; + public static ITopLevelNativeMenuExporter? TryCreateTopLevelNativeMenu(IntPtr xid) => + DBusHelper.Connection is null ? null : new DBusMenuExporterImpl(DBusHelper.Connection, xid); - return new DBusMenuExporterImpl(DBusHelper.Connection, xid); - } - - public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection) - { - return new DBusMenuExporterImpl(currentConection, path); - } + public static INativeMenuExporter TryCreateDetachedNativeMenu(string path, Connection currentConnection) => + new DBusMenuExporterImpl(currentConnection, path); - public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/" - + Guid.NewGuid().ToString("N"); + public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable + private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IMethodHandler, IDisposable { - private readonly Connection _dbus; + private readonly Connection _connection; + private readonly DBusMenuProperties _dBusMenuProperties = new() { Version = 2, Status = "normal" }; + private readonly Dictionary _idsToItems = new(); + private readonly Dictionary _itemsToIds = new(); + private readonly HashSet _menus = new(); private readonly uint _xid; - private IRegistrar? _registrar; + private readonly bool _appMenu = true; + private Registrar? _registrar; + private NativeMenu? _menu; private bool _disposed; private uint _revision = 1; - private NativeMenu? _menu; - private readonly Dictionary _idsToItems = new Dictionary(); - private readonly Dictionary _itemsToIds = new Dictionary(); - private readonly HashSet _menus = new HashSet(); private bool _resetQueued; private int _nextId = 1; - private bool _appMenu = true; - - public DBusMenuExporterImpl(Connection dbus, IntPtr xid) + + public DBusMenuExporterImpl(Connection connection, IntPtr xid) { - _dbus = dbus; + _connection = connection; _xid = (uint)xid.ToInt32(); - ObjectPath = GenerateDBusMenuObjPath; + Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); Init(); } - public DBusMenuExporterImpl(Connection dbus, ObjectPath path) + public DBusMenuExporterImpl(Connection connection, string path) { - _dbus = dbus; + _connection = connection; _appMenu = false; - ObjectPath = path; + Path = path; SetNativeMenu(new NativeMenu()); Init(); } - - async void Init() - { - try - { - if (_appMenu) - { - await _dbus.RegisterObjectAsync(this); - _registrar = DBusHelper.Connection?.CreateProxy( - "com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar"); - if (!_disposed && _registrar is { }) - await _registrar.RegisterWindowAsync(_xid, ObjectPath); - } - else - { - await _dbus.RegisterObjectAsync(this); - } - } - catch (Exception e) - { - Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform) - ?.Log(this, e.Message); - // It's not really important if this code succeeds, - // and it's not important to know if it succeeds - // since even if we register the window it's not guaranteed that - // menu will be actually exported - } + public string Path { get; } + + private async void Init() + { + _connection.AddMethodHandler(this); + if (!_appMenu) + return; + var services = await _connection.ListServicesAsync(); + if (!services.Contains("com.canonical.AppMenu.Registrar")) + return; + _registrar = new RegistrarService(_connection, "com.canonical.AppMenu.Registrar") + .CreateRegistrar("/com/canonical/AppMenu/Registrar"); + if (!_disposed) + await _registrar.RegisterWindowAsync(_xid, Path); + // It's not really important if this code succeeds, + // and it's not important to know if it succeeds + // since even if we register the window it's not guaranteed that + // menu will be actually exported } public void Dispose() @@ -101,7 +84,7 @@ namespace Avalonia.FreeDesktop if (_disposed) return; _disposed = true; - _dbus.UnregisterObject(this); + // Fire and forget _registrar?.UnregisterWindowAsync(_xid); } @@ -113,17 +96,17 @@ namespace Avalonia.FreeDesktop public void SetNativeMenu(NativeMenu? menu) { - if (menu == null) + if (menu is null) menu = new NativeMenu(); - if (_menu != null) + if (_menu is not null) ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; _menu = menu; ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; - + DoLayoutReset(); } - + /* This is basic initial implementation, so we don't actually track anything and just reset the whole layout on *ANY* change @@ -131,10 +114,10 @@ namespace Avalonia.FreeDesktop This is not how it should work and will prevent us from implementing various features, but that's the fastest way to get things working, so... */ - void DoLayoutReset() + private void DoLayoutReset() { _resetQueued = false; - foreach (var i in _idsToItems.Values) + foreach (var i in _idsToItems.Values) i.PropertyChanged -= OnItemPropertyChanged; foreach(var menu in _menus) ((INotifyCollectionChanged)menu.Items).CollectionChanged -= OnMenuItemsChanged; @@ -142,10 +125,10 @@ namespace Avalonia.FreeDesktop _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; - LayoutUpdated?.Invoke((_revision, 0)); + EmitUIntIntSignal("LayoutUpdated", _revision, 0); } - void QueueReset() + private void QueueReset() { if(_resetQueued) return; @@ -163,10 +146,10 @@ namespace Avalonia.FreeDesktop private void EnsureSubscribed(NativeMenu? menu) { - if(menu!=null && _menus.Add(menu)) + if (menu is not null && _menus.Add(menu)) ((INotifyCollectionChanged)menu.Items).CollectionChanged += OnMenuItemsChanged; } - + private int GetId(NativeMenuItemBase item) { if (_itemsToIds.TryGetValue(item, out var id)) @@ -190,33 +173,11 @@ namespace Avalonia.FreeDesktop QueueReset(); } - public ObjectPath ObjectPath { get; } - - - async Task IFreeDesktopDBusProperties.GetAsync(string prop) - { - if (prop == "Version") - return 2; - if (prop == "Status") - return "normal"; - return 0; - } - - async Task IFreeDesktopDBusProperties.GetAllAsync() - { - return new DBusMenuProperties - { - Version = 2, - Status = "normal", - }; - } - - private static string[] AllProperties = new[] - { + private static readonly string[] AllProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - - object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + + private object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; @@ -228,24 +189,20 @@ namespace Avalonia.FreeDesktop else if (it is NativeMenuItem item) { if (name == "type") - { return null; - } if (name == "label") - return item?.Header ?? ""; + return item.Header ?? ""; if (name == "enabled") { - if (item == null) - return null; - if (item.Menu != null && item.Menu.Items.Count == 0) + if (item.Menu is not null && item.Menu.Items.Count == 0) return false; - if (item.IsEnabled == false) + if (!item.IsEnabled) return false; return null; } if (name == "shortcut") { - if (item?.Gesture == null) + if (item.Gesture is null) return null; if (item.Gesture.KeyModifiers == 0) return null; @@ -271,19 +228,16 @@ namespace Avalonia.FreeDesktop return "radio"; } - if (name == "toggle-state") - { - if (item.ToggleType != NativeMenuItemToggleType.None) - return item.IsChecked ? 1 : 0; - } - + if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) + return item.IsChecked ? 1 : 0; + if (name == "icon-data") { - if (item.Icon != null) + if (item.Icon is not null) { var loader = AvaloniaLocator.Current.GetService(); - if (loader != null) + if (loader is not null) { var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); @@ -293,155 +247,199 @@ namespace Avalonia.FreeDesktop } } } - + if (name == "children-display") - return menu != null ? "submenu" : null; + return menu is not null ? "submenu" : null; } return null; } - private List> _reusablePropertyList = new List>(); - KeyValuePair[] GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { - if (names?.Length > 0 != true) + if (names.Length == 0) names = AllProperties; - _reusablePropertyList.Clear(); + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); - if (v != null) - _reusablePropertyList.Add(new KeyValuePair(n, v)); + if (v is not null) + properties.Add(n, v); } - return _reusablePropertyList.ToArray(); + return properties; } - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task<(uint revision, (int, KeyValuePair[], object[]) layout)> GetLayoutAsync( - int ParentId, int RecursionDepth, string[] PropertyNames) + private (int, Dictionary, object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { - var menu = GetMenu(ParentId); - var rv = (_revision, GetLayout(menu.item, menu.menu, RecursionDepth, PropertyNames)); - if (!IsNativeMenuExported) - { - IsNativeMenuExported = true; - Dispatcher.UIThread.Post(() => - { - OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); - }); - } - return Task.FromResult(rv); - } - - (int, KeyValuePair[], object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) - { - var id = item == null ? 0 : GetId(item); + var id = item is null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); - var children = (depth == 0 || menu == null) ? Array.Empty() : new object[menu.Items.Count]; - if(menu != null) + var children = depth == 0 || menu is null ? Array.Empty() : new object[menu.Items.Count]; + if (menu is not null) + { for (var c = 0; c < children.Length; c++) { var ch = menu.Items[c]; - children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); } - - return (id, props, children); - } - - public Task<(int, KeyValuePair[])[]> GetGroupPropertiesAsync(int[] Ids, string[] PropertyNames) - { - var arr = new (int, KeyValuePair[])[Ids.Length]; - for (var c = 0; c < Ids.Length; c++) - { - var id = Ids[c]; - var item = GetMenu(id); - var props = GetProperties(item, PropertyNames); - arr[c] = (id, props); } - return Task.FromResult(arr); - } - - public async Task GetPropertyAsync(int Id, string Name) - { - return GetProperty(GetMenu(Id), Name) ?? 0; + return (id, props, children); } - - public void HandleEvent(int id, string eventId, object data, uint timestamp) + private void HandleEvent(int id, string eventId, object data, uint timestamp) { if (eventId == "clicked") { var item = GetMenu(id).item; - - if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge) - { - if (menuItem?.IsEnabled == true) - bridge?.RaiseClicked(); - } + if (item is NativeMenuItem { IsEnabled: true } and INativeMenuItemExporterEventsImplBridge bridge) + bridge.RaiseClicked(); } } - - public Task EventAsync(int Id, string EventId, object Data, uint Timestamp) - { - HandleEvent(Id, EventId, Data, Timestamp); - return Task.CompletedTask; - } - public Task EventGroupAsync((int id, string eventId, object data, uint timestamp)[] Events) + public ValueTask HandleMethodAsync(MethodContext context) { - foreach (var e in Events) - HandleEvent(e.id, e.eventId, e.data, e.timestamp); - return Task.FromResult(Array.Empty()); - } - - public async Task AboutToShowAsync(int Id) - { - return false; - } - - public async Task<(int[] updatesNeeded, int[] idErrors)> AboutToShowGroupAsync(int[] Ids) - { - return (Array.Empty(), Array.Empty()); - } + switch (context.Request.InterfaceAsString) + { + case "com.canonical.dbusmenu": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("GetLayout", "iias"): + { + using var writer = context.CreateReplyWriter("u(ia{sv}av)"); + var reader = context.Request.GetBodyReader(); + var parentId = reader.ReadInt32(); + var recursionDepth = reader.ReadInt32(); + var propertyNames = reader.ReadArray(); + var menu = GetMenu(parentId); + var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); + writer.WriteUInt32(_revision); + writer.WriteStruct(layout); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); + } + + context.Reply(writer.CreateMessage()); + break; + } + case ("GetGroupProperties", "aias"): + { + using var writer = context.CreateReplyWriter("a(ia{sv})"); + var reader = context.Request.GetBodyReader(); + var ids = reader.ReadArray(); + var propertyNames = reader.ReadArray(); + var arrayStart = writer.WriteArrayStart(DBusType.Struct); + foreach (var id in ids) + { + var item = GetMenu(id); + var props = GetProperties(item, propertyNames); + writer.WriteStruct((id, props)); + } + + writer.WriteArrayEnd(arrayStart); + context.Reply(writer.CreateMessage()); + break; + } + case ("GetProperty", "is"): + { + using var writer = context.CreateReplyWriter("v"); + var reader = context.Request.GetBodyReader(); + var id = reader.ReadInt32(); + var name = reader.ReadString(); + writer.WriteVariant(GetProperty(GetMenu(id), name) ?? 0); + context.Reply(writer.CreateMessage()); + break; + } + case ("Event", "isvu"): + { + var reader = context.Request.GetBodyReader(); + var id = reader.ReadInt32(); + var eventId = reader.ReadString(); + var data = reader.ReadVariant(); + var timestamp = reader.ReadUInt32(); + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); + break; + } + case ("EventGroup", "a(isvu)"): + { + using var writer = context.CreateReplyWriter("ai"); + var reader = context.Request.GetBodyReader(); + var events = reader.ReadArray<(int Id, string EventId, object Data, uint Timestamp)>(); + foreach (var e in events) + Dispatcher.UIThread.Post(() => HandleEvent(e.Id, e.EventId, e.Data, e.Timestamp)); + writer.WriteArray(Array.Empty()); + context.Reply(writer.CreateMessage()); + break; + } + case ("AboutToShow", "i"): + { + using var writer = context.CreateReplyWriter("b"); + writer.WriteBool(false); + context.Reply(writer.CreateMessage()); + break; + } + case ("AboutToShowGroup", "ai"): + { + using var writer = context.CreateReplyWriter("aiai"); + writer.WriteStruct((Array.Empty(), Array.Empty())); + context.Reply(writer.CreateMessage()); + break; + } + } - #region Events + break; + case "org.freedesktop.DBus.Properties": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("Version", "u"): + { + using var writer = context.CreateReplyWriter("u"); + writer.WriteUInt32(_dBusMenuProperties.Version); + context.Reply(writer.CreateMessage()); + break; + } + case ("TextDirection", "s"): + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_dBusMenuProperties.TextDirection); + context.Reply(writer.CreateMessage()); + break; + } + case ("Status", "s"): + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_dBusMenuProperties.Status); + context.Reply(writer.CreateMessage()); + break; + } + case ("IconThemePath", "as"): + { + using var writer = context.CreateReplyWriter("as"); + writer.WriteArray(_dBusMenuProperties.IconThemePath); + context.Reply(writer.CreateMessage()); + break; + } + } - private event Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> - ItemsPropertiesUpdated { add { } remove { } } - private event Action<(uint revision, int parent)>? LayoutUpdated; - private event Action<(int id, uint timestamp)> ItemActivationRequested { add { } remove { } } - private event Action PropertiesChanged { add { } remove { } } + break; + } - async Task IDBusMenu.WatchItemsPropertiesUpdatedAsync(Action<((int, IDictionary)[] updatedProps, (int, string[])[] removedProps)> handler, Action? onError) - { - ItemsPropertiesUpdated += handler; - return Disposable.Create(() => ItemsPropertiesUpdated -= handler); - } - async Task IDBusMenu.WatchLayoutUpdatedAsync(Action<(uint revision, int parent)> handler, Action? onError) - { - LayoutUpdated += handler; - return Disposable.Create(() => LayoutUpdated -= handler); + return default; } - async Task IDBusMenu.WatchItemActivationRequestedAsync(Action<(int id, uint timestamp)> handler, Action? onError) - { - ItemActivationRequested+= handler; - return Disposable.Create(() => ItemActivationRequested -= handler); - } + public bool RunMethodHandlerSynchronously(Message message) => true; - async Task IFreeDesktopDBusProperties.WatchPropertiesAsync(Action handler) + private void EmitUIntIntSignal(string member, uint arg0, int arg1) { - PropertiesChanged += handler; - return Disposable.Create(() => PropertiesChanged -= handler); + using var writer = _connection.GetMessageWriter(); + writer.WriteSignalHeader(null, Path, "com.canonical.dbusmenu", member, "ui"); + writer.WriteUInt32(arg0); + writer.WriteInt32(arg1); + _connection.TrySendMessage(writer.CreateMessage()); } - - #endregion } } } diff --git a/src/Avalonia.FreeDesktop/DBusRequest.cs b/src/Avalonia.FreeDesktop/DBusRequest.cs deleted file mode 100644 index d84905324f..0000000000 --- a/src/Avalonia.FreeDesktop/DBusRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] -namespace Avalonia.FreeDesktop -{ - [DBusInterface("org.freedesktop.portal.Request")] - internal interface IRequest : IDBusObject - { - Task CloseAsync(); - Task WatchResponseAsync(Action<(uint response, IDictionary results)> handler, Action? onError = null); - } -} diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 8597f3922a..d59af3b5dd 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -4,45 +4,33 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; - -using Tmds.DBus; +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { internal class DBusSystemDialog : BclStorageProvider { - private static readonly Lazy s_fileChooser = new(() => DBusHelper.Connection? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop")); - - internal static async Task TryCreate(IPlatformHandle handle) + internal static async Task TryCreateAsync(IPlatformHandle handle) { - if (handle.HandleDescriptor == "XID" && s_fileChooser.Value is { } fileChooser) - { - try - { - await fileChooser.GetVersionAsync(); - return new DBusSystemDialog(fileChooser, handle); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)?.Log(null, $"Unable to connect to org.freedesktop.portal.Desktop: {e.Message}"); - return null; - } - } - - return null; + if (DBusHelper.Connection is null) + return null; + var services = await DBusHelper.Connection.ListServicesAsync(); + if (!services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal)) + return null; + return new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle); } - private readonly IFileChooser _fileChooser; + private readonly DesktopService _desktopService; + private readonly FileChooser _fileChooser; private readonly IPlatformHandle _handle; - private DBusSystemDialog(IFileChooser fileChooser, IPlatformHandle handle) + private DBusSystemDialog(DesktopService desktopService, IPlatformHandle handle) { - _fileChooser = fileChooser; + _desktopService = desktopService; + _fileChooser = desktopService.CreateFileChooser("/org/freedesktop/portal/desktop"); _handle = handle; } @@ -59,20 +47,24 @@ namespace Avalonia.FreeDesktop var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeFilter); if (filters.Any()) - { chooserOptions.Add("filters", filters); - } chooserOptions.Add("multiple", options.AllowMultiple); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = _desktopService.CreateRequest(objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + tsc.TrySetException(e); + else + tsc.TrySetResult(x.results["uris"] as string[]); + }); - return uris.Select(path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); + var uris = await tsc.Task ?? Array.Empty(); + return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); } public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) @@ -82,33 +74,33 @@ namespace Avalonia.FreeDesktop var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeChoices); if (filters.Any()) - { chooserOptions.Add("filters", filters); - } if (options.SuggestedFileName is { } currentName) chooserOptions.Add("current_name", currentName); if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true) chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); - objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); + var request = _desktopService.CreateRequest(objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + tsc.TrySetException(e); + else + tsc.TrySetResult(x.results["uris"] as string[]); + }); + var uris = await tsc.Task; var path = uris?.FirstOrDefault() is { } filePath ? new Uri(filePath).LocalPath : null; if (path is null) - { return null; - } - else - { - // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. - path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); - return new BclStorageFile(new FileInfo(path)); - } + // WSL2 freedesktop automatically adds extension from selected file type, but we can't pass "default ext". So apply it manually. + path = StorageProviderHelpers.NameWithExtension(path, options.DefaultExtension, null); + return new BclStorageFile(new FileInfo(path)); } public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) @@ -119,27 +111,31 @@ namespace Avalonia.FreeDesktop { "directory", true }, { "multiple", options.AllowMultiple } }; + var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = DBusHelper.Connection!.CreateProxy("org.freedesktop.portal.Request", objectPath); + var request = _desktopService.CreateRequest(objectPath); var tsc = new TaskCompletionSource(); - using var disposable = await request.WatchResponseAsync(x => tsc.SetResult(x.results["uris"] as string[]), tsc.SetException); - var uris = await tsc.Task ?? Array.Empty(); + using var disposable = await request.WatchResponseAsync((e, x) => + { + if (e is not null) + tsc.TrySetException(e); + else + tsc.TrySetResult(x.results["uris"] as string[]); + }); + var uris = await tsc.Task ?? Array.Empty(); return uris - .Select(path => new Uri(path).LocalPath) + .Select(static path => new Uri(path).LocalPath) // WSL2 freedesktop allows to select files as well in directory picker, filter it out. .Where(Directory.Exists) - .Select(path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); + .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); } private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList? fileTypes) { // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] - if (fileTypes is null) - { return Array.Empty<(string name, (uint style, string extension)[])>(); - } var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) @@ -150,18 +146,11 @@ namespace Avalonia.FreeDesktop var extensions = Enumerable.Empty<(uint, string)>(); if (fileType.Patterns is { } patterns) - { extensions = extensions.Concat(patterns.Select(static x => (globStyle, x))); - } else if (fileType.MimeTypes is { } mimeTypes) - { extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x))); - } - if (extensions.Any()) - { filters.Add((fileType.Name, extensions.ToArray())); - } } return filters.ToArray(); diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index e37067d05c..ed041cb2ca 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,16 +1,12 @@ -#nullable enable - -using System; +using System; +using System.Collections.Generic; using System.Diagnostics; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; +using System.Linq; using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; -using Tmds.DBus; - -[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] +using Tmds.DBus.Protocol; namespace Avalonia.FreeDesktop { @@ -20,11 +16,12 @@ namespace Avalonia.FreeDesktop private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private IDisposable? _serviceWatchDisposable; + private readonly DBus? _dBus; + private IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private IStatusNotifierWatcher? _statusNotifierWatcher; - private DbusPixmap _icon; + private StatusNotifierWatcher? _statusNotifierWatcher; + private (int, int, byte[]) _icon; private string? _sysTrayServiceName; private string? _tooltipText; @@ -51,6 +48,7 @@ namespace Avalonia.FreeDesktop IsActive = true; + _dBus = new DBusService(_connection, "org.freedesktop.DBus").CreateDBus("/org/freedesktop/DBus"); _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); @@ -60,47 +58,31 @@ namespace Avalonia.FreeDesktop private void InitializeSNWService() { - if (_connection is null || _isDisposed) return; - - try - { - _statusNotifierWatcher = _connection.CreateProxy( - "org.kde.StatusNotifierWatcher", - "/StatusNotifierWatcher"); - } - catch - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it."); - + if (_connection is null || _isDisposed) return; - } + _statusNotifierWatcher = new StatusNotifierWatcherService(_connection, "org.kde.StatusNotifierWatcher") + .CreateStatusNotifierWatcher("/StatusNotifierWatcher"); _serviceConnected = true; } private async void WatchAsync() { - try - { - _serviceWatchDisposable = - await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, - $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}"); - } + var services = await _connection!.ListServicesAsync(); + if (!services.Contains("org.kde.StatusNotifierWatcher", StringComparer.Ordinal)) + return; + + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((e, x) => { OnNameChange(x.A2); }); + var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); + OnNameChange(nameOwner); } - private void OnNameChange(ServiceOwnerChangedEventArgs obj) + private void OnNameChange(string? newOwner) { if (_isDisposed) return; - if (!_serviceConnected & obj.NewOwner != null) + if (!_serviceConnected & newOwner is not null) { _serviceConnected = true; InitializeSNWService(); @@ -108,55 +90,45 @@ namespace Avalonia.FreeDesktop DestroyTrayIcon(); if (_isVisible) - { CreateTrayIcon(); - } } - else if (_serviceConnected & obj.NewOwner is null) + else if (_serviceConnected & newOwner is null) { DestroyTrayIcon(); _serviceConnected = false; } } - private void CreateTrayIcon() + private async void CreateTrayIcon() { if (_connection is null || !_serviceConnected || _isDisposed) return; +#if NET5_0_OR_GREATER + var pid = Environment.ProcessId; +#else var pid = Process.GetCurrentProcess().Id; +#endif var tid = s_trayIconInstanceId++; _sysTrayServiceName = FormattableString.Invariant($"org.kde.StatusNotifierItem-{pid}-{tid}"); - _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath); - - try - { - _connection.RegisterObjectAsync(_statusNotifierItemDbusObj); - _connection.RegisterServiceAsync(_sysTrayServiceName); - _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "DBUS") - ?.Log(this, $"Error creating a DBus tray icon: {e}."); + _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_connection, _dbusMenuPath); - _serviceConnected = false; - } + _connection.AddMethodHandler(_statusNotifierItemDbusObj); + await _dBus!.RequestNameAsync(_sysTrayServiceName, 0); + await _statusNotifierWatcher!.RegisterStatusNotifierItemAsync(_sysTrayServiceName); _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText); _statusNotifierItemDbusObj.SetIcon(_icon); - _statusNotifierItemDbusObj.ActivationDelegate += OnClicked; } private void DestroyTrayIcon() { - if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null) + if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _connection.UnregisterObject(_statusNotifierItemDbusObj); - _connection.UnregisterServiceAsync(_sysTrayServiceName); + _dBus.ReleaseNameAsync(_sysTrayServiceName); } public void Dispose() @@ -175,13 +147,14 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap); + _statusNotifierItemDbusObj?.SetIcon((1, 1, new byte[] { 255, 0, 0, 0 })); return; } var x11iconData = IconConverterDelegate(icon); - if (x11iconData.Length == 0) return; + if (x11iconData.Length == 0) + return; var w = (int)x11iconData[0]; var h = (int)x11iconData[1]; @@ -199,7 +172,7 @@ namespace Avalonia.FreeDesktop pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF); } - _icon = new DbusPixmap(w, h, pixByteArray); + _icon = (w, h, pixByteArray); _statusNotifierItemDbusObj?.SetIcon(_icon); } @@ -237,111 +210,38 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : IStatusNotifierItem + internal class StatusNotifierItemDbusObj : IMethodHandler { + private readonly Connection _connection; private readonly StatusNotifierItemProperties _backingProperties; - public event Action? OnTitleChanged; - public event Action? OnIconChanged; - public event Action? OnAttentionIconChanged; - public event Action? OnOverlayIconChanged; - public event Action? OnTooltipChanged; - public Action? NewStatusAsync { get; set; } - public Action? ActivationDelegate { get; set; } - public ObjectPath ObjectPath { get; } - - public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath) - { - ObjectPath = new ObjectPath($"/StatusNotifierItem"); + public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) + { + _connection = connection; _backingProperties = new StatusNotifierItemProperties { - Menu = dbusmenuPath, // Needs a dbus menu somehow - ToolTip = new ToolTip("") + Menu = dbusMenuPath, // Needs a dbus menu somehow + ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty) }; InvalidateAll(); } - public Task ContextMenuAsync(int x, int y) => Task.CompletedTask; - - public Task ActivateAsync(int x, int y) - { - ActivationDelegate?.Invoke(); - return Task.CompletedTask; - } + public string Path => "/StatusNotifierItem"; - public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask; - - public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask; + public event Action? ActivationDelegate; public void InvalidateAll() { - OnTitleChanged?.Invoke(); - OnIconChanged?.Invoke(); - OnOverlayIconChanged?.Invoke(); - OnAttentionIconChanged?.Invoke(); - OnTooltipChanged?.Invoke(); - } - - public Task WatchNewTitleAsync(Action handler, Action onError) - { - OnTitleChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler)); - } - - public Task WatchNewIconAsync(Action handler, Action onError) - { - OnIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler)); - } - - public Task WatchNewAttentionIconAsync(Action handler, Action onError) - { - OnAttentionIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler)); + EmitVoidSignal("NewTitle"); + EmitVoidSignal("NewIcon"); + EmitVoidSignal("NewAttentionIcon"); + EmitVoidSignal("NewOverlayIcon"); + EmitVoidSignal("NewToolTip"); + EmitStringSignal("NewStatus", _backingProperties.Status ?? string.Empty); } - public Task WatchNewOverlayIconAsync(Action handler, Action onError) - { - OnOverlayIconChanged += handler; - return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler)); - } - - public Task WatchNewToolTipAsync(Action handler, Action onError) - { - OnTooltipChanged += handler; - return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler)); - } - - public Task WatchNewStatusAsync(Action handler, Action onError) - { - NewStatusAsync += handler; - return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler)); - } - - public Task GetAsync(string prop) - { - return Task.FromResult(prop switch - { - nameof(_backingProperties.Category) => _backingProperties.Category, - nameof(_backingProperties.Id) => _backingProperties.Id, - nameof(_backingProperties.Menu) => _backingProperties.Menu, - nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap, - nameof(_backingProperties.Status) => _backingProperties.Status, - nameof(_backingProperties.Title) => _backingProperties.Title, - nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip, - _ => null - }); - } - - public Task GetAllAsync() => Task.FromResult(_backingProperties); - - public Task SetAsync(string prop, object val) => Task.CompletedTask; - - public Task WatchPropertiesAsync(Action handler) => - Task.FromResult(Disposable.Empty); - - public void SetIcon(DbusPixmap dbusPixmap) + public void SetIcon((int, int, byte[]) dbusPixmap) { _backingProperties.IconPixmap = new[] { dbusPixmap }; InvalidateAll(); @@ -356,98 +256,153 @@ namespace Avalonia.FreeDesktop _backingProperties.Category = "ApplicationStatus"; _backingProperties.Status = text; _backingProperties.Title = text; - _backingProperties.ToolTip = new ToolTip(text); - + _backingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } - } - - [DBusInterface("org.kde.StatusNotifierWatcher")] - internal interface IStatusNotifierWatcher : IDBusObject - { - Task RegisterStatusNotifierItemAsync(string Service); - Task RegisterStatusNotifierHostAsync(string Service); - } - [DBusInterface("org.kde.StatusNotifierItem")] - internal interface IStatusNotifierItem : IDBusObject - { - Task ContextMenuAsync(int x, int y); - Task ActivateAsync(int x, int y); - Task SecondaryActivateAsync(int x, int y); - Task ScrollAsync(int delta, string orientation); - Task WatchNewTitleAsync(Action handler, Action onError); - Task WatchNewIconAsync(Action handler, Action onError); - Task WatchNewAttentionIconAsync(Action handler, Action onError); - Task WatchNewOverlayIconAsync(Action handler, Action onError); - Task WatchNewToolTipAsync(Action handler, Action onError); - Task WatchNewStatusAsync(Action handler, Action onError); - Task GetAsync(string prop); - Task GetAllAsync(); - Task SetAsync(string prop, object val); - Task WatchPropertiesAsync(Action handler); - } - - // This class is used by Tmds.Dbus to ferry properties - // from the SNI spec. - // Don't change this to actual C# properties since - // Tmds.Dbus will get confused. - [Dictionary] - internal class StatusNotifierItemProperties - { - public string? Category; - - public string? Id; - - public string? Title; + public bool RunMethodHandlerSynchronously(Message message) => false; - public string? Status; - - public ObjectPath Menu; - - public DbusPixmap[]? IconPixmap; + public ValueTask HandleMethodAsync(MethodContext context) + { + switch (context.Request.InterfaceAsString) + { + case "org.kde.StatusNotifierItem": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("ContextMenu", "ii"): + break; + case ("Activate", "ii"): + ActivationDelegate?.Invoke(); + break; + case ("SecondaryActivate", "ii"): + break; + case ("Scroll", "is"): + break; + } - public ToolTip ToolTip; - } + break; + case "org.freedesktop.DBus.Properties": + switch (context.Request.MemberAsString, context.Request.SignatureAsString) + { + case ("Get", "ss"): + { + var reader = context.Request.GetBodyReader(); + var interfaceName = reader.ReadString(); + var member = reader.ReadString(); + switch (member) + { + case "Category": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Category); + context.Reply(writer.CreateMessage()); + break; + } + case "Id": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Id); + context.Reply(writer.CreateMessage()); + break; + } + case "Title": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Title); + context.Reply(writer.CreateMessage()); + break; + } + case "Status": + { + using var writer = context.CreateReplyWriter("s"); + writer.WriteString(_backingProperties.Status); + context.Reply(writer.CreateMessage()); + break; + } + case "Menu": + { + using var writer = context.CreateReplyWriter("o"); + writer.WriteObjectPath(_backingProperties.Menu); + context.Reply(writer.CreateMessage()); + break; + } + case "IconPixmap": + { + using var writer = context.CreateReplyWriter("a(iiay)"); + writer.WriteArray(_backingProperties.IconPixmap); + context.Reply(writer.CreateMessage()); + break; + } + case "ToolTip": + { + using var writer = context.CreateReplyWriter("(sa(iiay)ss)"); + writer.WriteStruct(_backingProperties.ToolTip); + context.Reply(writer.CreateMessage()); + break; + } + } + + break; + } + case ("GetAll", "s"): + { + var writer = context.CreateReplyWriter("a{sv}"); + var dict = new Dictionary + { + { "Category", _backingProperties.Category ?? string.Empty }, + { "Id", _backingProperties.Id ?? string.Empty }, + { "Title", _backingProperties.Title ?? string.Empty }, + { "Status", _backingProperties.Status ?? string.Empty }, + { "Menu", _backingProperties.Menu }, + { "IconPixmap", _backingProperties.IconPixmap }, + { "ToolTip", _backingProperties.ToolTip } + }; + + writer.WriteDictionary(dict); + context.Reply(writer.CreateMessage()); + break; + } + } - internal struct ToolTip - { - public readonly string First; - public readonly DbusPixmap[] Second; - public readonly string Third; - public readonly string Fourth; + break; + } - private static readonly DbusPixmap[] s_blank = - { - new DbusPixmap(0, 0, Array.Empty()), new DbusPixmap(0, 0, Array.Empty()) - }; + return default; + } - public ToolTip(string message) : this("", s_blank, message, "") + private void EmitVoidSignal(string member) { + using var writer = _connection.GetMessageWriter(); + writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member); + _connection.TrySendMessage(writer.CreateMessage()); } - public ToolTip(string first, DbusPixmap[] second, string third, string fourth) + private void EmitStringSignal(string member, string value) { - First = first; - Second = second; - Third = third; - Fourth = fourth; + using var writer = _connection.GetMessageWriter(); + writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member, "s"); + writer.WriteString(value); + _connection.TrySendMessage(writer.CreateMessage()); } - } - internal readonly struct DbusPixmap - { - public readonly int Width; - public readonly int Height; - public readonly byte[] Data; - - public DbusPixmap(int width, int height, byte[] data) + private record StatusNotifierItemProperties { - Width = width; - Height = height; - Data = data; + public string? Category { get; set; } + public string? Id { get; set; } + public string? Title { get; set; } + public string? Status { get; set; } + public int WindowId { get; set; } + public string? IconThemePath { get; set; } + public ObjectPath Menu { get; set; } + public bool ItemIsMenu { get; set; } + public string? IconName { get; set; } + public (int, int, byte[])[]? IconPixmap { get; set; } + public string? OverlayIconName { get; set; } + public (int, int, byte[])[]? OverlayIconPixmap { get; set; } + public string? AttentionIconName { get; set; } + public (int, int, byte[])[]? AttentionIconPixmap { get; set; } + public string? AttentionMovieName { get; set; } + public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } } - - public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 }); } } diff --git a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs new file mode 100644 index 0000000000..a0ca8abc97 --- /dev/null +++ b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs @@ -0,0 +1,937 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop +{ + internal record NotificationProperties + { + public uint Version { get; set; } + } + + internal class Notification : DesktopObject + { + private const string Interface = "org.freedesktop.portal.Notification"; + + public Notification(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task AddNotificationAsync(string id, Dictionary notification) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sa{sv}", + member: "AddNotification"); + writer.WriteString(id); + writer.WriteDictionary(notification); + return writer.CreateMessage(); + } + } + + public Task RemoveNotificationAsync(string id) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RemoveNotification"); + writer.WriteString(id); + return writer.CreateMessage(); + } + } + + public ValueTask WatchActionInvokedAsync(Action handler, + bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", + (m, s) => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static NotificationProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static NotificationProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new NotificationProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal record OpenURIProperties + { + public uint Version { get; set; } + } + + internal class OpenURI : DesktopObject + { + private const string Interface = "org.freedesktop.portal.OpenURI"; + + public OpenURI(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task OpenUriAsync(string parentWindow, string uri, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "OpenURI"); + writer.WriteString(parentWindow); + writer.WriteString(uri); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task OpenFileAsync(string parentWindow, SafeHandle fd, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sha{sv}", + member: "OpenFile"); + writer.WriteString(parentWindow); + writer.WriteHandle(fd); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task OpenDirectoryAsync(string parentWindow, SafeHandle fd, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sha{sv}", + member: "OpenDirectory"); + writer.WriteString(parentWindow); + writer.WriteHandle(fd); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static OpenURIProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static OpenURIProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new OpenURIProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal record DynamicLauncherProperties + { + public uint SupportedLauncherTypes { get; set; } + public uint Version { get; set; } + } + + internal class DynamicLauncher : DesktopObject + { + private const string Interface = "org.freedesktop.portal.DynamicLauncher"; + + public DynamicLauncher(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task InstallAsync(string token, string desktopFileId, string desktopEntry, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sssa{sv}", + member: "Install"); + writer.WriteString(token); + writer.WriteString(desktopFileId); + writer.WriteString(desktopEntry); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task PrepareInstallAsync(string parentWindow, string name, object iconV, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssva{sv}", + member: "PrepareInstall"); + writer.WriteString(parentWindow); + writer.WriteString(name); + writer.WriteVariant(iconV); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task RequestInstallTokenAsync(string name, object iconV, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sva{sv}", + member: "RequestInstallToken"); + writer.WriteString(name); + writer.WriteVariant(iconV); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task UninstallAsync(string desktopFileId, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sa{sv}", + member: "Uninstall"); + writer.WriteString(desktopFileId); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task GetDesktopEntryAsync(string desktopFileId) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetDesktopEntry"); + writer.WriteString(desktopFileId); + return writer.CreateMessage(); + } + } + + public Task<(object IconV, string IconFormat, uint IconSize)> GetIconAsync(string desktopFileId) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_vsu(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "GetIcon"); + writer.WriteString(desktopFileId); + return writer.CreateMessage(); + } + } + + public Task LaunchAsync(string desktopFileId, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "sa{sv}", + member: "Launch"); + writer.WriteString(desktopFileId); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SetSupportedLauncherTypesAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("SupportedLauncherTypes"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetSupportedLauncherTypesAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static DynamicLauncherProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "SupportedLauncherTypes": + invalidated.Add("SupportedLauncherTypes"); + break; + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static DynamicLauncherProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new DynamicLauncherProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "SupportedLauncherTypes": + reader.ReadSignature("u"); + props.SupportedLauncherTypes = reader.ReadUInt32(); + changedList?.Add("SupportedLauncherTypes"); + break; + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal record FileChooserProperties + { + public uint Version { get; set; } + } + + internal class FileChooser : DesktopObject + { + private const string Interface = "org.freedesktop.portal.FileChooser"; + + public FileChooser(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task OpenFileAsync(string parentWindow, string title, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "OpenFile"); + writer.WriteString(parentWindow); + writer.WriteString(title); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SaveFileAsync(string parentWindow, string title, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "SaveFile"); + writer.WriteString(parentWindow); + writer.WriteString(title); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SaveFilesAsync(string parentWindow, string title, Dictionary options) + { + return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "ssa{sv}", + member: "SaveFiles"); + writer.WriteString(parentWindow); + writer.WriteString(title); + writer.WriteDictionary(options); + return writer.CreateMessage(); + } + } + + public Task SetVersionAsync(uint value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("version"); + writer.WriteSignature("u"); + writer.WriteUInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetVersionAsync() + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), + (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), + (m, s) => ReadMessage(m, (DesktopObject)s!), this); + + static FileChooserProperties ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, + bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new(); + var property = reader.ReadString(); + switch (property) + { + case "version": + invalidated.Add("Version"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static FileChooserProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new FileChooserProperties(); + ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "version": + reader.ReadSignature("u"); + props.Version = reader.ReadUInt32(); + changedList?.Add("Version"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal class Request : DesktopObject + { + private const string Interface = "org.freedesktop.portal.Request"; + + public Request(DesktopService service, ObjectPath path) : base(service, path) { } + + public Task CloseAsync() + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + "Close"); + return writer.CreateMessage(); + } + } + + public ValueTask WatchResponseAsync(Action results)> handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "Response", + static (m, s) => ReadMessage_uaesv(m, (DesktopObject)s!), handler, emitOnCapturedContext); + } + + internal class DesktopService + { + public DesktopService(Connection connection, string destination) + => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + public Notification CreateNotification(string path) => new(this, path); + public OpenURI CreateOpenUri(string path) => new(this, path); + public DynamicLauncher CreateDynamicLauncher(string path) => new(this, path); + public FileChooser CreateFileChooser(string path) => new(this, path); + public Request CreateRequest(string path) => new(this, path); + } + + internal class DesktopObject + { + protected DesktopObject(DesktopService service, ObjectPath path) + => (Service, Path) = (service, path); + + public DesktopService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, + MessageValueReader> reader, Action> handler, + bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, changes, rs, hs) => + ((Action>)hs!).Invoke(ex, changes), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + protected static ObjectPath ReadMessage_o(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadObjectPath(); + } + + protected static uint ReadMessage_v_u(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("u"); + return reader.ReadUInt32(); + } + + protected static (string, string, object[]) ReadMessage_ssav(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadString(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadArray(); + return (arg0, arg1, arg2); + } + + protected static (uint, Dictionary) ReadMessage_uaesv(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadUInt32(); + var arg1 = reader.ReadDictionary(); + return (arg0, arg1); + } + + protected static string ReadMessage_s(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static (object, string, uint) ReadMessage_vsu(Message message, DesktopObject _) + { + var reader = message.GetBodyReader(); + var arg0 = reader.ReadVariant(); + var arg1 = reader.ReadString(); + var arg2 = reader.ReadUInt32(); + return (arg0, arg1, arg2); + } + } + + internal class PropertyChanges + { + public PropertyChanges(TProperties properties, string[] invalidated, string[] changed) + => (Properties, Invalidated, Changed) = (properties, invalidated, changed); + + public TProperties Properties { get; } + public string[] Invalidated { get; } + public string[] Changed { get; } + public bool HasChanged(string property) => Array.IndexOf(Changed, property) != -1; + public bool IsInvalidated(string property) => Array.IndexOf(Invalidated, property) != -1; + } +} diff --git a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs new file mode 100644 index 0000000000..5cd44905af --- /dev/null +++ b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Tmds.DBus.Protocol; + +namespace Avalonia.FreeDesktop +{ + internal record StatusNotifierWatcherProperties + { + public string[] RegisteredStatusNotifierItems { get; set; } = default!; + public bool IsStatusNotifierHostRegistered { get; set; } + public int ProtocolVersion { get; set; } + } + + internal class StatusNotifierWatcher : StatusNotifierWatcherObject + { + private const string Interface = "org.kde.StatusNotifierWatcher"; + + public StatusNotifierWatcher(StatusNotifierWatcherService service, ObjectPath path) : base(service, path) { } + + public Task RegisterStatusNotifierItemAsync(string service) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RegisterStatusNotifierItem"); + writer.WriteString(service); + return writer.CreateMessage(); + } + } + + public Task RegisterStatusNotifierHostAsync(string service) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + Interface, + signature: "s", + member: "RegisterStatusNotifierHost"); + writer.WriteString(service); + return writer.CreateMessage(); + } + } + + public ValueTask WatchStatusNotifierItemRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemRegistered", static (m, s) => + ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchStatusNotifierItemUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) + => WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemUnregistered", static (m, s) + => ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + + public ValueTask WatchStatusNotifierHostRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostRegistered", handler, emitOnCapturedContext); + + public ValueTask WatchStatusNotifierHostUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) => + WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostUnregistered", handler, emitOnCapturedContext); + + public Task SetRegisteredStatusNotifierItemsAsync(string[] value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("RegisteredStatusNotifierItems"); + writer.WriteSignature("as"); + writer.WriteArray(value); + return writer.CreateMessage(); + } + } + + public Task SetIsStatusNotifierHostRegisteredAsync(bool value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("IsStatusNotifierHostRegistered"); + writer.WriteSignature("b"); + writer.WriteBool(value); + return writer.CreateMessage(); + } + } + + public Task SetProtocolVersionAsync(int value) + { + return Connection.CallMethodAsync(CreateMessage()); + + MessageBuffer CreateMessage() + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ssv", + member: "Set"); + writer.WriteString(Interface); + writer.WriteString("ProtocolVersion"); + writer.WriteSignature("i"); + writer.WriteInt32(value); + return writer.CreateMessage(); + } + } + + public Task GetRegisteredStatusNotifierItemsAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "RegisteredStatusNotifierItems"), static (m, s) + => ReadMessage_v_as(m, (StatusNotifierWatcherObject)s!), this); + + public Task GetIsStatusNotifierHostRegisteredAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IsStatusNotifierHostRegistered"), static (m, s) => + ReadMessage_v_b(m, (StatusNotifierWatcherObject)s!), this); + + public Task GetProtocolVersionAsync() => + Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "ProtocolVersion"), static (m, s) + => ReadMessage_v_i(m, (StatusNotifierWatcherObject)s!), this); + + public Task GetPropertiesAsync() + { + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) + => ReadMessage(m, (StatusNotifierWatcherObject)s!), this); + + static StatusNotifierWatcherProperties ReadMessage(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + return ReadProperties(ref reader); + } + } + + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) + { + return base.WatchPropertiesChangedAsync(Interface, static (m, s) => ReadMessage(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + + static PropertyChanges ReadMessage(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadString(); // interface + List changed = new(); + return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), + ReadInvalidated(ref reader)); + } + + static string[] ReadInvalidated(ref Reader reader) + { + List? invalidated = null; + var headersEnd = reader.ReadArrayStart(DBusType.String); + while (reader.HasNext(headersEnd)) + { + invalidated ??= new List(); + var property = reader.ReadString(); + switch (property) + { + case "RegisteredStatusNotifierItems": + invalidated.Add("RegisteredStatusNotifierItems"); + break; + case "IsStatusNotifierHostRegistered": + invalidated.Add("IsStatusNotifierHostRegistered"); + break; + case "ProtocolVersion": + invalidated.Add("ProtocolVersion"); + break; + } + } + + return invalidated?.ToArray() ?? Array.Empty(); + } + } + + private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, List? changedList = null) + { + var props = new StatusNotifierWatcherProperties(); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); + while (reader.HasNext(headersEnd)) + { + var property = reader.ReadString(); + switch (property) + { + case "RegisteredStatusNotifierItems": + reader.ReadSignature("as"); + props.RegisteredStatusNotifierItems = reader.ReadArray(); + changedList?.Add("RegisteredStatusNotifierItems"); + break; + case "IsStatusNotifierHostRegistered": + reader.ReadSignature("b"); + props.IsStatusNotifierHostRegistered = reader.ReadBool(); + changedList?.Add("IsStatusNotifierHostRegistered"); + break; + case "ProtocolVersion": + reader.ReadSignature("i"); + props.ProtocolVersion = reader.ReadInt32(); + changedList?.Add("ProtocolVersion"); + break; + default: + reader.ReadVariant(); + break; + } + } + + return props; + } + } + + internal class StatusNotifierWatcherService + { + public StatusNotifierWatcherService(Connection connection, string destination) => (Connection, Destination) = (connection, destination); + + public Connection Connection { get; } + public string Destination { get; } + + public StatusNotifierWatcher CreateStatusNotifierWatcher(string path) => new(this, path); + } + + internal class StatusNotifierWatcherObject + { + protected StatusNotifierWatcherObject(StatusNotifierWatcherService service, ObjectPath path) + { + (Service, Path) = (service, path); + } + + public StatusNotifierWatcherService Service { get; } + public ObjectPath Path { get; } + protected Connection Connection => Service.Connection; + + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "ss", + member: "Get"); + writer.WriteString(@interface); + writer.WriteString(property); + return writer.CreateMessage(); + } + + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) + { + using var writer = Connection.GetMessageWriter(); + writer.WriteMethodCallHeader( + Service.Destination, + Path, + "org.freedesktop.DBus.Properties", + signature: "s", + member: "GetAll"); + writer.WriteString(@interface); + return writer.CreateMessage(); + } + + protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = Service.Destination, + Path = Path, + Interface = "org.freedesktop.DBus.Properties", + Member = "PropertiesChanged", + Arg0 = @interface + }; + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => + ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, reader, + (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), + this, handler, emitOnCapturedContext); + } + + public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + { + var rule = new MatchRule + { + Type = MessageType.Signal, + Sender = sender, + Path = path, + Member = signal, + Interface = @interface + }; + return Connection.AddMatchAsync(rule, static (_, _) + => null!, static (Exception? ex, object _, object? _, object? hs) + => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + } + + protected static string ReadMessage_s(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + return reader.ReadString(); + } + + protected static string[] ReadMessage_v_as(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("as"); + return reader.ReadArray(); + } + + protected static bool ReadMessage_v_b(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("b"); + return reader.ReadBool(); + } + + protected static int ReadMessage_v_i(Message message, StatusNotifierWatcherObject _) + { + var reader = message.GetBodyReader(); + reader.ReadSignature("i"); + return reader.ReadInt32(); + } + } +} diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 810a806c8a..e0d6cf3e55 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -212,10 +212,10 @@ namespace Avalonia.X11 _x11.Atoms.XA_CARDINAL, 32, PropertyMode.Replace, ref _xSyncCounter, 1); } - StorageProvider = new CompositeStorageProvider(new Func>[] + StorageProvider = new CompositeStorageProvider(new[] { - () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreate(Handle) : Task.FromResult(null), - () => GtkSystemDialog.TryCreate(this), + () => _platform.Options.UseDBusFilePicker ? DBusSystemDialog.TryCreateAsync(Handle) : Task.FromResult(null), + () => GtkSystemDialog.TryCreate(this) }); } diff --git a/src/Linux/Tmds.DBus b/src/Linux/Tmds.DBus new file mode 160000 index 0000000000..bfca94ab05 --- /dev/null +++ b/src/Linux/Tmds.DBus @@ -0,0 +1 @@ +Subproject commit bfca94ab052683c7ccef51e4a036098f539cc676 From d5df9298bd2642492654b56505180da89c4faf6e Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 30 Dec 2022 14:34:10 +0100 Subject: [PATCH 004/326] Fix warnings --- .../AppMenuRegistrar.DBus.cs | 25 ++- src/Avalonia.FreeDesktop/DBus.DBus.cs | 14 +- src/Avalonia.FreeDesktop/DBusCallQueue.cs | 13 +- .../DBusIme/Fcitx/FcitxEnums.cs | 64 ++++---- .../DBusIme/IBus/IBusEnums.cs | 2 +- src/Avalonia.FreeDesktop/DBusMenu.DBus.cs | 131 ++++++++++------ src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 3 +- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 18 +-- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 32 ++-- .../FreeDesktopPortalDesktop.DBus.cs | 143 +++++++++--------- src/Avalonia.FreeDesktop/IX11InputMethod.cs | 4 +- src/Avalonia.FreeDesktop/NativeMethods.cs | 8 +- .../StatusNotifierWatcher.DBus.cs | 25 ++- 13 files changed, 258 insertions(+), 224 deletions(-) diff --git a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs index a45d1642fc..75c8ce61a5 100644 --- a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs +++ b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs @@ -1,9 +1,7 @@ using System; using System.Threading.Tasks; - using Tmds.DBus.Protocol; - namespace Avalonia.FreeDesktop { internal class Registrar : AppMenuRegistrarObject @@ -52,7 +50,7 @@ namespace Avalonia.FreeDesktop public Task<(string Service, ObjectPath MenuObjectPath)> GetMenuForWindowAsync(uint windowId) { - return Connection.CallMethodAsync(CreateMessage(), (Message m, object? s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); MessageBuffer CreateMessage() { @@ -76,7 +74,7 @@ namespace Avalonia.FreeDesktop public Connection Connection { get; } public string Destination { get; } - public Registrar CreateRegistrar(string path) => new Registrar(this, path); + public Registrar CreateRegistrar(string path) => new(this, path); } internal class AppMenuRegistrarObject @@ -84,8 +82,8 @@ namespace Avalonia.FreeDesktop protected AppMenuRegistrarObject(RegistrarService service, ObjectPath path) => (Service, Path) = (service, path); - public RegistrarService Service { get; } - public ObjectPath Path { get; } + protected RegistrarService Service { get; } + protected ObjectPath Path { get; } protected Connection Connection => Service.Connection; protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) @@ -128,9 +126,8 @@ namespace Avalonia.FreeDesktop Member = "PropertiesChanged", Arg0 = @interface }; - return Connection.AddMatchAsync(rule, reader, - (Exception? ex, PropertyChanges changes, object? rs, object? hs) => - ((Action>)hs!).Invoke(ex, changes), + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) + => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } @@ -145,9 +142,8 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (Exception? ex, TArg arg, object? rs, object? hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, @@ -161,8 +157,9 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, (Message message, object? state) => null!, - (Exception? ex, object v, object? rs, object? hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, static (_, _) + => null!, static (ex, _, _, hs) + => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); } protected static (string, ObjectPath) ReadMessage_so(Message message, AppMenuRegistrarObject _) diff --git a/src/Avalonia.FreeDesktop/DBus.DBus.cs b/src/Avalonia.FreeDesktop/DBus.DBus.cs index 4cf2b76b45..0f83ddaa4a 100644 --- a/src/Avalonia.FreeDesktop/DBus.DBus.cs +++ b/src/Avalonia.FreeDesktop/DBus.DBus.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; - using Tmds.DBus.Protocol; - namespace Avalonia.FreeDesktop { internal record DBusProperties @@ -417,10 +415,10 @@ namespace Avalonia.FreeDesktop public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) => - ReadMessage(m, (DBusObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => + ReadMessage(m), this); - static DBusProperties ReadMessage(Message message, DBusObject _) + static DBusProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -429,10 +427,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, static (m, s) => - ReadMessage(m, (DBusObject)s!), handler, emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) => + ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DBusObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface diff --git a/src/Avalonia.FreeDesktop/DBusCallQueue.cs b/src/Avalonia.FreeDesktop/DBusCallQueue.cs index e7c07dcbf9..b853626d45 100644 --- a/src/Avalonia.FreeDesktop/DBusCallQueue.cs +++ b/src/Avalonia.FreeDesktop/DBusCallQueue.cs @@ -7,19 +7,20 @@ namespace Avalonia.FreeDesktop class DBusCallQueue { private readonly Func _errorHandler; + private readonly Queue _q = new(); - record Item(Func Callback) + private bool _processing; + + private record Item(Func Callback) { public Action? OnFinish; } - private Queue _q = new Queue(); - private bool _processing; public DBusCallQueue(Func errorHandler) { _errorHandler = errorHandler; } - + public void Enqueue(Func cb) { _q.Enqueue(new Item(cb)); @@ -42,7 +43,7 @@ namespace Avalonia.FreeDesktop Process(); return tcs.Task; } - + public Task EnqueueAsync(Func> cb) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -62,7 +63,7 @@ namespace Avalonia.FreeDesktop return tcs.Task; } - async void Process() + private async void Process() { if(_processing) return; diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs index 6510a5877a..fafc66b6b9 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxEnums.cs @@ -2,45 +2,45 @@ using System; namespace Avalonia.FreeDesktop.DBusIme.Fcitx { - enum FcitxKeyEventType + internal enum FcitxKeyEventType { FCITX_PRESS_KEY, FCITX_RELEASE_KEY - }; - + } + [Flags] - enum FcitxCapabilityFlags + internal enum FcitxCapabilityFlags { CAPACITY_NONE = 0, - CAPACITY_CLIENT_SIDE_UI = (1 << 0), - CAPACITY_PREEDIT = (1 << 1), - CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2), - CAPACITY_PASSWORD = (1 << 3), - CAPACITY_FORMATTED_PREEDIT = (1 << 4), - CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5), - CAPACITY_SURROUNDING_TEXT = (1 << 6), - CAPACITY_EMAIL = (1 << 7), - CAPACITY_DIGIT = (1 << 8), - CAPACITY_UPPERCASE = (1 << 9), - CAPACITY_LOWERCASE = (1 << 10), - CAPACITY_NOAUTOUPPERCASE = (1 << 11), - CAPACITY_URL = (1 << 12), - CAPACITY_DIALABLE = (1 << 13), - CAPACITY_NUMBER = (1 << 14), - CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15), - CAPACITY_SPELLCHECK = (1 << 16), - CAPACITY_NO_SPELLCHECK = (1 << 17), - CAPACITY_WORD_COMPLETION = (1 << 18), - CAPACITY_UPPERCASE_WORDS = (1 << 19), - CAPACITY_UPPERCASE_SENTENCES = (1 << 20), - CAPACITY_ALPHA = (1 << 21), - CAPACITY_NAME = (1 << 22), - CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23), - CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24), - }; + CAPACITY_CLIENT_SIDE_UI = 1 << 0, + CAPACITY_PREEDIT = 1 << 1, + CAPACITY_CLIENT_SIDE_CONTROL_STATE = 1 << 2, + CAPACITY_PASSWORD = 1 << 3, + CAPACITY_FORMATTED_PREEDIT = 1 << 4, + CAPACITY_CLIENT_UNFOCUS_COMMIT = 1 << 5, + CAPACITY_SURROUNDING_TEXT = 1 << 6, + CAPACITY_EMAIL = 1 << 7, + CAPACITY_DIGIT = 1 << 8, + CAPACITY_UPPERCASE = 1 << 9, + CAPACITY_LOWERCASE = 1 << 10, + CAPACITY_NOAUTOUPPERCASE = 1 << 11, + CAPACITY_URL = 1 << 12, + CAPACITY_DIALABLE = 1 << 13, + CAPACITY_NUMBER = 1 << 14, + CAPACITY_NO_ON_SCREEN_KEYBOARD = 1 << 15, + CAPACITY_SPELLCHECK = 1 << 16, + CAPACITY_NO_SPELLCHECK = 1 << 17, + CAPACITY_WORD_COMPLETION = 1 << 18, + CAPACITY_UPPERCASE_WORDS = 1 << 19, + CAPACITY_UPPERCASE_SENTENCES = 1 << 20, + CAPACITY_ALPHA = 1 << 21, + CAPACITY_NAME = 1 << 22, + CAPACITY_GET_IM_INFO_ON_FOCUS = 1 << 23, + CAPACITY_RELATIVE_CURSOR_RECT = 1 << 24 + } [Flags] - enum FcitxKeyState + internal enum FcitxKeyState { FcitxKeyState_None = 0, FcitxKeyState_Shift = 1 << 0, @@ -63,5 +63,5 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx FcitxKeyState_Hyper = 1 << 27, FcitxKeyState_Meta = 1 << 28, FcitxKeyState_UsedMask = 0x5c001fff - }; + } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs index b8ac816694..1b430f9c90 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusEnums.cs @@ -40,6 +40,6 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus CapLookupTable = 1 << 2, CapFocus = 1 << 3, CapProperty = 1 << 4, - CapSurroundingText = 1 << 5, + CapSurroundingText = 1 << 5 } } diff --git a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs index d9016d8644..e3928cfbb2 100644 --- a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs +++ b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs @@ -16,11 +16,12 @@ namespace Avalonia.FreeDesktop internal class DBusMenu : DBusMenuObject { private const string Interface = "com.canonical.dbusmenu"; - public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) - { } + + public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) { } + public Task<(uint Revision, (int, Dictionary, object[]) Layout)> GetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_uriaesvavz(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_uriaesvavz(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -36,9 +37,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task<(int, Dictionary)[]> GetGroupPropertiesAsync(int[] ids, string[] propertyNames) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ariaesvz(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ariaesvz(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -53,9 +55,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task GetPropertyAsync(int id, string name) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_v(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_v(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -70,6 +73,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task EventAsync(int id, string eventId, object data, uint timestamp) { return Connection.CallMethodAsync(CreateMessage()); @@ -89,9 +93,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task EventGroupAsync((int, string, object, uint)[] events) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_ai(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ai(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -105,9 +110,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task AboutToShowAsync(int id) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_b(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_b(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -121,9 +127,10 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task<(int[] UpdatesNeeded, int[] IdErrors)> AboutToShowGroupAsync(int[] ids) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_aiai(m, (DBusMenuObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_aiai(m), this); MessageBuffer CreateMessage() { using var writer = Connection.GetMessageWriter(); @@ -137,12 +144,16 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public ValueTask WatchItemsPropertiesUpdatedAsync(Action)[] UpdatedProps, (int, string[])[] RemovedProps)> handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", (m, s) => ReadMessage_ariaesvzariasz(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", static (m, _) => ReadMessage_ariaesvzariasz(m), handler, emitOnCapturedContext); + public ValueTask WatchLayoutUpdatedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", (m, s) => ReadMessage_ui(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", static (m, _) => ReadMessage_ui(m), handler, emitOnCapturedContext); + public ValueTask WatchItemActivationRequestedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", (m, s) => ReadMessage_iu(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", static (m, _) => ReadMessage_iu(m), handler, emitOnCapturedContext); + public Task SetVersionAsync(uint value) { return Connection.CallMethodAsync(CreateMessage()); @@ -162,6 +173,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task SetTextDirectionAsync(string value) { return Connection.CallMethodAsync(CreateMessage()); @@ -181,6 +193,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task SetStatusAsync(string value) { return Connection.CallMethodAsync(CreateMessage()); @@ -200,6 +213,7 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task SetIconThemePathAsync(string[] value) { return Connection.CallMethodAsync(CreateMessage()); @@ -219,40 +233,46 @@ namespace Avalonia.FreeDesktop return writer.CreateMessage(); } } + public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), (m, s) => ReadMessage_v_u(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), static (m, _) => ReadMessage_v_u(m), this); + public Task GetTextDirectionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), static (m, _) => ReadMessage_v_s(m), this); + public Task GetStatusAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), (m, s) => ReadMessage_v_s(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), static (m, _) => ReadMessage_v_s(m), this); + public Task GetIconThemePathAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), (m, s) => ReadMessage_v_as(m, (DBusMenuObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), static (m, _) => ReadMessage_v_as(m), this); + public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), (m, s) => ReadMessage(m, (DBusMenuObject)s!), this); - static DBusMenuProperties ReadMessage(Message message, DBusMenuObject _) + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => ReadMessage(m), this); + static DBusMenuProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); } } + public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DBusMenuObject)s!), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DBusMenuObject _) + return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface - List changed = new(), invalidated = new(); + List changed = new(); return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); } static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -265,10 +285,11 @@ namespace Avalonia.FreeDesktop return invalidated?.ToArray() ?? Array.Empty(); } } - private static DBusMenuProperties ReadProperties(ref Reader reader, List? changedList = null) + + private static DBusMenuProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new DBusMenuProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -309,7 +330,7 @@ namespace Avalonia.FreeDesktop public string Destination { get; } public DBusMenuService(Connection connection, string destination) => (Connection, Destination) = (connection, destination); - public DBusMenu CreateDbusmenu(string path) => new DBusMenu(this, path); + public DBusMenu CreateDbusmenu(string path) => new(this, path); } internal class DBusMenuObject @@ -319,6 +340,7 @@ namespace Avalonia.FreeDesktop protected Connection Connection => Service.Connection; protected DBusMenuObject(DBusMenuService service, ObjectPath path) => (Service, Path) = (service, path); + protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) { using var writer = Connection.GetMessageWriter(); @@ -332,6 +354,7 @@ namespace Avalonia.FreeDesktop writer.WriteString(property); return writer.CreateMessage(); } + protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) { using var writer = Connection.GetMessageWriter(); @@ -344,6 +367,7 @@ namespace Avalonia.FreeDesktop writer.WriteString(@interface); return writer.CreateMessage(); } + protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) { var rule = new MatchRule @@ -355,11 +379,11 @@ namespace Avalonia.FreeDesktop Member = "PropertiesChanged", Arg0 = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, changes, rs, hs) => ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) + => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { @@ -369,11 +393,11 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) + => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { @@ -383,77 +407,90 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, (message, state) => null!, - (ex, v, rs, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, static (_, _) + => null!, static (ex, _, _, hs) + => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); } - protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message, DBusMenuObject _) + + protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadUInt32(); var arg1 = reader.ReadStruct, object[]>(); return (arg0, arg1); } - protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message, DBusMenuObject _) + + protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message) { var reader = message.GetBodyReader(); return reader.ReadArray<(int, Dictionary)>(); } - protected static object ReadMessage_v(Message message, DBusMenuObject _) + + protected static object ReadMessage_v(Message message) { var reader = message.GetBodyReader(); return reader.ReadVariant(); } - protected static int[] ReadMessage_ai(Message message, DBusMenuObject _) + + protected static int[] ReadMessage_ai(Message message) { var reader = message.GetBodyReader(); return reader.ReadArray(); } - protected static bool ReadMessage_b(Message message, DBusMenuObject _) + + protected static bool ReadMessage_b(Message message) { var reader = message.GetBodyReader(); return reader.ReadBool(); } - protected static (int[], int[]) ReadMessage_aiai(Message message, DBusMenuObject _) + + protected static (int[], int[]) ReadMessage_aiai(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadArray(); var arg1 = reader.ReadArray(); return (arg0, arg1); } - protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message, DBusMenuObject _) + + protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadArray<(int, Dictionary)>(); var arg1 = reader.ReadArray<(int, string[])>(); return (arg0, arg1); } - protected static (uint, int) ReadMessage_ui(Message message, DBusMenuObject _) + + protected static (uint, int) ReadMessage_ui(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadUInt32(); var arg1 = reader.ReadInt32(); return (arg0, arg1); } - protected static (int, uint) ReadMessage_iu(Message message, DBusMenuObject _) + + protected static (int, uint) ReadMessage_iu(Message message) { var reader = message.GetBodyReader(); var arg0 = reader.ReadInt32(); var arg1 = reader.ReadUInt32(); return (arg0, arg1); } - protected static uint ReadMessage_v_u(Message message, DBusMenuObject _) + + protected static uint ReadMessage_v_u(Message message) { var reader = message.GetBodyReader(); reader.ReadSignature("u"); return reader.ReadUInt32(); } - protected static string ReadMessage_v_s(Message message, DBusMenuObject _) + + protected static string ReadMessage_v_s(Message message) { var reader = message.GetBodyReader(); reader.ReadSignature("s"); return reader.ReadString(); } - protected static string[] ReadMessage_v_as(Message message, DBusMenuObject _) + + protected static string[] ReadMessage_v_as(Message message) { var reader = message.GetBodyReader(); reader.ReadSignature("as"); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 682434bc8d..69e2eed72f 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -96,8 +96,7 @@ namespace Avalonia.FreeDesktop public void SetNativeMenu(NativeMenu? menu) { - if (menu is null) - menu = new NativeMenu(); + menu ??= new NativeMenu(); if (_menu is not null) ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index d59af3b5dd..7d1a0bbfc4 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -18,9 +18,9 @@ namespace Avalonia.FreeDesktop if (DBusHelper.Connection is null) return null; var services = await DBusHelper.Connection.ListServicesAsync(); - if (!services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal)) - return null; - return new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle); + return services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal) + ? new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle) + : null; } private readonly DesktopService _desktopService; @@ -140,15 +140,15 @@ namespace Avalonia.FreeDesktop var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) { - const uint globStyle = 0u; - const uint mimeStyle = 1u; + const uint GlobStyle = 0u; + const uint MimeStyle = 1u; var extensions = Enumerable.Empty<(uint, string)>(); - if (fileType.Patterns is { } patterns) - extensions = extensions.Concat(patterns.Select(static x => (globStyle, x))); - else if (fileType.MimeTypes is { } mimeTypes) - extensions = extensions.Concat(mimeTypes.Select(static x => (mimeStyle, x))); + if (fileType.Patterns is not null) + extensions = extensions.Concat(fileType.Patterns.Select(static x => (globStyle: GlobStyle, x))); + else if (fileType.MimeTypes is not null) + extensions = extensions.Concat(fileType.MimeTypes.Select(static x => (mimeStyle: MimeStyle, x))); if (extensions.Any()) filters.Add((fileType.Name, extensions.ToArray())); } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index ed041cb2ca..d6518f7246 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -72,7 +72,7 @@ namespace Avalonia.FreeDesktop if (!services.Contains("org.kde.StatusNotifierWatcher", StringComparer.Ordinal)) return; - _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((e, x) => { OnNameChange(x.A2); }); + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.A2) ); var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); OnNameChange(nameOwner); } @@ -128,7 +128,7 @@ namespace Avalonia.FreeDesktop if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null || _sysTrayServiceName is null) return; - _dBus.ReleaseNameAsync(_sysTrayServiceName); + _dBus!.ReleaseNameAsync(_sysTrayServiceName); } public void Dispose() @@ -147,7 +147,7 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon((1, 1, new byte[] { 255, 0, 0, 0 })); + _statusNotifierItemDbusObj?.SetIcon(StatusNotifierItemDbusObj.StatusNotifierItemProperties.EmptyPixmap); return; } @@ -238,7 +238,7 @@ namespace Avalonia.FreeDesktop EmitVoidSignal("NewAttentionIcon"); EmitVoidSignal("NewOverlayIcon"); EmitVoidSignal("NewToolTip"); - EmitStringSignal("NewStatus", _backingProperties.Status ?? string.Empty); + EmitStringSignal("NewStatus", _backingProperties.Status); } public void SetIcon((int, int, byte[]) dbusPixmap) @@ -287,7 +287,7 @@ namespace Avalonia.FreeDesktop case ("Get", "ss"): { var reader = context.Request.GetBodyReader(); - var interfaceName = reader.ReadString(); + _ = reader.ReadString(); var member = reader.ReadString(); switch (member) { @@ -349,10 +349,10 @@ namespace Avalonia.FreeDesktop var writer = context.CreateReplyWriter("a{sv}"); var dict = new Dictionary { - { "Category", _backingProperties.Category ?? string.Empty }, - { "Id", _backingProperties.Id ?? string.Empty }, - { "Title", _backingProperties.Title ?? string.Empty }, - { "Status", _backingProperties.Status ?? string.Empty }, + { "Category", _backingProperties.Category }, + { "Id", _backingProperties.Id }, + { "Title", _backingProperties.Title }, + { "Status", _backingProperties.Status }, { "Menu", _backingProperties.Menu }, { "IconPixmap", _backingProperties.IconPixmap }, { "ToolTip", _backingProperties.ToolTip } @@ -385,24 +385,26 @@ namespace Avalonia.FreeDesktop _connection.TrySendMessage(writer.CreateMessage()); } - private record StatusNotifierItemProperties + internal record StatusNotifierItemProperties { - public string? Category { get; set; } - public string? Id { get; set; } - public string? Title { get; set; } - public string? Status { get; set; } + public string Category { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; public int WindowId { get; set; } public string? IconThemePath { get; set; } public ObjectPath Menu { get; set; } public bool ItemIsMenu { get; set; } public string? IconName { get; set; } - public (int, int, byte[])[]? IconPixmap { get; set; } + public (int, int, byte[])[] IconPixmap { get; set; } = { EmptyPixmap }; public string? OverlayIconName { get; set; } public (int, int, byte[])[]? OverlayIconPixmap { get; set; } public string? AttentionIconName { get; set; } public (int, int, byte[])[]? AttentionIconPixmap { get; set; } public string? AttentionMovieName { get; set; } public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } + + public static (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); } } } diff --git a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs index a0ca8abc97..23f1dcf0e7 100644 --- a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs +++ b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs @@ -56,8 +56,8 @@ namespace Avalonia.FreeDesktop public ValueTask WatchActionInvokedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", - (m, s) => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); + => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", static (m, s) + => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); public Task SetVersionAsync(uint value) { @@ -81,15 +81,15 @@ namespace Avalonia.FreeDesktop } public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static NotificationProperties ReadMessage(Message message, DesktopObject _) + static NotificationProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -99,10 +99,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, - emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -114,10 +114,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -131,10 +131,10 @@ namespace Avalonia.FreeDesktop } } - private static NotificationProperties ReadProperties(ref Reader reader, List? changedList = null) + private static NotificationProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new NotificationProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -168,7 +168,7 @@ namespace Avalonia.FreeDesktop public Task OpenUriAsync(string parentWindow, string uri, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -188,7 +188,7 @@ namespace Avalonia.FreeDesktop public Task OpenFileAsync(string parentWindow, SafeHandle fd, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -208,7 +208,7 @@ namespace Avalonia.FreeDesktop public Task OpenDirectoryAsync(string parentWindow, SafeHandle fd, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -248,15 +248,15 @@ namespace Avalonia.FreeDesktop } public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static OpenURIProperties ReadMessage(Message message, DesktopObject _) + static OpenURIProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -266,10 +266,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, - emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -280,10 +280,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -297,10 +297,10 @@ namespace Avalonia.FreeDesktop } } - private static OpenURIProperties ReadProperties(ref Reader reader, List? changedList = null) + private static OpenURIProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new OpenURIProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -356,7 +356,7 @@ namespace Avalonia.FreeDesktop public Task PrepareInstallAsync(string parentWindow, string name, object iconV, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -377,7 +377,8 @@ namespace Avalonia.FreeDesktop public Task RequestInstallTokenAsync(string name, object iconV, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) + => ReadMessage_s(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -416,7 +417,8 @@ namespace Avalonia.FreeDesktop public Task GetDesktopEntryAsync(string desktopFileId) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_s(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) + => ReadMessage_s(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -434,7 +436,8 @@ namespace Avalonia.FreeDesktop public Task<(object IconV, string IconFormat, uint IconSize)> GetIconAsync(string desktopFileId) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_vsu(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) + => ReadMessage_vsu(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -512,19 +515,19 @@ namespace Avalonia.FreeDesktop } public Task GetSupportedLauncherTypesAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static DynamicLauncherProperties ReadMessage(Message message, DesktopObject _) + static DynamicLauncherProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -534,10 +537,11 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -549,10 +553,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -569,10 +573,10 @@ namespace Avalonia.FreeDesktop } } - private static DynamicLauncherProperties ReadProperties(ref Reader reader, List? changedList = null) + private static DynamicLauncherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new DynamicLauncherProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -611,7 +615,7 @@ namespace Avalonia.FreeDesktop public Task OpenFileAsync(string parentWindow, string title, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -631,7 +635,7 @@ namespace Avalonia.FreeDesktop public Task SaveFileAsync(string parentWindow, string title, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -651,7 +655,7 @@ namespace Avalonia.FreeDesktop public Task SaveFilesAsync(string parentWindow, string title, Dictionary options) { - return Connection.CallMethodAsync(CreateMessage(), (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); MessageBuffer CreateMessage() { @@ -691,15 +695,15 @@ namespace Avalonia.FreeDesktop } public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), - (m, s) => ReadMessage_v_u(m, (DesktopObject)s!), this); + => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) + => ReadMessage_v_u(m, (DesktopObject)s!), this); public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), - (m, s) => ReadMessage(m, (DesktopObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static FileChooserProperties ReadMessage(Message message, DesktopObject _) + static FileChooserProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -709,10 +713,10 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, (m, s) => ReadMessage(m, (DesktopObject)s!), handler, - emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) + => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, DesktopObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -724,10 +728,10 @@ namespace Avalonia.FreeDesktop static string[] ReadInvalidated(ref Reader reader) { List? invalidated = null; - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.String); + var headersEnd = reader.ReadArrayStart(DBusType.String); while (reader.HasNext(headersEnd)) { - invalidated ??= new(); + invalidated ??= new List(); var property = reader.ReadString(); switch (property) { @@ -741,10 +745,10 @@ namespace Avalonia.FreeDesktop } } - private static FileChooserProperties ReadProperties(ref Reader reader, List? changedList = null) + private static FileChooserProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new FileChooserProperties(); - ArrayEnd headersEnd = reader.ReadArrayStart(DBusType.Struct); + var headersEnd = reader.ReadArrayStart(DBusType.Struct); while (reader.HasNext(headersEnd)) { var property = reader.ReadString(); @@ -811,8 +815,8 @@ namespace Avalonia.FreeDesktop protected DesktopObject(DesktopService service, ObjectPath path) => (Service, Path) = (service, path); - public DesktopService Service { get; } - public ObjectPath Path { get; } + protected DesktopService Service { get; } + protected ObjectPath Path { get; } protected Connection Connection => Service.Connection; protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) @@ -855,13 +859,11 @@ namespace Avalonia.FreeDesktop Member = "PropertiesChanged", Arg0 = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, changes, rs, hs) => - ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) + => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule @@ -872,9 +874,8 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, arg, rs, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => + ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } protected static ObjectPath ReadMessage_o(Message message, DesktopObject _) diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs index 9fa9c1809e..6ae63ea322 100644 --- a/src/Avalonia.FreeDesktop/IX11InputMethod.cs +++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs @@ -19,7 +19,7 @@ namespace Avalonia.FreeDesktop public KeyModifiers Modifiers { get; set; } public RawKeyEventType Type { get; set; } } - + public interface IX11InputMethodControl : IDisposable { void SetWindowActive(bool active); @@ -27,7 +27,7 @@ namespace Avalonia.FreeDesktop ValueTask HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode); event Action Commit; event Action ForwardKey; - + void UpdateWindowInfo(PixelPoint position, double scaling); } } diff --git a/src/Avalonia.FreeDesktop/NativeMethods.cs b/src/Avalonia.FreeDesktop/NativeMethods.cs index 147955b6a3..df22f46323 100644 --- a/src/Avalonia.FreeDesktop/NativeMethods.cs +++ b/src/Avalonia.FreeDesktop/NativeMethods.cs @@ -15,18 +15,18 @@ namespace Avalonia.FreeDesktop public static string ReadLink(string path) { var symlinkSize = Encoding.UTF8.GetByteCount(path); - var bufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated + const int BufferSize = 4097; // PATH_MAX is (usually?) 4096, but we need to know if the result was truncated var symlink = ArrayPool.Shared.Rent(symlinkSize + 1); - var buffer = ArrayPool.Shared.Rent(bufferSize); + var buffer = ArrayPool.Shared.Rent(BufferSize); try { Encoding.UTF8.GetBytes(path, 0, path.Length, symlink, 0); symlink[symlinkSize] = 0; - var size = readlink(symlink, buffer, bufferSize); - Debug.Assert(size < bufferSize); // if this fails, we need to increase the buffer size (dynamically?) + var size = readlink(symlink, buffer, BufferSize); + Debug.Assert(size < BufferSize); // if this fails, we need to increase the buffer size (dynamically?) return Encoding.UTF8.GetString(buffer, 0, (int)size); } diff --git a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs index 5cd44905af..4e6ce3bd43 100644 --- a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs +++ b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs @@ -145,10 +145,10 @@ namespace Avalonia.FreeDesktop public Task GetPropertiesAsync() { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, s) - => ReadMessage(m, (StatusNotifierWatcherObject)s!), this); + return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) + => ReadMessage(m), this); - static StatusNotifierWatcherProperties ReadMessage(Message message, StatusNotifierWatcherObject _) + static StatusNotifierWatcherProperties ReadMessage(Message message) { var reader = message.GetBodyReader(); return ReadProperties(ref reader); @@ -157,9 +157,9 @@ namespace Avalonia.FreeDesktop public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) { - return base.WatchPropertiesChangedAsync(Interface, static (m, s) => ReadMessage(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); + return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message, StatusNotifierWatcherObject _) + static PropertyChanges ReadMessage(Message message) { var reader = message.GetBodyReader(); reader.ReadString(); // interface @@ -194,7 +194,7 @@ namespace Avalonia.FreeDesktop } } - private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, List? changedList = null) + private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) { var props = new StatusNotifierWatcherProperties(); var headersEnd = reader.ReadArrayStart(DBusType.Struct); @@ -245,8 +245,8 @@ namespace Avalonia.FreeDesktop (Service, Path) = (service, path); } - public StatusNotifierWatcherService Service { get; } - public ObjectPath Path { get; } + protected StatusNotifierWatcherService Service { get; } + protected ObjectPath Path { get; } protected Connection Connection => Service.Connection; protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) @@ -291,7 +291,7 @@ namespace Avalonia.FreeDesktop ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { @@ -301,12 +301,11 @@ namespace Avalonia.FreeDesktop Member = signal, Interface = @interface }; - return Connection.AddMatchAsync(rule, reader, - (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); + return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) + => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); } - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) + protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) { var rule = new MatchRule { From e5985aa675221931c25cbc27b168b9fac3a254a4 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 30 Dec 2022 14:34:31 +0100 Subject: [PATCH 005/326] Import TrimmingEnable.props --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a33fa7b32d..e80a90e38a 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,6 +5,9 @@ enable + + + @@ -15,7 +18,9 @@ + + From 2c2eed724eb6e821b41d0d61134737bdb4525068 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:26:36 +0100 Subject: [PATCH 006/326] Use source generator --- .gitmodules | 3 + Avalonia.Desktop.slnf | 1 + Avalonia.sln | 7 + .../AppMenuRegistrar.DBus.cs | 173 ---- .../Avalonia.FreeDesktop.csproj | 5 + src/Avalonia.FreeDesktop/DBus.DBus.cs | 641 ------------ .../DBusIme/DBusTextInputMethodBase.cs | 2 +- .../DBusIme/Fcitx/Fcitx.DBus.cs | 627 ------------ .../DBusIme/Fcitx/FcitxICWrapper.cs | 10 +- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 9 +- .../DBusIme/IBus/IBus.DBus.cs | 513 ---------- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 12 +- src/Avalonia.FreeDesktop/DBusInterfaces.cs | 18 + src/Avalonia.FreeDesktop/DBusMenu.DBus.cs | 500 ---------- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 178 +--- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 18 +- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 190 +--- .../FreeDesktopPortalDesktop.DBus.cs | 938 ------------------ .../StatusNotifierWatcher.DBus.cs | 350 ------- src/tools/Tmds.DBus.SourceGenerator | 1 + 20 files changed, 130 insertions(+), 4066 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBus.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs create mode 100644 src/Avalonia.FreeDesktop/DBusInterfaces.cs delete mode 100644 src/Avalonia.FreeDesktop/DBusMenu.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs delete mode 100644 src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs create mode 160000 src/tools/Tmds.DBus.SourceGenerator diff --git a/.gitmodules b/.gitmodules index 16bc977251..2b899e8f5a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "Tmds.DBus"] path = src/Linux/Tmds.DBus url = https://github.com/affederaffe/Tmds.DBus +[submodule "src/tools/Tmds.DBus.SourceGenerator"] + path = src/tools/Tmds.DBus.SourceGenerator + url = https://github.com/affederaffe/Tmds.DBus.SourceGenerator diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 72eb13d0a9..165245781b 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -38,6 +38,7 @@ "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\tools\\Tmds.DBus.SourceGenerator\\Tmds.DBus.SourceGenerator\\Tmds.DBus.SourceGenerator.csproj", "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index e399483896..47ed2b9140 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -233,6 +233,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.Protocol", "src\Linux\Tmds.DBus\src\Tmds.DBus.Protocol\Tmds.DBus.Protocol.csproj", "{29E25263-3CC3-4D55-A042-00BA136867D4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.SourceGenerator", "src\tools\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator.csproj", "{5E9C0032-E460-4BC1-BCC7-6448F34DD679}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -548,6 +550,10 @@ Global {29E25263-3CC3-4D55-A042-00BA136867D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {29E25263-3CC3-4D55-A042-00BA136867D4}.Release|Any CPU.Build.0 = Release|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E9C0032-E460-4BC1-BCC7-6448F34DD679}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -613,6 +619,7 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {29E25263-3CC3-4D55-A042-00BA136867D4} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} + {5E9C0032-E460-4BC1-BCC7-6448F34DD679} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs b/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs deleted file mode 100644 index 75c8ce61a5..0000000000 --- a/src/Avalonia.FreeDesktop/AppMenuRegistrar.DBus.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal class Registrar : AppMenuRegistrarObject - { - private const string Interface = "com.canonical.AppMenu.Registrar"; - - public Registrar(RegistrarService service, ObjectPath path) : base(service, path) { } - - public Task RegisterWindowAsync(uint windowId, ObjectPath menuObjectPath) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uo", - member: "RegisterWindow", - flags: MessageFlags.NoReplyExpected); - writer.WriteUInt32(windowId); - writer.WriteObjectPath(menuObjectPath); - return writer.CreateMessage(); - } - } - - public Task UnregisterWindowAsync(uint windowId) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "UnregisterWindow"); - writer.WriteUInt32(windowId); - return writer.CreateMessage(); - } - } - - public Task<(string Service, ObjectPath MenuObjectPath)> GetMenuForWindowAsync(uint windowId) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_so(m, (AppMenuRegistrarObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "GetMenuForWindow"); - writer.WriteUInt32(windowId); - return writer.CreateMessage(); - } - } - } - - internal class RegistrarService - { - public RegistrarService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public Registrar CreateRegistrar(string path) => new(this, path); - } - - internal class AppMenuRegistrarObject - { - protected AppMenuRegistrarObject(RegistrarService service, ObjectPath path) - => (Service, Path) = (service, path); - - protected RegistrarService Service { get; } - protected ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) - => ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) - => null!, static (ex, _, _, hs) - => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static (string, ObjectPath) ReadMessage_so(Message message, AppMenuRegistrarObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadObjectPath(); - return (arg0, arg1); - } - } -} diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index e80a90e38a..e1e0a636ea 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -17,10 +17,15 @@ + + + + + diff --git a/src/Avalonia.FreeDesktop/DBus.DBus.cs b/src/Avalonia.FreeDesktop/DBus.DBus.cs deleted file mode 100644 index 0f83ddaa4a..0000000000 --- a/src/Avalonia.FreeDesktop/DBus.DBus.cs +++ /dev/null @@ -1,641 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal record DBusProperties - { - public string[] Features { get; set; } = default!; - public string[] Interfaces { get; set; } = default!; - } - - internal class DBus : DBusObject - { - private const string Interface = "org.freedesktop.DBus"; - - public DBus(DBusService service, ObjectPath path) : base(service, path) { } - - public Task HelloAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Hello"); - return writer.CreateMessage(); - } - } - - public Task RequestNameAsync(string a0, uint a1) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "su", - member: "RequestName"); - writer.WriteString(a0); - writer.WriteUInt32(a1); - return writer.CreateMessage(); - } - } - - public Task ReleaseNameAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "ReleaseName"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task StartServiceByNameAsync(string a0, uint a1) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "su", - member: "StartServiceByName"); - writer.WriteString(a0); - writer.WriteUInt32(a1); - return writer.CreateMessage(); - } - } - - public Task UpdateActivationEnvironmentAsync(Dictionary a0) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "a{ss}", - member: "UpdateActivationEnvironment"); - writer.WriteDictionary(a0); - return writer.CreateMessage(); - } - } - - public Task NameHasOwnerAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "NameHasOwner"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task ListNamesAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "ListNames"); - return writer.CreateMessage(); - } - } - - public Task ListActivatableNamesAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "ListActivatableNames"); - return writer.CreateMessage(); - } - } - - public Task AddMatchAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "AddMatch"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task RemoveMatchAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RemoveMatch"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetNameOwnerAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetNameOwner"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task ListQueuedOwnersAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_as(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "ListQueuedOwners"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetConnectionUnixUserAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionUnixUser"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetConnectionUnixProcessIDAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_u(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionUnixProcessID"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetAdtAuditSessionDataAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetAdtAuditSessionData"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task GetConnectionSELinuxSecurityContextAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ay(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionSELinuxSecurityContext"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public Task ReloadConfigAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "ReloadConfig"); - return writer.CreateMessage(); - } - } - - public Task GetIdAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_s(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "GetId"); - return writer.CreateMessage(); - } - } - - public Task> GetConnectionCredentialsAsync(string a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_aesv(m, (DBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetConnectionCredentials"); - writer.WriteString(a0); - return writer.CreateMessage(); - } - } - - public ValueTask WatchNameOwnerChangedAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "NameOwnerChanged", static (m, s) => - ReadMessage_sss(m, (DBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchNameLostAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "NameLost", static (m, s) => - ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchNameAcquiredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "NameAcquired", static (m, s) => - ReadMessage_s(m, (DBusObject)s!), handler, emitOnCapturedContext); - - public Task SetFeaturesAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Features"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task SetInterfacesAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Interfaces"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task GetFeaturesAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Features"), static (m, s) => - ReadMessage_v_as(m, (DBusObject)s!), this); - - public Task GetInterfacesAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Interfaces"), static (m, s) => - ReadMessage_v_as(m, (DBusObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => - ReadMessage(m), this); - - static DBusProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) => - ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "Features": - invalidated.Add("Features"); - break; - case "Interfaces": - invalidated.Add("Interfaces"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static DBusProperties ReadProperties(ref Reader reader, List? changedList = null) - { - var props = new DBusProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "Features": - reader.ReadSignature("as"); - props.Features = reader.ReadArray(); - changedList?.Add("Features"); - break; - case "Interfaces": - reader.ReadSignature("as"); - props.Interfaces = reader.ReadArray(); - changedList?.Add("Interfaces"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal class DBusService - { - public DBusService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public DBus CreateDBus(string path) => new(this, path); - } - - internal class DBusObject - { - protected DBusObject(DBusService service, ObjectPath path) - { - Service = service; - Path = path; - } - - public DBusService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => - ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static string ReadMessage_s(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static uint ReadMessage_u(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadUInt32(); - } - - protected static bool ReadMessage_b(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static string[] ReadMessage_as(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadArray(); - } - - protected static byte[] ReadMessage_ay(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadArray(); - } - - protected static Dictionary ReadMessage_aesv(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadDictionary(); - } - - protected static (string, string, string) ReadMessage_sss(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadString(); - return (arg0, arg1, arg2); - } - - protected static string[] ReadMessage_v_as(Message message, DBusObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("as"); - return reader.ReadArray(); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index c7bf530cfd..08d2a0c219 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -60,7 +60,7 @@ namespace Avalonia.FreeDesktop.DBusIme { foreach (var name in _knownNames) { - var dbus = new DBusService(Connection, name).CreateDBus("/org/freedesktop/DBus"); + var dbus = new OrgFreedesktopDBus(Connection, name, "/org/freedesktop/DBus"); _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); var nameOwner = await dbus.GetNameOwnerAsync(name); OnNameChange(null, (name, null, nameOwner)); diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs deleted file mode 100644 index f9e4497892..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/Fcitx.DBus.cs +++ /dev/null @@ -1,627 +0,0 @@ -using System; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop.DBusIme.Fcitx -{ - internal class InputContext : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputContext"; - - public InputContext(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task FocusInAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusIn"); - return writer.CreateMessage(); - } - } - - public Task FocusOutAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusOut"); - return writer.CreateMessage(); - } - } - - public Task ResetAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Reset"); - return writer.CreateMessage(); - } - } - - public Task SetCursorRectAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorRect"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task SetCapacityAsync(uint caps) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "SetCapacity"); - writer.WriteUInt32(caps); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "suu", - member: "SetSurroundingText"); - writer.WriteString(text); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uu", - member: "SetSurroundingTextPosition"); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task DestroyICAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "DestroyIC"); - return writer.CreateMessage(); - } - } - - public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, int type, uint time) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_i(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uuuiu", - member: "ProcessKeyEvent"); - writer.WriteUInt32(keyval); - writer.WriteUInt32(keycode); - writer.WriteUInt32(state); - writer.WriteInt32(type); - writer.WriteUInt32(time); - return writer.CreateMessage(); - } - } - - public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => - ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchCurrentIMAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => - ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => - ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchForwardKeyAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => - ReadMessage_uui(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchDeleteSurroundingTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => - ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); - } - - internal class InputMethod : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputMethod"; - - public InputMethod(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task<(int Icid, bool Enable, uint Keyval1, uint State1, uint Keyval2, uint State2)> CreateICv3Async(string appname, int pid) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_ibuuuu(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "si", - member: "CreateICv3"); - writer.WriteString(appname); - writer.WriteInt32(pid); - return writer.CreateMessage(); - } - } - } - - internal class InputContext1 : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputContext1"; - - public InputContext1(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task FocusInAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusIn"); - return writer.CreateMessage(); - } - } - - public Task FocusOutAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusOut"); - return writer.CreateMessage(); - } - } - - public Task ResetAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Reset"); - return writer.CreateMessage(); - } - } - - public Task SetCursorRectAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorRect"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task SetCapabilityAsync(ulong caps) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "t", - member: "SetCapability"); - writer.WriteUInt64(caps); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextAsync(string text, uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "suu", - member: "SetSurroundingText"); - writer.WriteString(text); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextPositionAsync(uint cursor, uint anchor) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uu", - member: "SetSurroundingTextPosition"); - writer.WriteUInt32(cursor); - writer.WriteUInt32(anchor); - return writer.CreateMessage(); - } - } - - public Task DestroyICAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "DestroyIC"); - return writer.CreateMessage(); - } - } - - public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state, bool type, uint time) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uuubu", - member: "ProcessKeyEvent"); - writer.WriteUInt32(keyval); - writer.WriteUInt32(keycode); - writer.WriteUInt32(state); - writer.WriteBool(type); - writer.WriteUInt32(time); - return writer.CreateMessage(); - } - } - - public ValueTask WatchCommitStringAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "CommitString", static (m, s) => - ReadMessage_s(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchCurrentIMAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CurrentIM", static (m, s) => - ReadMessage_sss(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdateFormattedPreeditAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateFormattedPreedit", static (m, s) => - ReadMessage_arsizi(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchForwardKeyAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKey", static (m, s) => - ReadMessage_uub(m, (FcitxObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchDeleteSurroundingTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "DeleteSurroundingText", static (m, s) => - ReadMessage_iu(m, (FcitxObject)s!), handler, emitOnCapturedContext); - } - - internal class InputMethod1 : FcitxObject - { - private const string Interface = "org.fcitx.Fcitx.InputMethod1"; - - public InputMethod1(FcitxService service, ObjectPath path) : base(service, path) { } - - public Task<(ObjectPath A0, byte[] A1)> CreateInputContextAsync((string, string)[] a0) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_oay(m, (FcitxObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "a(ss)", - member: "CreateInputContext"); - writer.WriteArray(a0); - return writer.CreateMessage(); - } - } - } - - internal class FcitxService - { - public FcitxService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public InputContext CreateInputContext(string path) => new(this, path); - public InputMethod CreateInputMethod(string path) => new(this, path); - public InputContext1 CreateInputContext1(string path) => new(this, path); - public InputMethod1 CreateInputMethod1(string path) => new(this, path); - } - - internal class FcitxObject - { - protected FcitxObject(FcitxService service, ObjectPath path) - => (Service, Path) = (service, path); - - public FcitxService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) => - null!, static (ex, _, _, hs) => - ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static int ReadMessage_i(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadInt32(); - } - - protected static string ReadMessage_s(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static (string, string, string) ReadMessage_sss(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadString(); - return (arg0, arg1, arg2); - } - - protected static ((string, int)[], int) ReadMessage_arsizi(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadArray<(string, int)>(); - var arg1 = reader.ReadInt32(); - return (arg0, arg1); - } - - protected static (uint, uint, int) ReadMessage_uui(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadInt32(); - return (arg0, arg1, arg2); - } - - protected static (int, uint) ReadMessage_iu(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadInt32(); - var arg1 = reader.ReadUInt32(); - return (arg0, arg1); - } - - protected static (int, bool, uint, uint, uint, uint) ReadMessage_ibuuuu(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadInt32(); - var arg1 = reader.ReadBool(); - var arg2 = reader.ReadUInt32(); - var arg3 = reader.ReadUInt32(); - var arg4 = reader.ReadUInt32(); - var arg5 = reader.ReadUInt32(); - return (arg0, arg1, arg2, arg3, arg4, arg5); - } - - protected static bool ReadMessage_b(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static (uint, uint, bool) ReadMessage_uub(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadBool(); - return (arg0, arg1, arg2); - } - - protected static (ObjectPath, byte[]) ReadMessage_oay(Message message, FcitxObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadObjectPath(); - var arg1 = reader.ReadArray(); - return (arg0, arg1); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index b71ca55cf8..b651f126fb 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -5,15 +5,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly InputContext1? _modern; - private readonly InputContext? _old; + private readonly OrgFcitxFcitxInputContext1? _modern; + private readonly OrgFcitxFcitxInputContext? _old; - public FcitxICWrapper(InputContext old) + public FcitxICWrapper(OrgFcitxFcitxInputContext old) { _old = old; } - public FcitxICWrapper(InputContext1 modern) + public FcitxICWrapper(OrgFcitxFcitxInputContext1 modern) { _modern = modern; } @@ -43,7 +43,7 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx public ValueTask WatchForwardKeyAsync(Action handler) => _old?.WatchForwardKeyAsync(handler) - ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.Keyval, ev.State, ev.Type ? 1 : 0))) + ?? _modern?.WatchForwardKeyAsync((e, ev) => handler.Invoke(e, (ev.keyval, ev.state, ev.type ? 1 : 0))) ?? new ValueTask(default(IDisposable?)); public Task SetCapacityAsync(uint flags) => diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 2a5260b1cf..6b30c5424a 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -17,21 +17,20 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx protected override async Task Connect(string name) { - var service = new FcitxService(Connection, name); if (name == "org.fcitx.Fcitx") { - var method = service.CreateInputMethod("/inputmethod"); + var method = new OrgFcitxFcitxInputMethod(Connection, name, "/inputmethod"); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = service.CreateInputContext($"/inputcontext_{resp.Icid}"); + var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.icid}"); _context = new FcitxICWrapper(proxy); } else { - var method = service.CreateInputMethod1("/inputmethod"); + var method = new OrgFcitxFcitxInputMethod1(Connection, name, "/inputmethod"); var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); - var proxy = service.CreateInputContext1(resp.A0); + var proxy = new OrgFcitxFcitxInputContext1(Connection, name, resp.Item1); _context = new FcitxICWrapper(proxy); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs deleted file mode 100644 index e0f9681766..0000000000 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBus.DBus.cs +++ /dev/null @@ -1,513 +0,0 @@ -using System; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop.DBusIme.IBus -{ - internal class Portal : IBusObject - { - private const string Interface = "org.freedesktop.IBus.Portal"; - - public Portal(IBusService service, ObjectPath path) : base(service, path) { } - - public Task CreateInputContextAsync(string clientName) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (IBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "CreateInputContext"); - writer.WriteString(clientName); - return writer.CreateMessage(); - } - } - } - - internal class InputContext : IBusObject - { - private const string Interface = "org.freedesktop.IBus.InputContext"; - - public InputContext(IBusService service, ObjectPath path) : base(service, path) { } - - public Task ProcessKeyEventAsync(uint keyval, uint keycode, uint state) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_b(m, (IBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "uuu", - member: "ProcessKeyEvent"); - writer.WriteUInt32(keyval); - writer.WriteUInt32(keycode); - writer.WriteUInt32(state); - return writer.CreateMessage(); - } - } - - public Task SetCursorLocationAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorLocation"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task SetCursorLocationRelativeAsync(int x, int y, int w, int h) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iiii", - member: "SetCursorLocationRelative"); - writer.WriteInt32(x); - writer.WriteInt32(y); - writer.WriteInt32(w); - writer.WriteInt32(h); - return writer.CreateMessage(); - } - } - - public Task ProcessHandWritingEventAsync(double[] coordinates) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ad", - member: "ProcessHandWritingEvent"); - writer.WriteArray(coordinates); - return writer.CreateMessage(); - } - } - - public Task CancelHandWritingAsync(uint nStrokes) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "CancelHandWriting"); - writer.WriteUInt32(nStrokes); - return writer.CreateMessage(); - } - } - - public Task FocusInAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusIn"); - return writer.CreateMessage(); - } - } - - public Task FocusOutAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "FocusOut"); - return writer.CreateMessage(); - } - } - - public Task ResetAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Reset"); - return writer.CreateMessage(); - } - } - - public Task SetCapabilitiesAsync(uint caps) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "u", - member: "SetCapabilities"); - writer.WriteUInt32(caps); - return writer.CreateMessage(); - } - } - - public Task PropertyActivateAsync(string name, uint state) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "su", - member: "PropertyActivate"); - writer.WriteString(name); - writer.WriteUInt32(state); - return writer.CreateMessage(); - } - } - - public Task SetEngineAsync(string name) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "SetEngine"); - writer.WriteString(name); - return writer.CreateMessage(); - } - } - - public Task GetEngineAsync() - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_v(m, (IBusObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "GetEngine"); - return writer.CreateMessage(); - } - } - - public Task SetSurroundingTextAsync(object text, uint cursorPos, uint anchorPos) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "vuu", - member: "SetSurroundingText"); - writer.WriteVariant(text); - writer.WriteUInt32(cursorPos); - writer.WriteUInt32(anchorPos); - return writer.CreateMessage(); - } - } - - public ValueTask WatchCommitTextAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "CommitText", static (m, s) => - ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchForwardKeyEventAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "ForwardKeyEvent", static (m, s) => - ReadMessage_uuu(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdatePreeditTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditText", static (m, s) => - ReadMessage_vub(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdatePreeditTextWithModeAsync( - Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdatePreeditTextWithMode", static (m, s) => - ReadMessage_vubu(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchShowPreeditTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ShowPreeditText", handler, emitOnCapturedContext); - - public ValueTask WatchHidePreeditTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "HidePreeditText", handler, emitOnCapturedContext); - - public ValueTask WatchUpdateAuxiliaryTextAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateAuxiliaryText", static (m, s) => - ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchShowAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ShowAuxiliaryText", handler, emitOnCapturedContext); - - public ValueTask WatchHideAuxiliaryTextAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "HideAuxiliaryText", handler, emitOnCapturedContext); - - public ValueTask WatchUpdateLookupTableAsync(Action handler, - bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "UpdateLookupTable", static (m, s) => - ReadMessage_vb(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchShowLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ShowLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchHideLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "HideLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchPageUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "PageUpLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchPageDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "PageDownLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchCursorUpLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "CursorUpLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchCursorDownLookupTableAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "CursorDownLookupTable", handler, emitOnCapturedContext); - - public ValueTask WatchRegisterPropertiesAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "RegisterProperties", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchUpdatePropertyAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "UpdateProperty", static (m, s) => ReadMessage_v(m, (IBusObject)s!), handler, emitOnCapturedContext); - } - - internal class Service : IBusObject - { - private const string Interface = "org.freedesktop.IBus.Service"; - - public Service(IBusService service, ObjectPath path) : base(service, path) { } - - public Task DestroyAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Destroy"); - return writer.CreateMessage(); - } - } - } - - internal class IBusService - { - public IBusService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public Portal CreatePortal(string path) => new(this, path); - public InputContext CreateInputContext(string path) => new(this, path); - public Service CreateService(string path) => new(this, path); - } - - internal class IBusObject - { - protected IBusObject(IBusService service, ObjectPath path) - => (Service, Path) = (service, path); - - public IBusService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => ((Action)hs!).Invoke(ex, arg), - this, handler, emitOnCapturedContext); - } - - public ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) => null!, static (ex, _, _, hs) => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static ObjectPath ReadMessage_o(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadObjectPath(); - } - - protected static bool ReadMessage_b(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static object ReadMessage_v(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadVariant(); - } - - protected static (uint, uint, uint) ReadMessage_uuu(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadUInt32(); - return (arg0, arg1, arg2); - } - - protected static (object, uint, bool) ReadMessage_vub(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadBool(); - return (arg0, arg1, arg2); - } - - protected static (object, uint, bool, uint) ReadMessage_vubu(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadUInt32(); - var arg2 = reader.ReadBool(); - var arg3 = reader.ReadUInt32(); - return (arg0, arg1, arg2, arg3); - } - - protected static (object, bool) ReadMessage_vb(Message message, IBusObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadBool(); - return (arg0, arg1); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index f3ab8617a4..4db0d95d3c 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -9,17 +9,17 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private Service? _service; - private InputContext? _context; + private OrgFreedesktopIBusService? _service; + private OrgFreedesktopIBusInputContext? _context; public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } protected override async Task Connect(string name) { - var service = new IBusService(Connection, name); - var path = await service.CreatePortal("/org/freedesktop/IBus").CreateInputContextAsync(GetAppName()); - _context = service.CreateInputContext(path); - _service = service.CreateService(path); + var portal = new OrgFreedesktopIBusPortal(Connection, name, "/org/freedesktop/IBus"); + var path = await portal.CreateInputContextAsync(GetAppName()); + _service = new OrgFreedesktopIBusService(Connection, name, path); + _context = new OrgFreedesktopIBusInputContext(Connection, name, path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); diff --git a/src/Avalonia.FreeDesktop/DBusInterfaces.cs b/src/Avalonia.FreeDesktop/DBusInterfaces.cs new file mode 100644 index 0000000000..3bece27ab1 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusInterfaces.cs @@ -0,0 +1,18 @@ +using Tmds.DBus.SourceGenerator; + +namespace Avalonia.FreeDesktop +{ + [DBusInterface("./DBusXml/DBus.xml")] + [DBusInterface("./DBusXml/StatusNotifierWatcher.xml")] + [DBusInterface("./DBusXml/com.canonical.AppMenu.Registrar.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext1.xml")] + [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod1.xml")] + [DBusInterface("./DBusXml/org.freedesktop.portal.FileChooser.xml")] + [DBusInterface("./DBusXml/org.freedesktop.portal.Request.xml")] + [DBusInterface("./DBusXml/org.freedesktop.IBus.Portal.xml")] + [DBusHandler("./DBusXml/DBusMenu.xml")] + [DBusHandler("./DBusXml/StatusNotifierItem.xml")] + internal class DBusInterfaces { } +} diff --git a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs b/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs deleted file mode 100644 index e3928cfbb2..0000000000 --- a/src/Avalonia.FreeDesktop/DBusMenu.DBus.cs +++ /dev/null @@ -1,500 +0,0 @@ -using System; -using Tmds.DBus.Protocol; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Avalonia.FreeDesktop -{ - internal record DBusMenuProperties - { - public uint Version { get; set; } - public string TextDirection { get; set; } = default!; - public string Status { get; set; } = default!; - public string[] IconThemePath { get; set; } = default!; - } - - internal class DBusMenu : DBusMenuObject - { - private const string Interface = "com.canonical.dbusmenu"; - - public DBusMenu(DBusMenuService service, ObjectPath path) : base(service, path) { } - - public Task<(uint Revision, (int, Dictionary, object[]) Layout)> GetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_uriaesvavz(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "iias", - member: "GetLayout"); - writer.WriteInt32(parentId); - writer.WriteInt32(recursionDepth); - writer.WriteArray(propertyNames); - return writer.CreateMessage(); - } - } - - public Task<(int, Dictionary)[]> GetGroupPropertiesAsync(int[] ids, string[] propertyNames) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ariaesvz(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "aias", - member: "GetGroupProperties"); - writer.WriteArray(ids); - writer.WriteArray(propertyNames); - return writer.CreateMessage(); - } - } - - public Task GetPropertyAsync(int id, string name) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_v(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "is", - member: "GetProperty"); - writer.WriteInt32(id); - writer.WriteString(name); - return writer.CreateMessage(); - } - } - - public Task EventAsync(int id, string eventId, object data, uint timestamp) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "isvu", - member: "Event"); - writer.WriteInt32(id); - writer.WriteString(eventId); - writer.WriteVariant(data); - writer.WriteUInt32(timestamp); - return writer.CreateMessage(); - } - } - - public Task EventGroupAsync((int, string, object, uint)[] events) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_ai(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "a(isvu)", - member: "EventGroup"); - writer.WriteArray(events); - return writer.CreateMessage(); - } - } - - public Task AboutToShowAsync(int id) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_b(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "i", - member: "AboutToShow"); - writer.WriteInt32(id); - return writer.CreateMessage(); - } - } - - public Task<(int[] UpdatesNeeded, int[] IdErrors)> AboutToShowGroupAsync(int[] ids) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, _) => ReadMessage_aiai(m), this); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ai", - member: "AboutToShowGroup"); - writer.WriteArray(ids); - return writer.CreateMessage(); - } - } - - public ValueTask WatchItemsPropertiesUpdatedAsync(Action)[] UpdatedProps, (int, string[])[] RemovedProps)> handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemsPropertiesUpdated", static (m, _) => ReadMessage_ariaesvzariasz(m), handler, emitOnCapturedContext); - - public ValueTask WatchLayoutUpdatedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "LayoutUpdated", static (m, _) => ReadMessage_ui(m), handler, emitOnCapturedContext); - - public ValueTask WatchItemActivationRequestedAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ItemActivationRequested", static (m, _) => ReadMessage_iu(m), handler, emitOnCapturedContext); - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task SetTextDirectionAsync(string value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("TextDirection"); - writer.WriteSignature("s"); - writer.WriteString(value); - return writer.CreateMessage(); - } - } - - public Task SetStatusAsync(string value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("Status"); - writer.WriteSignature("s"); - writer.WriteString(value); - return writer.CreateMessage(); - } - } - - public Task SetIconThemePathAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("IconThemePath"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Version"), static (m, _) => ReadMessage_v_u(m), this); - - public Task GetTextDirectionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "TextDirection"), static (m, _) => ReadMessage_v_s(m), this); - - public Task GetStatusAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "Status"), static (m, _) => ReadMessage_v_s(m), this); - - public Task GetIconThemePathAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IconThemePath"), static (m, _) => ReadMessage_v_as(m), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) => ReadMessage(m), this); - static DBusMenuProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); - } - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "Version": invalidated.Add("Version"); break; - case "TextDirection": invalidated.Add("TextDirection"); break; - case "Status": invalidated.Add("Status"); break; - case "IconThemePath": invalidated.Add("IconThemePath"); break; - } - } - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static DBusMenuProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new DBusMenuProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "Version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - case "TextDirection": - reader.ReadSignature("s"); - props.TextDirection = reader.ReadString(); - changedList?.Add("TextDirection"); - break; - case "Status": - reader.ReadSignature("s"); - props.Status = reader.ReadString(); - changedList?.Add("Status"); - break; - case "IconThemePath": - reader.ReadSignature("as"); - props.IconThemePath = reader.ReadArray(); - changedList?.Add("IconThemePath"); - break; - default: - reader.ReadVariant(); - break; - } - } - return props; - } - } - - internal class DBusMenuService - { - public Connection Connection { get; } - public string Destination { get; } - public DBusMenuService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - public DBusMenu CreateDbusmenu(string path) => new(this, path); - } - - internal class DBusMenuObject - { - public DBusMenuService Service { get; } - public ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - protected DBusMenuObject(DBusMenuService service, ObjectPath path) - => (Service, Path) = (service, path); - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) - => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) - => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) - => null!, static (ex, _, _, hs) - => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static (uint, (int, Dictionary, object[])) ReadMessage_uriaesvavz(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadStruct, object[]>(); - return (arg0, arg1); - } - - protected static (int, Dictionary)[] ReadMessage_ariaesvz(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadArray<(int, Dictionary)>(); - } - - protected static object ReadMessage_v(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadVariant(); - } - - protected static int[] ReadMessage_ai(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadArray(); - } - - protected static bool ReadMessage_b(Message message) - { - var reader = message.GetBodyReader(); - return reader.ReadBool(); - } - - protected static (int[], int[]) ReadMessage_aiai(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadArray(); - var arg1 = reader.ReadArray(); - return (arg0, arg1); - } - - protected static ((int, Dictionary)[], (int, string[])[]) ReadMessage_ariaesvzariasz(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadArray<(int, Dictionary)>(); - var arg1 = reader.ReadArray<(int, string[])>(); - return (arg0, arg1); - } - - protected static (uint, int) ReadMessage_ui(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadInt32(); - return (arg0, arg1); - } - - protected static (int, uint) ReadMessage_iu(Message message) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadInt32(); - var arg1 = reader.ReadUInt32(); - return (arg0, arg1); - } - - protected static uint ReadMessage_v_u(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("u"); - return reader.ReadUInt32(); - } - - protected static string ReadMessage_v_s(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("s"); - return reader.ReadString(); - } - - protected static string[] ReadMessage_v_as(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("as"); - return reader.ReadArray(); - } - } -} diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 69e2eed72f..e8d4a7b6b4 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -25,16 +24,15 @@ namespace Avalonia.FreeDesktop public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IMethodHandler, IDisposable + private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable { private readonly Connection _connection; - private readonly DBusMenuProperties _dBusMenuProperties = new() { Version = 2, Status = "normal" }; private readonly Dictionary _idsToItems = new(); private readonly Dictionary _itemsToIds = new(); private readonly HashSet _menus = new(); private readonly uint _xid; private readonly bool _appMenu = true; - private Registrar? _registrar; + private ComCanonicalAppMenuRegistrar? _registrar; private NativeMenu? _menu; private bool _disposed; private uint _revision = 1; @@ -59,7 +57,40 @@ namespace Avalonia.FreeDesktop Init(); } - public string Path { get; } + public override string Path { get; } + + protected override (uint revision, (int, Dictionary, object[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) + { + var menu = GetMenu(parentId); + var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); + if (!IsNativeMenuExported) + { + IsNativeMenuExported = true; + Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); + } + + return (_revision, layout); + } + + protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => + ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray(); + + protected override object OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? 0; + + protected override void OnEvent(int id, string eventId, object data, uint timestamp) => + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); + + protected override int[] OnEventGroup((int, string, object, uint)[] events) + { + foreach (var e in events) + Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2, e.Item3, e.Item4)); + return Array.Empty(); + } + + protected override bool OnAboutToShow(int id) => false; + + protected override (int[] updatesNeeded, int[] idErrors) OnAboutToShowGroup(int[] ids) => + (Array.Empty(), Array.Empty()); private async void Init() { @@ -69,8 +100,7 @@ namespace Avalonia.FreeDesktop var services = await _connection.ListServicesAsync(); if (!services.Contains("com.canonical.AppMenu.Registrar")) return; - _registrar = new RegistrarService(_connection, "com.canonical.AppMenu.Registrar") - .CreateRegistrar("/com/canonical/AppMenu/Registrar"); + _registrar = new ComCanonicalAppMenuRegistrar(_connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); if (!_disposed) await _registrar.RegisterWindowAsync(_xid, Path); // It's not really important if this code succeeds, @@ -297,140 +327,6 @@ namespace Avalonia.FreeDesktop } } - public ValueTask HandleMethodAsync(MethodContext context) - { - switch (context.Request.InterfaceAsString) - { - case "com.canonical.dbusmenu": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("GetLayout", "iias"): - { - using var writer = context.CreateReplyWriter("u(ia{sv}av)"); - var reader = context.Request.GetBodyReader(); - var parentId = reader.ReadInt32(); - var recursionDepth = reader.ReadInt32(); - var propertyNames = reader.ReadArray(); - var menu = GetMenu(parentId); - var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); - writer.WriteUInt32(_revision); - writer.WriteStruct(layout); - if (!IsNativeMenuExported) - { - IsNativeMenuExported = true; - Dispatcher.UIThread.Post(() => OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty)); - } - - context.Reply(writer.CreateMessage()); - break; - } - case ("GetGroupProperties", "aias"): - { - using var writer = context.CreateReplyWriter("a(ia{sv})"); - var reader = context.Request.GetBodyReader(); - var ids = reader.ReadArray(); - var propertyNames = reader.ReadArray(); - var arrayStart = writer.WriteArrayStart(DBusType.Struct); - foreach (var id in ids) - { - var item = GetMenu(id); - var props = GetProperties(item, propertyNames); - writer.WriteStruct((id, props)); - } - - writer.WriteArrayEnd(arrayStart); - context.Reply(writer.CreateMessage()); - break; - } - case ("GetProperty", "is"): - { - using var writer = context.CreateReplyWriter("v"); - var reader = context.Request.GetBodyReader(); - var id = reader.ReadInt32(); - var name = reader.ReadString(); - writer.WriteVariant(GetProperty(GetMenu(id), name) ?? 0); - context.Reply(writer.CreateMessage()); - break; - } - case ("Event", "isvu"): - { - var reader = context.Request.GetBodyReader(); - var id = reader.ReadInt32(); - var eventId = reader.ReadString(); - var data = reader.ReadVariant(); - var timestamp = reader.ReadUInt32(); - Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); - break; - } - case ("EventGroup", "a(isvu)"): - { - using var writer = context.CreateReplyWriter("ai"); - var reader = context.Request.GetBodyReader(); - var events = reader.ReadArray<(int Id, string EventId, object Data, uint Timestamp)>(); - foreach (var e in events) - Dispatcher.UIThread.Post(() => HandleEvent(e.Id, e.EventId, e.Data, e.Timestamp)); - writer.WriteArray(Array.Empty()); - context.Reply(writer.CreateMessage()); - break; - } - case ("AboutToShow", "i"): - { - using var writer = context.CreateReplyWriter("b"); - writer.WriteBool(false); - context.Reply(writer.CreateMessage()); - break; - } - case ("AboutToShowGroup", "ai"): - { - using var writer = context.CreateReplyWriter("aiai"); - writer.WriteStruct((Array.Empty(), Array.Empty())); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - case "org.freedesktop.DBus.Properties": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("Version", "u"): - { - using var writer = context.CreateReplyWriter("u"); - writer.WriteUInt32(_dBusMenuProperties.Version); - context.Reply(writer.CreateMessage()); - break; - } - case ("TextDirection", "s"): - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_dBusMenuProperties.TextDirection); - context.Reply(writer.CreateMessage()); - break; - } - case ("Status", "s"): - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_dBusMenuProperties.Status); - context.Reply(writer.CreateMessage()); - break; - } - case ("IconThemePath", "as"): - { - using var writer = context.CreateReplyWriter("as"); - writer.WriteArray(_dBusMenuProperties.IconThemePath); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - } - - return default; - } - - public bool RunMethodHandlerSynchronously(Message message) => true; - private void EmitUIntIntSignal(string member, uint arg0, int arg1) { using var writer = _connection.GetMessageWriter(); diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 7d1a0bbfc4..81233f3b2f 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -19,18 +19,18 @@ namespace Avalonia.FreeDesktop return null; var services = await DBusHelper.Connection.ListServicesAsync(); return services.Contains("org.freedesktop.portal.Desktop", StringComparer.Ordinal) - ? new DBusSystemDialog(new DesktopService(DBusHelper.Connection, "org.freedesktop.portal.Desktop"), handle) + ? new DBusSystemDialog(DBusHelper.Connection, handle) : null; } - private readonly DesktopService _desktopService; - private readonly FileChooser _fileChooser; + private readonly Connection _connection; + private readonly OrgFreedesktopPortalFileChooser _fileChooser; private readonly IPlatformHandle _handle; - private DBusSystemDialog(DesktopService desktopService, IPlatformHandle handle) + private DBusSystemDialog(Connection connection, IPlatformHandle handle) { - _desktopService = desktopService; - _fileChooser = desktopService.CreateFileChooser("/org/freedesktop/portal/desktop"); + _connection = connection; + _fileChooser = new OrgFreedesktopPortalFileChooser(connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); _handle = handle; } @@ -53,7 +53,7 @@ namespace Avalonia.FreeDesktop objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = _desktopService.CreateRequest(objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { @@ -82,7 +82,7 @@ namespace Avalonia.FreeDesktop chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = _desktopService.CreateRequest(objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { @@ -113,7 +113,7 @@ namespace Avalonia.FreeDesktop }; var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = _desktopService.CreateRequest(objectPath); + var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index d6518f7246..215408a0e3 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.Logging; using Avalonia.Platform; @@ -13,14 +11,15 @@ namespace Avalonia.FreeDesktop internal class DBusTrayIconImpl : ITrayIconImpl { private static int s_trayIconInstanceId; + public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); private readonly ObjectPath _dbusMenuPath; private readonly Connection? _connection; - private readonly DBus? _dBus; + private readonly OrgFreedesktopDBus? _dBus; private IDisposable? _serviceWatchDisposable; private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private StatusNotifierWatcher? _statusNotifierWatcher; + private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher; private (int, int, byte[]) _icon; private string? _sysTrayServiceName; @@ -48,7 +47,7 @@ namespace Avalonia.FreeDesktop IsActive = true; - _dBus = new DBusService(_connection, "org.freedesktop.DBus").CreateDBus("/org/freedesktop/DBus"); + _dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection); @@ -61,8 +60,7 @@ namespace Avalonia.FreeDesktop if (_connection is null || _isDisposed) return; - _statusNotifierWatcher = new StatusNotifierWatcherService(_connection, "org.kde.StatusNotifierWatcher") - .CreateStatusNotifierWatcher("/StatusNotifierWatcher"); + _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); _serviceConnected = true; } @@ -72,7 +70,7 @@ namespace Avalonia.FreeDesktop if (!services.Contains("org.kde.StatusNotifierWatcher", StringComparer.Ordinal)) return; - _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.A2) ); + _serviceWatchDisposable = await _dBus!.WatchNameOwnerChangedAsync((_, x) => OnNameChange(x.Item2) ); var nameOwner = await _dBus.GetNameOwnerAsync("org.kde.StatusNotifierWatcher"); OnNameChange(nameOwner); } @@ -147,7 +145,7 @@ namespace Avalonia.FreeDesktop if (icon is null) { - _statusNotifierItemDbusObj?.SetIcon(StatusNotifierItemDbusObj.StatusNotifierItemProperties.EmptyPixmap); + _statusNotifierItemDbusObj?.SetIcon(EmptyPixmap); return; } @@ -210,27 +208,37 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : IMethodHandler + internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem { private readonly Connection _connection; - private readonly StatusNotifierItemProperties _backingProperties; public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) { _connection = connection; - _backingProperties = new StatusNotifierItemProperties - { - Menu = dbusMenuPath, // Needs a dbus menu somehow - ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty) - }; - + BackingProperties.Menu = dbusMenuPath; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); + BackingProperties.IconName = string.Empty; + BackingProperties.AttentionIconName = string.Empty; + BackingProperties.AttentionIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; + BackingProperties.AttentionMovieName = string.Empty; + BackingProperties.IconThemePath = string.Empty; + BackingProperties.OverlayIconName = string.Empty; + BackingProperties.OverlayIconPixmap = new []{ DBusTrayIconImpl.EmptyPixmap }; InvalidateAll(); } - public string Path => "/StatusNotifierItem"; + public override string Path => "/StatusNotifierItem"; public event Action? ActivationDelegate; + protected override void OnContextMenu(int x, int y) { } + + protected override void OnActivate(int x, int y) => ActivationDelegate?.Invoke(); + + protected override void OnSecondaryActivate(int x, int y) { } + + protected override void OnScroll(int delta, string orientation) { } + public void InvalidateAll() { EmitVoidSignal("NewTitle"); @@ -238,12 +246,12 @@ namespace Avalonia.FreeDesktop EmitVoidSignal("NewAttentionIcon"); EmitVoidSignal("NewOverlayIcon"); EmitVoidSignal("NewToolTip"); - EmitStringSignal("NewStatus", _backingProperties.Status); + EmitStringSignal("NewStatus", BackingProperties.Status); } public void SetIcon((int, int, byte[]) dbusPixmap) { - _backingProperties.IconPixmap = new[] { dbusPixmap }; + BackingProperties.IconPixmap = new[] { dbusPixmap }; InvalidateAll(); } @@ -252,124 +260,14 @@ namespace Avalonia.FreeDesktop if (text is null) return; - _backingProperties.Id = text; - _backingProperties.Category = "ApplicationStatus"; - _backingProperties.Status = text; - _backingProperties.Title = text; - _backingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); + BackingProperties.Id = text; + BackingProperties.Category = "ApplicationStatus"; + BackingProperties.Status = text; + BackingProperties.Title = text; + BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } - public bool RunMethodHandlerSynchronously(Message message) => false; - - public ValueTask HandleMethodAsync(MethodContext context) - { - switch (context.Request.InterfaceAsString) - { - case "org.kde.StatusNotifierItem": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("ContextMenu", "ii"): - break; - case ("Activate", "ii"): - ActivationDelegate?.Invoke(); - break; - case ("SecondaryActivate", "ii"): - break; - case ("Scroll", "is"): - break; - } - - break; - case "org.freedesktop.DBus.Properties": - switch (context.Request.MemberAsString, context.Request.SignatureAsString) - { - case ("Get", "ss"): - { - var reader = context.Request.GetBodyReader(); - _ = reader.ReadString(); - var member = reader.ReadString(); - switch (member) - { - case "Category": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Category); - context.Reply(writer.CreateMessage()); - break; - } - case "Id": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Id); - context.Reply(writer.CreateMessage()); - break; - } - case "Title": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Title); - context.Reply(writer.CreateMessage()); - break; - } - case "Status": - { - using var writer = context.CreateReplyWriter("s"); - writer.WriteString(_backingProperties.Status); - context.Reply(writer.CreateMessage()); - break; - } - case "Menu": - { - using var writer = context.CreateReplyWriter("o"); - writer.WriteObjectPath(_backingProperties.Menu); - context.Reply(writer.CreateMessage()); - break; - } - case "IconPixmap": - { - using var writer = context.CreateReplyWriter("a(iiay)"); - writer.WriteArray(_backingProperties.IconPixmap); - context.Reply(writer.CreateMessage()); - break; - } - case "ToolTip": - { - using var writer = context.CreateReplyWriter("(sa(iiay)ss)"); - writer.WriteStruct(_backingProperties.ToolTip); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - } - case ("GetAll", "s"): - { - var writer = context.CreateReplyWriter("a{sv}"); - var dict = new Dictionary - { - { "Category", _backingProperties.Category }, - { "Id", _backingProperties.Id }, - { "Title", _backingProperties.Title }, - { "Status", _backingProperties.Status }, - { "Menu", _backingProperties.Menu }, - { "IconPixmap", _backingProperties.IconPixmap }, - { "ToolTip", _backingProperties.ToolTip } - }; - - writer.WriteDictionary(dict); - context.Reply(writer.CreateMessage()); - break; - } - } - - break; - } - - return default; - } - private void EmitVoidSignal(string member) { using var writer = _connection.GetMessageWriter(); @@ -384,27 +282,5 @@ namespace Avalonia.FreeDesktop writer.WriteString(value); _connection.TrySendMessage(writer.CreateMessage()); } - - internal record StatusNotifierItemProperties - { - public string Category { get; set; } = string.Empty; - public string Id { get; set; } = string.Empty; - public string Title { get; set; } = string.Empty; - public string Status { get; set; } = string.Empty; - public int WindowId { get; set; } - public string? IconThemePath { get; set; } - public ObjectPath Menu { get; set; } - public bool ItemIsMenu { get; set; } - public string? IconName { get; set; } - public (int, int, byte[])[] IconPixmap { get; set; } = { EmptyPixmap }; - public string? OverlayIconName { get; set; } - public (int, int, byte[])[]? OverlayIconPixmap { get; set; } - public string? AttentionIconName { get; set; } - public (int, int, byte[])[]? AttentionIconPixmap { get; set; } - public string? AttentionMovieName { get; set; } - public (string, (int, int, byte[])[], string, string) ToolTip { get; set; } - - public static (int, int, byte[]) EmptyPixmap = (1, 1, new byte[] { 255, 0, 0, 0 }); - } } } diff --git a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs b/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs deleted file mode 100644 index 23f1dcf0e7..0000000000 --- a/src/Avalonia.FreeDesktop/FreeDesktopPortalDesktop.DBus.cs +++ /dev/null @@ -1,938 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal record NotificationProperties - { - public uint Version { get; set; } - } - - internal class Notification : DesktopObject - { - private const string Interface = "org.freedesktop.portal.Notification"; - - public Notification(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task AddNotificationAsync(string id, Dictionary notification) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sa{sv}", - member: "AddNotification"); - writer.WriteString(id); - writer.WriteDictionary(notification); - return writer.CreateMessage(); - } - } - - public Task RemoveNotificationAsync(string id) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RemoveNotification"); - writer.WriteString(id); - return writer.CreateMessage(); - } - } - - public ValueTask WatchActionInvokedAsync(Action handler, - bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "ActionInvoked", static (m, s) - => ReadMessage_ssav(m, (DesktopObject)s!), handler, emitOnCapturedContext); - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static NotificationProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static NotificationProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new NotificationProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal record OpenURIProperties - { - public uint Version { get; set; } - } - - internal class OpenURI : DesktopObject - { - private const string Interface = "org.freedesktop.portal.OpenURI"; - - public OpenURI(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task OpenUriAsync(string parentWindow, string uri, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "OpenURI"); - writer.WriteString(parentWindow); - writer.WriteString(uri); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task OpenFileAsync(string parentWindow, SafeHandle fd, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sha{sv}", - member: "OpenFile"); - writer.WriteString(parentWindow); - writer.WriteHandle(fd); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task OpenDirectoryAsync(string parentWindow, SafeHandle fd, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sha{sv}", - member: "OpenDirectory"); - writer.WriteString(parentWindow); - writer.WriteHandle(fd); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static OpenURIProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static OpenURIProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new OpenURIProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal record DynamicLauncherProperties - { - public uint SupportedLauncherTypes { get; set; } - public uint Version { get; set; } - } - - internal class DynamicLauncher : DesktopObject - { - private const string Interface = "org.freedesktop.portal.DynamicLauncher"; - - public DynamicLauncher(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task InstallAsync(string token, string desktopFileId, string desktopEntry, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sssa{sv}", - member: "Install"); - writer.WriteString(token); - writer.WriteString(desktopFileId); - writer.WriteString(desktopEntry); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task PrepareInstallAsync(string parentWindow, string name, object iconV, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssva{sv}", - member: "PrepareInstall"); - writer.WriteString(parentWindow); - writer.WriteString(name); - writer.WriteVariant(iconV); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task RequestInstallTokenAsync(string name, object iconV, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) - => ReadMessage_s(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sva{sv}", - member: "RequestInstallToken"); - writer.WriteString(name); - writer.WriteVariant(iconV); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task UninstallAsync(string desktopFileId, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sa{sv}", - member: "Uninstall"); - writer.WriteString(desktopFileId); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task GetDesktopEntryAsync(string desktopFileId) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) - => ReadMessage_s(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetDesktopEntry"); - writer.WriteString(desktopFileId); - return writer.CreateMessage(); - } - } - - public Task<(object IconV, string IconFormat, uint IconSize)> GetIconAsync(string desktopFileId) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) - => ReadMessage_vsu(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "GetIcon"); - writer.WriteString(desktopFileId); - return writer.CreateMessage(); - } - } - - public Task LaunchAsync(string desktopFileId, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "sa{sv}", - member: "Launch"); - writer.WriteString(desktopFileId); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SetSupportedLauncherTypesAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("SupportedLauncherTypes"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetSupportedLauncherTypesAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "SupportedLauncherTypes"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static DynamicLauncherProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, - emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "SupportedLauncherTypes": - invalidated.Add("SupportedLauncherTypes"); - break; - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static DynamicLauncherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new DynamicLauncherProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "SupportedLauncherTypes": - reader.ReadSignature("u"); - props.SupportedLauncherTypes = reader.ReadUInt32(); - changedList?.Add("SupportedLauncherTypes"); - break; - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal record FileChooserProperties - { - public uint Version { get; set; } - } - - internal class FileChooser : DesktopObject - { - private const string Interface = "org.freedesktop.portal.FileChooser"; - - public FileChooser(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task OpenFileAsync(string parentWindow, string title, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "OpenFile"); - writer.WriteString(parentWindow); - writer.WriteString(title); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SaveFileAsync(string parentWindow, string title, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "SaveFile"); - writer.WriteString(parentWindow); - writer.WriteString(title); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SaveFilesAsync(string parentWindow, string title, Dictionary options) - { - return Connection.CallMethodAsync(CreateMessage(), static (m, s) => ReadMessage_o(m, (DesktopObject)s!), this); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "ssa{sv}", - member: "SaveFiles"); - writer.WriteString(parentWindow); - writer.WriteString(title); - writer.WriteDictionary(options); - return writer.CreateMessage(); - } - } - - public Task SetVersionAsync(uint value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("version"); - writer.WriteSignature("u"); - writer.WriteUInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetVersionAsync() - => Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "version"), static (m, s) - => ReadMessage_v_u(m, (DesktopObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static FileChooserProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, - bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) - => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "version": - invalidated.Add("Version"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static FileChooserProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new FileChooserProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "version": - reader.ReadSignature("u"); - props.Version = reader.ReadUInt32(); - changedList?.Add("Version"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal class Request : DesktopObject - { - private const string Interface = "org.freedesktop.portal.Request"; - - public Request(DesktopService service, ObjectPath path) : base(service, path) { } - - public Task CloseAsync() - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - "Close"); - return writer.CreateMessage(); - } - } - - public ValueTask WatchResponseAsync(Action results)> handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "Response", - static (m, s) => ReadMessage_uaesv(m, (DesktopObject)s!), handler, emitOnCapturedContext); - } - - internal class DesktopService - { - public DesktopService(Connection connection, string destination) - => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - public Notification CreateNotification(string path) => new(this, path); - public OpenURI CreateOpenUri(string path) => new(this, path); - public DynamicLauncher CreateDynamicLauncher(string path) => new(this, path); - public FileChooser CreateFileChooser(string path) => new(this, path); - public Request CreateRequest(string path) => new(this, path); - } - - internal class DesktopObject - { - protected DesktopObject(DesktopService service, ObjectPath path) - => (Service, Path) = (service, path); - - protected DesktopService Service { get; } - protected ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, - MessageValueReader> reader, Action> handler, - bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) - => ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, - MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) => - ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - protected static ObjectPath ReadMessage_o(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadObjectPath(); - } - - protected static uint ReadMessage_v_u(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("u"); - return reader.ReadUInt32(); - } - - protected static (string, string, object[]) ReadMessage_ssav(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadString(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadArray(); - return (arg0, arg1, arg2); - } - - protected static (uint, Dictionary) ReadMessage_uaesv(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadUInt32(); - var arg1 = reader.ReadDictionary(); - return (arg0, arg1); - } - - protected static string ReadMessage_s(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static (object, string, uint) ReadMessage_vsu(Message message, DesktopObject _) - { - var reader = message.GetBodyReader(); - var arg0 = reader.ReadVariant(); - var arg1 = reader.ReadString(); - var arg2 = reader.ReadUInt32(); - return (arg0, arg1, arg2); - } - } - - internal class PropertyChanges - { - public PropertyChanges(TProperties properties, string[] invalidated, string[] changed) - => (Properties, Invalidated, Changed) = (properties, invalidated, changed); - - public TProperties Properties { get; } - public string[] Invalidated { get; } - public string[] Changed { get; } - public bool HasChanged(string property) => Array.IndexOf(Changed, property) != -1; - public bool IsInvalidated(string property) => Array.IndexOf(Invalidated, property) != -1; - } -} diff --git a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs b/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs deleted file mode 100644 index 4e6ce3bd43..0000000000 --- a/src/Avalonia.FreeDesktop/StatusNotifierWatcher.DBus.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus.Protocol; - -namespace Avalonia.FreeDesktop -{ - internal record StatusNotifierWatcherProperties - { - public string[] RegisteredStatusNotifierItems { get; set; } = default!; - public bool IsStatusNotifierHostRegistered { get; set; } - public int ProtocolVersion { get; set; } - } - - internal class StatusNotifierWatcher : StatusNotifierWatcherObject - { - private const string Interface = "org.kde.StatusNotifierWatcher"; - - public StatusNotifierWatcher(StatusNotifierWatcherService service, ObjectPath path) : base(service, path) { } - - public Task RegisterStatusNotifierItemAsync(string service) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RegisterStatusNotifierItem"); - writer.WriteString(service); - return writer.CreateMessage(); - } - } - - public Task RegisterStatusNotifierHostAsync(string service) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - Interface, - signature: "s", - member: "RegisterStatusNotifierHost"); - writer.WriteString(service); - return writer.CreateMessage(); - } - } - - public ValueTask WatchStatusNotifierItemRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemRegistered", static (m, s) => - ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchStatusNotifierItemUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) - => WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierItemUnregistered", static (m, s) - => ReadMessage_s(m, (StatusNotifierWatcherObject)s!), handler, emitOnCapturedContext); - - public ValueTask WatchStatusNotifierHostRegisteredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostRegistered", handler, emitOnCapturedContext); - - public ValueTask WatchStatusNotifierHostUnregisteredAsync(Action handler, bool emitOnCapturedContext = true) => - WatchSignalAsync(Service.Destination, Interface, Path, "StatusNotifierHostUnregistered", handler, emitOnCapturedContext); - - public Task SetRegisteredStatusNotifierItemsAsync(string[] value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("RegisteredStatusNotifierItems"); - writer.WriteSignature("as"); - writer.WriteArray(value); - return writer.CreateMessage(); - } - } - - public Task SetIsStatusNotifierHostRegisteredAsync(bool value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("IsStatusNotifierHostRegistered"); - writer.WriteSignature("b"); - writer.WriteBool(value); - return writer.CreateMessage(); - } - } - - public Task SetProtocolVersionAsync(int value) - { - return Connection.CallMethodAsync(CreateMessage()); - - MessageBuffer CreateMessage() - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ssv", - member: "Set"); - writer.WriteString(Interface); - writer.WriteString("ProtocolVersion"); - writer.WriteSignature("i"); - writer.WriteInt32(value); - return writer.CreateMessage(); - } - } - - public Task GetRegisteredStatusNotifierItemsAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "RegisteredStatusNotifierItems"), static (m, s) - => ReadMessage_v_as(m, (StatusNotifierWatcherObject)s!), this); - - public Task GetIsStatusNotifierHostRegisteredAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "IsStatusNotifierHostRegistered"), static (m, s) => - ReadMessage_v_b(m, (StatusNotifierWatcherObject)s!), this); - - public Task GetProtocolVersionAsync() => - Connection.CallMethodAsync(CreateGetPropertyMessage(Interface, "ProtocolVersion"), static (m, s) - => ReadMessage_v_i(m, (StatusNotifierWatcherObject)s!), this); - - public Task GetPropertiesAsync() - { - return Connection.CallMethodAsync(CreateGetAllPropertiesMessage(Interface), static (m, _) - => ReadMessage(m), this); - - static StatusNotifierWatcherProperties ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - return ReadProperties(ref reader); - } - } - - public ValueTask WatchPropertiesChangedAsync(Action> handler, bool emitOnCapturedContext = true) - { - return base.WatchPropertiesChangedAsync(Interface, static (m, _) => ReadMessage(m), handler, emitOnCapturedContext); - - static PropertyChanges ReadMessage(Message message) - { - var reader = message.GetBodyReader(); - reader.ReadString(); // interface - List changed = new(); - return new PropertyChanges(ReadProperties(ref reader, changed), changed.ToArray(), - ReadInvalidated(ref reader)); - } - - static string[] ReadInvalidated(ref Reader reader) - { - List? invalidated = null; - var headersEnd = reader.ReadArrayStart(DBusType.String); - while (reader.HasNext(headersEnd)) - { - invalidated ??= new List(); - var property = reader.ReadString(); - switch (property) - { - case "RegisteredStatusNotifierItems": - invalidated.Add("RegisteredStatusNotifierItems"); - break; - case "IsStatusNotifierHostRegistered": - invalidated.Add("IsStatusNotifierHostRegistered"); - break; - case "ProtocolVersion": - invalidated.Add("ProtocolVersion"); - break; - } - } - - return invalidated?.ToArray() ?? Array.Empty(); - } - } - - private static StatusNotifierWatcherProperties ReadProperties(ref Reader reader, ICollection? changedList = null) - { - var props = new StatusNotifierWatcherProperties(); - var headersEnd = reader.ReadArrayStart(DBusType.Struct); - while (reader.HasNext(headersEnd)) - { - var property = reader.ReadString(); - switch (property) - { - case "RegisteredStatusNotifierItems": - reader.ReadSignature("as"); - props.RegisteredStatusNotifierItems = reader.ReadArray(); - changedList?.Add("RegisteredStatusNotifierItems"); - break; - case "IsStatusNotifierHostRegistered": - reader.ReadSignature("b"); - props.IsStatusNotifierHostRegistered = reader.ReadBool(); - changedList?.Add("IsStatusNotifierHostRegistered"); - break; - case "ProtocolVersion": - reader.ReadSignature("i"); - props.ProtocolVersion = reader.ReadInt32(); - changedList?.Add("ProtocolVersion"); - break; - default: - reader.ReadVariant(); - break; - } - } - - return props; - } - } - - internal class StatusNotifierWatcherService - { - public StatusNotifierWatcherService(Connection connection, string destination) => (Connection, Destination) = (connection, destination); - - public Connection Connection { get; } - public string Destination { get; } - - public StatusNotifierWatcher CreateStatusNotifierWatcher(string path) => new(this, path); - } - - internal class StatusNotifierWatcherObject - { - protected StatusNotifierWatcherObject(StatusNotifierWatcherService service, ObjectPath path) - { - (Service, Path) = (service, path); - } - - protected StatusNotifierWatcherService Service { get; } - protected ObjectPath Path { get; } - protected Connection Connection => Service.Connection; - - protected MessageBuffer CreateGetPropertyMessage(string @interface, string property) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "ss", - member: "Get"); - writer.WriteString(@interface); - writer.WriteString(property); - return writer.CreateMessage(); - } - - protected MessageBuffer CreateGetAllPropertiesMessage(string @interface) - { - using var writer = Connection.GetMessageWriter(); - writer.WriteMethodCallHeader( - Service.Destination, - Path, - "org.freedesktop.DBus.Properties", - signature: "s", - member: "GetAll"); - writer.WriteString(@interface); - return writer.CreateMessage(); - } - - protected ValueTask WatchPropertiesChangedAsync(string @interface, MessageValueReader> reader, Action> handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = Service.Destination, - Path = Path, - Interface = "org.freedesktop.DBus.Properties", - Member = "PropertiesChanged", - Arg0 = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, changes, _, hs) => - ((Action>)hs!).Invoke(ex, changes), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, MessageValueReader reader, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, reader, static (ex, arg, _, hs) - => ((Action)hs!).Invoke(ex, arg), this, handler, emitOnCapturedContext); - } - - protected ValueTask WatchSignalAsync(string sender, string @interface, ObjectPath path, string signal, Action handler, bool emitOnCapturedContext) - { - var rule = new MatchRule - { - Type = MessageType.Signal, - Sender = sender, - Path = path, - Member = signal, - Interface = @interface - }; - return Connection.AddMatchAsync(rule, static (_, _) - => null!, static (Exception? ex, object _, object? _, object? hs) - => ((Action)hs!).Invoke(ex), this, handler, emitOnCapturedContext); - } - - protected static string ReadMessage_s(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - return reader.ReadString(); - } - - protected static string[] ReadMessage_v_as(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("as"); - return reader.ReadArray(); - } - - protected static bool ReadMessage_v_b(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("b"); - return reader.ReadBool(); - } - - protected static int ReadMessage_v_i(Message message, StatusNotifierWatcherObject _) - { - var reader = message.GetBodyReader(); - reader.ReadSignature("i"); - return reader.ReadInt32(); - } - } -} diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator new file mode 160000 index 0000000000..1cf4faba30 --- /dev/null +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -0,0 +1 @@ +Subproject commit 1cf4faba30741f799a33313a2842cf70eeb6c67e From ebc77f9ba1dbcc15ab98ae72b4f0f92ef808da42 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:41:42 +0100 Subject: [PATCH 007/326] Add xml files --- src/Avalonia.FreeDesktop/DBusXml/DBus.xml | 89 ++++ src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml | 437 ++++++++++++++++++ .../DBusXml/StatusNotifierItem.xml | 96 ++++ .../DBusXml/StatusNotifierWatcher.xml | 42 ++ .../com.canonical.AppMenu.Registrar.xml | 56 +++ .../DBusXml/org.fcitx.Fcitx.InputContext.xml | 64 +++ .../DBusXml/org.fcitx.Fcitx.InputContext1.xml | 64 +++ .../DBusXml/org.fcitx.Fcitx.InputMethod.xml | 16 + .../DBusXml/org.fcitx.Fcitx.InputMethod1.xml | 12 + .../DBusXml/org.freedesktop.IBus.Portal.xml | 139 ++++++ .../org.freedesktop.portal.FileChooser.xml | 377 +++++++++++++++ .../org.freedesktop.portal.Request.xml | 86 ++++ 12 files changed, 1478 insertions(+) create mode 100644 src/Avalonia.FreeDesktop/DBusXml/DBus.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBus.xml b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml new file mode 100644 index 0000000000..a7ecce70f2 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBus.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml new file mode 100644 index 0000000000..de6868cb3e --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/DBusMenu.xml @@ -0,0 +1,437 @@ + + + + + + + + Name + Type + Description + Default Value + + + type + String + Can be one of: + - "standard": an item which can be clicked to trigger an action or + show another menu + - "separator": a separator + + Vendor specific types can be added by prefixing them with + "x--". + + "standard" + + + label + string + Text of the item, except that: + -# two consecutive underscore characters "__" are displayed as a + single underscore, + -# any remaining underscore characters are not displayed at all, + -# the first of those remaining underscore characters (unless it is + the last character in the string) indicates that the following + character is the access key. + + "" + + + enabled + boolean + Whether the item can be activated or not. + true + + + visible + boolean + True if the item is visible in the menu. + true + + + icon-name + string + Icon name of the item, following the freedesktop.org icon spec. + "" + + + icon-data + binary + PNG data of the icon. + Empty + + + shortcut + array of arrays of strings + The shortcut of the item. Each array represents the key press + in the list of keypresses. Each list of strings contains a list of + modifiers and then the key that is used. The modifier strings + allowed are: "Control", "Alt", "Shift" and "Super". + + - A simple shortcut like Ctrl+S is represented as: + [["Control", "S"]] + - A complex shortcut like Ctrl+Q, Alt+X is represented as: + [["Control", "Q"], ["Alt", "X"]] + Empty + + + toggle-type + string + + If the item can be toggled, this property should be set to: + - "checkmark": Item is an independent togglable item + - "radio": Item is part of a group where only one item can be + toggled at a time + - "": Item cannot be toggled + + "" + + + toggle-state + int + + Describe the current state of a "togglable" item. Can be one of: + - 0 = off + - 1 = on + - anything else = indeterminate + + Note: + The implementation does not itself handle ensuring that only one + item in a radio group is set to "on", or that a group does not have + "on" and "indeterminate" items simultaneously; maintaining this + policy is up to the toolkit wrappers. + + -1 + + + children-display + string + + If the menu item has children this property should be set to + "submenu". + + "" + + + disposition + string + + How the menuitem feels the information it's displaying to the + user should be presented. + - "normal" a standard menu item + - "informative" providing additional information to the user + - "warning" looking at potentially harmful results + - "alert" something bad could potentially happen + + "normal" + + + + Vendor specific properties can be added by prefixing them with + "x--". + ]]> + + + + + Provides the version of the DBusmenu API that this API is + implementing. + + + + + + Represents the way the text direction of the application. This + allows the server to handle mismatches intelligently. For left- + to-right the string is "ltr" for right-to-left it is "rtl". + + + + + + Tells if the menus are in a normal state or they believe that they + could use some attention. Cases for showing them would be if help + were referring to them or they accessors were being highlighted. + This property can have two values: "normal" in almost all cases and + "notice" when they should have a higher priority to be shown. + + + + + + A list of directories that should be used for finding icons using + the icon naming spec. Idealy there should only be one for the icon + theme, but additional ones are often added by applications for + app specific icons. + + + + + + + + Provides the layout and propertiers that are attached to the entries + that are in the layout. It only gives the items that are children + of the item that is specified in @a parentId. It will return all of the + properties or specific ones depending of the value in @a propertyNames. + + The format is recursive, where the second 'v' is in the same format + as the original 'a(ia{sv}av)'. Its content depends on the value + of @a recursionDepth. + + + The ID of the parent node for the layout. For + grabbing the layout from the root node use zero. + + + + The amount of levels of recursion to use. This affects the + content of the second variant array. + - -1: deliver all the items under the @a parentId. + - 0: no recursion, the array will be empty. + - n: array will contains items up to 'n' level depth. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + The revision number of the layout. For matching + with layoutUpdated signals. + + + The layout, as a recursive structure. + + + + + + Returns the list of items which are children of @a parentId. + + + + A list of ids that we should be finding the properties + on. If the list is empty, all menu items should be sent. + + + + + The list of item properties we are + interested in. If there are no entries in the list all of + the properties will be sent. + + + + + An array of property values. + An item in this area is represented as a struct following + this format: + @li id unsigned the item id + @li properties map(string => variant) the requested item properties + + + + + + + Get a signal property on a single item. This is not useful if you're + going to implement this interface, it should only be used if you're + debugging via a commandline tool. + + + the id of the item which received the event + + + the name of the property to get + + + the value of the property + + + + + -" + ]]> + + the id of the item which received the event + + + the type of event + + + event-specific data + + + The time that the event occured if available or the time the message was sent if not + + + + + + Used to pass a set of events as a single message for possibily several + different menuitems. This is done to optimize DBus traffic. + + + + An array of all the events that should be passed. This tuple should + match the parameters of the 'Event' signal. Which is roughly: + id, eventID, data and timestamp. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + This is called by the applet to notify the application that it is about + to show the menu under the specified item. + + + + Which menu item represents the parent of the item about to be shown. + + + + + Whether this AboutToShow event should result in the menu being updated. + + + + + + + A function to tell several menus being shown that they are about to + be shown to the user. This is likely only useful for programitc purposes + so while the return values are returned, in general, the singular function + should be used in most user interacation scenarios. + + + + The IDs of the menu items who's submenus are being shown. + + + + + The IDs of the menus that need updates. Note: if no update information + is needed the DBus message should set the no reply flag. + + + + + I list of menuitem IDs that couldn't be found. If none of the ones + in the list can be found, a DBus error is returned. + + + + + + + + Triggered when there are lots of property updates across many items + so they all get grouped into a single dbus message. The format is + the ID of the item with a hashtable of names and values for those + properties. + + + + + + + Triggered by the application to notify display of a layout update, up to + revision + + + The revision of the layout that we're currently on + + + + If the layout update is only of a subtree, this is the + parent item for the entries that have changed. It is zero if + the whole layout should be considered invalid. + + + + + + The server is requesting that all clients displaying this + menu open it to the user. This would be for things like + hotkeys that when the user presses them the menu should + open and display itself to the user. + + + ID of the menu that should be activated + + + The time that the event occured + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml new file mode 100644 index 0000000000..7866a74639 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierItem.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml new file mode 100644 index 0000000000..2eb1a7a0b8 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/StatusNotifierWatcher.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml new file mode 100644 index 0000000000..42a71707b6 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/com.canonical.AppMenu.Registrar.xml @@ -0,0 +1,56 @@ + + + + + + An interface to register a menu from an application's window to be displayed in another + window.  This manages that association between XWindow Window IDs and the dbus + address and object that provides the menu using the dbusmenu dbus interface. + + + + + The XWindow ID of the window + + + The object on the dbus interface implementing the dbusmenu interface + + + + + A method to allow removing a window from the database. Windows will also be removed + when the client drops off DBus so this is not required. It is polite though. And + important for testing. + + + The XWindow ID of the window + + + + Gets the registered menu for a given window ID. + + The XWindow ID of the window to get + + + The address of the connection on DBus (e.g. :1.23 or org.example.service) + + + The path to the object which implements the com.canonical.dbusmenu interface. + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml new file mode 100644 index 0000000000..b30d94cebf --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml new file mode 100644 index 0000000000..6cb130d48a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputContext1.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml new file mode 100644 index 0000000000..b8d60f0d37 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml new file mode 100644 index 0000000000..0cc358a09a --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.fcitx.Fcitx.InputMethod1.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml new file mode 100644 index 0000000000..376ad424d4 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.IBus.Portal.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml new file mode 100644 index 0000000000..2ae3546955 --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.FileChooser.xml @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml new file mode 100644 index 0000000000..c1abb4eb7b --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Request.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + From 18737db8f4b0f486067069724e74b7b86b0bc8a7 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 9 Jan 2023 18:12:32 +0100 Subject: [PATCH 008/326] Use generated signal emit methods --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 33 +++++++------------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 33 ++++++-------------- src/tools/Tmds.DBus.SourceGenerator | 2 +- 3 files changed, 22 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 77f5048cd3..cb6def056f 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -2,8 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; -using Avalonia.Reactive; -using System.Threading.Tasks; +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -27,7 +26,6 @@ namespace Avalonia.FreeDesktop private class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable { - private readonly Connection _connection; private readonly Dictionary _idsToItems = new(); private readonly Dictionary _itemsToIds = new(); private readonly HashSet _menus = new(); @@ -42,7 +40,7 @@ namespace Avalonia.FreeDesktop public DBusMenuExporterImpl(Connection connection, IntPtr xid) { - _connection = connection; + Connection = connection; _xid = (uint)xid.ToInt32(); Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); @@ -51,13 +49,15 @@ namespace Avalonia.FreeDesktop public DBusMenuExporterImpl(Connection connection, string path) { - _connection = connection; + Connection = connection; _appMenu = false; Path = path; SetNativeMenu(new NativeMenu()); Init(); } + protected override Connection Connection { get; } + public override string Path { get; } protected override (uint revision, (int, Dictionary, object[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) @@ -79,12 +79,12 @@ namespace Avalonia.FreeDesktop protected override object OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? 0; protected override void OnEvent(int id, string eventId, object data, uint timestamp) => - Dispatcher.UIThread.Post(() => HandleEvent(id, eventId, data, timestamp)); + Dispatcher.UIThread.Post(() => HandleEvent(id, eventId)); protected override int[] OnEventGroup((int, string, object, uint)[] events) { foreach (var e in events) - Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2, e.Item3, e.Item4)); + Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2)); return Array.Empty(); } @@ -95,13 +95,13 @@ namespace Avalonia.FreeDesktop private async void Init() { - _connection.AddMethodHandler(this); + Connection.AddMethodHandler(this); if (!_appMenu) return; - var services = await _connection.ListServicesAsync(); + var services = await Connection.ListServicesAsync(); if (!services.Contains("com.canonical.AppMenu.Registrar")) return; - _registrar = new ComCanonicalAppMenuRegistrar(_connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); + _registrar = new ComCanonicalAppMenuRegistrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); if (!_disposed) await _registrar.RegisterWindowAsync(_xid, Path); // It's not really important if this code succeeds, @@ -155,7 +155,7 @@ namespace Avalonia.FreeDesktop _idsToItems.Clear(); _itemsToIds.Clear(); _revision++; - EmitUIntIntSignal("LayoutUpdated", _revision, 0); + EmitLayoutUpdated(_revision, 0); } private void QueueReset() @@ -318,7 +318,7 @@ namespace Avalonia.FreeDesktop } - private void HandleEvent(int id, string eventId, object data, uint timestamp) + private void HandleEvent(int id, string eventId) { if (eventId == "clicked") { @@ -327,15 +327,6 @@ namespace Avalonia.FreeDesktop bridge.RaiseClicked(); } } - - private void EmitUIntIntSignal(string member, uint arg0, int arg1) - { - using var writer = _connection.GetMessageWriter(); - writer.WriteSignalHeader(null, Path, "com.canonical.dbusmenu", member, "ui"); - writer.WriteUInt32(arg0); - writer.WriteInt32(arg1); - _connection.TrySendMessage(writer.CreateMessage()); - } } } } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 4e9c495665..16b1a1d686 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -211,11 +211,9 @@ namespace Avalonia.FreeDesktop /// internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem { - private readonly Connection _connection; - public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) { - _connection = connection; + Connection = connection; BackingProperties.Menu = dbusMenuPath; BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); BackingProperties.IconName = string.Empty; @@ -228,6 +226,8 @@ namespace Avalonia.FreeDesktop InvalidateAll(); } + protected override Connection Connection { get; } + public override string Path => "/StatusNotifierItem"; public event Action? ActivationDelegate; @@ -242,12 +242,12 @@ namespace Avalonia.FreeDesktop public void InvalidateAll() { - EmitVoidSignal("NewTitle"); - EmitVoidSignal("NewIcon"); - EmitVoidSignal("NewAttentionIcon"); - EmitVoidSignal("NewOverlayIcon"); - EmitVoidSignal("NewToolTip"); - EmitStringSignal("NewStatus", BackingProperties.Status); + EmitNewTitle(); + EmitNewIcon(); + EmitNewAttentionIcon(); + EmitNewOverlayIcon(); + EmitNewToolTip(); + EmitNewStatus(BackingProperties.Status); } public void SetIcon((int, int, byte[]) dbusPixmap) @@ -268,20 +268,5 @@ namespace Avalonia.FreeDesktop BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } - - private void EmitVoidSignal(string member) - { - using var writer = _connection.GetMessageWriter(); - writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member); - _connection.TrySendMessage(writer.CreateMessage()); - } - - private void EmitStringSignal(string member, string value) - { - using var writer = _connection.GetMessageWriter(); - writer.WriteSignalHeader(null, Path, "org.kde.StatusNotifierItem", member, "s"); - writer.WriteString(value); - _connection.TrySendMessage(writer.CreateMessage()); - } } } diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator index 1cf4faba30..6007f27d04 160000 --- a/src/tools/Tmds.DBus.SourceGenerator +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -1 +1 @@ -Subproject commit 1cf4faba30741f799a33313a2842cf70eeb6c67e +Subproject commit 6007f27d04691f7c4c35722df3dc65bf4b558f18 From b570bfa1a9f0fad0af88f109788a8af05256f040 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Mon, 9 Jan 2023 23:12:09 +0100 Subject: [PATCH 009/326] Fix ObjectDisposedException on Dispose --- .../DBusIme/DBusTextInputMethodBase.cs | 7 +++++-- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 8 +++++++- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 6 ++++++ src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 1 - 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 08d2a0c219..9fd58402c6 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -60,7 +60,7 @@ namespace Avalonia.FreeDesktop.DBusIme { foreach (var name in _knownNames) { - var dbus = new OrgFreedesktopDBus(Connection, name, "/org/freedesktop/DBus"); + var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); var nameOwner = await dbus.GetNameOwnerAsync(name); OnNameChange(null, (name, null, nameOwner)); @@ -74,6 +74,9 @@ namespace Avalonia.FreeDesktop.DBusIme private async void OnNameChange(Exception? e, (string ServiceName, string? OldOwner, string? NewOwner) args) { + if (e is not null) + return; + if (args.NewOwner is not null && _currentName is null) { _onlineNamesQueue.Enqueue(args.ServiceName); @@ -162,7 +165,7 @@ namespace Avalonia.FreeDesktop.DBusIme protected void AddDisposable(IDisposable? d) { - if(d is { }) + if (d is { }) _disposables.Add(d); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index 6b30c5424a..be88b94707 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -134,6 +134,12 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx }); } - private void OnCommitString(Exception? e, string s) => FireCommit(s); + private void OnCommitString(Exception? e, string s) + { + if (e is not null) + return; + + FireCommit(s); + } } } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 4db0d95d3c..57bb10a885 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -28,6 +28,9 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus private void OnForwardKey(Exception? e, (uint keyval, uint keycode, uint state) k) { + if (e is not null) + return; + var state = (IBusModifierMask)k.state; KeyModifiers mods = default; if (state.HasAllFlags(IBusModifierMask.ControlMask)) @@ -48,6 +51,9 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus private void OnCommitText(Exception? e, object wtf) { + if (e is not null) + return; + // Hello darkness, my old friend if (wtf.GetType().GetField("Item3") is { } prop) { diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 16b1a1d686..a34a9a0a84 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using Avalonia.Reactive; using System.Linq; using Avalonia.Controls.Platform; using Avalonia.Logging; From 37545cbeb1a07c9fbea4e86912686b0d48717e6a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 12 Jan 2023 00:35:03 -0500 Subject: [PATCH 010/326] IStorageProvider API updates --- samples/ControlCatalog/Pages/DialogsPage.xaml | 17 ++- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 59 +++++++-- .../Avalonia.Android/AvaloniaMainActivity.cs | 9 ++ .../IActivityResultHandler.cs | 3 + .../Platform/PlatformSupport.cs | 52 ++++++++ .../Platform/Storage/AndroidStorageItem.cs | 101 +++++++++----- .../Storage/AndroidStorageProvider.cs | 124 ++++++++++++++++-- .../Platform/Storage/FileIO/BclStorageFile.cs | 71 +++++----- .../Storage/FileIO/BclStorageFolder.cs | 63 ++++----- .../Storage/FileIO/BclStorageProvider.cs | 103 ++++++++++++++- .../Storage/FileIO/StorageProviderHelpers.cs | 18 ++- .../Platform/Storage/IStorageItem.cs | 4 +- .../Platform/Storage/IStorageProvider.cs | 35 ++++- .../Platform/Storage/NameCollisionOption.cs | 6 + .../Storage/StorageProviderExtensions.cs | 55 ++++++++ .../Platform/Storage/WellKnownFolder.cs | 37 ++++++ .../Platform/Dialogs/SystemDialogImpl.cs | 11 +- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 5 + .../Internal/ManagedFileChooserViewModel.cs | 12 +- .../ManagedFileDialogExtensions.cs | 4 +- .../ManagedStorageProvider.cs | 2 +- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 6 +- src/Avalonia.Native/SystemDialogs.cs | 9 +- .../NativeDialogs/CompositeStorageProvider.cs | 18 +++ .../NativeDialogs/GtkNativeFileDialogs.cs | 10 +- .../Avalonia.Browser/Interop/StorageHelper.cs | 3 + .../Storage/BrowserStorageProvider.cs | 34 ++++- .../webapp/modules/storage/storageItem.ts | 34 ++++- .../webapp/modules/storage/storageProvider.ts | 6 +- .../Avalonia.Win32/Win32StorageProvider.cs | 4 +- .../Avalonia.iOS/Storage/IOSStorageItem.cs | 41 +++--- .../Storage/IOSStorageProvider.cs | 50 +++++-- .../Utilities/UriExtensionsTests.cs | 15 +++ 33 files changed, 804 insertions(+), 217 deletions(-) create mode 100644 src/Android/Avalonia.Android/Platform/PlatformSupport.cs create mode 100644 src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs create mode 100644 src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs create mode 100644 src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index cc23ef796a..53c0a2c547 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -1,6 +1,8 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:storage="clr-namespace:Avalonia.Platform.Storage;assembly=Avalonia.Base" + xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections"> @@ -42,6 +44,19 @@ + + + + Desktop + Documents + Downloads + Pictures + Videos + Music + + + + ("PickerLastResults"); var resultsVisible = this.Get("PickerLastResultsVisible"); var bookmarkContainer = this.Get("BookmarkContainer"); var openedFileContent = this.Get("OpenedFileContent"); var openMultiple = this.Get("OpenMultiple"); + var currentFolderBox = this.Get("CurrentFolderBox"); + + currentFolderBox.TextChanged += async (sender, args) => + { + if (ignoreTextChanged) return; + + if (Enum.TryParse(currentFolderBox.Text, true, out var folderEnum)) + { + lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum); + } + else + { + if (!Uri.TryCreate(currentFolderBox.Text, UriKind.Absolute, out var folderLink)) + { + Uri.TryCreate("file://" + currentFolderBox.Text, UriKind.Absolute, out folderLink); + } + + if (folderLink is not null) + { + lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink); + } + } + }; - IStorageFolder? lastSelectedDirectory = null; List GetFilters() { @@ -84,7 +109,7 @@ namespace ControlCatalog.Pages { Title = "Open multiple files", Filters = GetFilters(), - Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, + Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null, AllowMultiple = true }.ShowAsync(GetWindow()); results.Items = result; @@ -97,7 +122,7 @@ namespace ControlCatalog.Pages { Title = "Save file", Filters = filters, - Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, + Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null, DefaultExtension = filters?.Any() == true ? "txt" : null, InitialFileName = "test.txt" }.ShowAsync(GetWindow()); @@ -109,7 +134,7 @@ namespace ControlCatalog.Pages var result = await new OpenFolderDialog() { Title = "Select folder", - Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null + Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null, }.ShowAsync(GetWindow()); if (string.IsNullOrEmpty(result)) { @@ -117,7 +142,7 @@ namespace ControlCatalog.Pages } else { - lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result)); + SetFolder(await GetStorageProvider().TryGetFolderFromPath(result)); results.Items = new[] { result }; resultsVisible.IsVisible = true; } @@ -127,7 +152,7 @@ namespace ControlCatalog.Pages var result = await new OpenFileDialog() { Title = "Select both", - Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, + Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null, AllowMultiple = true }.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions { @@ -210,7 +235,7 @@ namespace ControlCatalog.Pages #endif await reader.WriteLineAsync(openedFileContent.Text); - lastSelectedDirectory = await file.GetParentAsync(); + SetFolder(await file.GetParentAsync()); } await SetPickerResult(file is null ? null : new [] {file}); @@ -226,7 +251,7 @@ namespace ControlCatalog.Pages await SetPickerResult(folders); - lastSelectedDirectory = folders.FirstOrDefault(); + SetFolder(folders.FirstOrDefault()); }; this.Get +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var windowResources = (ResourceDictionary)window.Resources; + var buttonResources = (ResourceDictionary)((Button)window.Content!).Resources; + + var brush = Assert.IsType(windowResources["Red2"]); + Assert.Equal(Colors.Red, brush.Color); + + Assert.False(windowResources.ContainsDeferredKey("Red")); + Assert.False(windowResources.ContainsDeferredKey("Red2")); + + Assert.True(buttonResources.ContainsDeferredKey("Red")); + } + } + private IDisposable StyledWindow(params (string, string)[] assets) { var services = TestServices.StyledWindow.With( diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 40306a4513..339cb1462c 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -155,7 +155,7 @@ namespace Avalonia.UnitTests private static IStyle CreateSimpleTheme() { - return new SimpleTheme { Mode = SimpleThemeMode.Light }; + return new SimpleTheme(); } private static IPlatformRenderInterface CreateRenderInterfaceMock() From 151fe0031a31c34acbd50600bba03a916a5bf20b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:13:01 -0500 Subject: [PATCH 030/326] Update control catalog and samples --- samples/ControlCatalog/App.xaml | 24 +++++- samples/ControlCatalog/App.xaml.cs | 48 ++--------- samples/ControlCatalog/MainView.xaml | 19 ++++- samples/ControlCatalog/MainView.xaml.cs | 39 ++++----- samples/ControlCatalog/Models/CatalogTheme.cs | 6 +- .../Pages/DateTimePickerPage.xaml | 30 +++---- .../ControlCatalog/Pages/FlyoutsPage.axaml | 22 +++--- .../Pages/ItemsRepeaterPage.xaml | 2 +- .../ControlCatalog/Pages/SplitViewPage.xaml | 16 ++-- .../ControlCatalog/Pages/TextBlockPage.xaml | 2 +- samples/ControlCatalog/Pages/ThemePage.axaml | 79 +++++++++++++++++++ .../ControlCatalog/Pages/ThemePage.axaml.cs | 37 +++++++++ samples/IntegrationTestApp/App.axaml | 2 +- samples/PlatformSanityChecks/App.xaml | 2 +- samples/Previewer/App.xaml | 2 +- .../HamburgerMenu/HamburgerMenu.xaml | 34 +++++--- samples/Sandbox/App.axaml | 2 +- 17 files changed, 239 insertions(+), 127 deletions(-) create mode 100644 samples/ControlCatalog/Pages/ThemePage.axaml create mode 100644 samples/ControlCatalog/Pages/ThemePage.axaml.cs diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 8f32fa01dd..3b847adcbb 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -6,18 +6,34 @@ x:Class="ControlCatalog.App"> + + + + + #33000000 + #99000000 + #FFE6E6E6 + #FF000000 + + + #33FFFFFF + #99FFFFFF + #FF1F1F1F + #FFFFFFFF + + + #FF0078D7 + #FF005A9E + + - - - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6c99eb5289..d71d51f068 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -16,7 +16,6 @@ namespace ControlCatalog private readonly Styles _themeStylesContainer = new(); private FluentTheme? _fluentTheme; private SimpleTheme? _simpleTheme; - private IResourceDictionary? _fluentBaseLightColors, _fluentBaseDarkColors; private IStyle? _colorPickerFluent, _colorPickerSimple; private IStyle? _dataGridFluent, _dataGridSimple; @@ -33,16 +32,12 @@ namespace ControlCatalog _fluentTheme = new FluentTheme(); _simpleTheme = new SimpleTheme(); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentAccentColors"]!); - _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentBaseColors"]!); _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!; _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!; _dataGridFluent = (IStyle)Resources["DataGridFluent"]!; _dataGridSimple = (IStyle)Resources["DataGridSimple"]!; - _fluentBaseLightColors = (IResourceDictionary)Resources["FluentBaseLightColors"]!; - _fluentBaseDarkColors = (IResourceDictionary)Resources["FluentBaseDarkColors"]!; - SetThemeVariant(CatalogTheme.FluentLight); + SetCatalogThemes(CatalogTheme.Fluent); } public override void OnFrameworkInitializationCompleted() @@ -61,19 +56,12 @@ namespace ControlCatalog private CatalogTheme _prevTheme; public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme; - public static void SetThemeVariant(CatalogTheme theme) + public static void SetCatalogThemes(CatalogTheme theme) { var app = (App)Current!; var prevTheme = app._prevTheme; app._prevTheme = theme; - var shouldReopenWindow = theme switch - { - CatalogTheme.FluentLight => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.FluentDark => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight, - CatalogTheme.SimpleLight => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - CatalogTheme.SimpleDark => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight, - _ => throw new ArgumentOutOfRangeException(nameof(theme), theme, null) - }; + var shouldReopenWindow = prevTheme != theme; if (app._themeStylesContainer.Count == 0) { @@ -81,36 +69,16 @@ namespace ControlCatalog app._themeStylesContainer.Add(new Style()); app._themeStylesContainer.Add(new Style()); } - - if (theme == CatalogTheme.FluentLight) - { - app._fluentTheme!.Mode = FluentThemeMode.Light; - app._themeStylesContainer[0] = app._fluentTheme; - app._themeStylesContainer[1] = app._colorPickerFluent!; - app._themeStylesContainer[2] = app._dataGridFluent!; - } - else if (theme == CatalogTheme.FluentDark) + + if (theme == CatalogTheme.Fluent) { - app._fluentTheme!.Mode = FluentThemeMode.Dark; - app._themeStylesContainer[0] = app._fluentTheme; + app._themeStylesContainer[0] = app._fluentTheme!; app._themeStylesContainer[1] = app._colorPickerFluent!; app._themeStylesContainer[2] = app._dataGridFluent!; } - else if (theme == CatalogTheme.SimpleLight) - { - app._simpleTheme!.Mode = SimpleThemeMode.Light; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseDarkColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseLightColors!); - app._themeStylesContainer[0] = app._simpleTheme; - app._themeStylesContainer[1] = app._colorPickerSimple!; - app._themeStylesContainer[2] = app._dataGridSimple!; - } - else if (theme == CatalogTheme.SimpleDark) + else if (theme == CatalogTheme.Simple) { - app._simpleTheme!.Mode = SimpleThemeMode.Dark; - app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseLightColors!); - app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseDarkColors!); - app._themeStylesContainer[0] = app._simpleTheme; + app._themeStylesContainer[0] = app._simpleTheme!; app._themeStylesContainer[1] = app._colorPickerSimple!; app._themeStylesContainer[2] = app._dataGridSimple!; } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 166b98436e..4eb73632c1 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -165,6 +165,9 @@ + + + @@ -198,14 +201,22 @@ Full + + + Default + Light + Dark + + - FluentLight - FluentDark - SimpleLight - SimpleDark + Fluent + Simple PlatformThemeVariant.Light, - CatalogTheme.FluentDark => PlatformThemeVariant.Dark, - CatalogTheme.SimpleLight => PlatformThemeVariant.Light, - CatalogTheme.SimpleDark => PlatformThemeVariant.Dark, - _ => throw new ArgumentOutOfRangeException() - }); + App.SetCatalogThemes(theme); + } + }; + var themeVariants = this.Get("ThemeVariants"); + themeVariants.SelectedItem = Application.Current!.RequestedThemeVariant; + themeVariants.SelectionChanged += (sender, e) => + { + if (themeVariants.SelectedItem is ThemeVariant themeVariant) + { + Application.Current!.RequestedThemeVariant = themeVariant; } }; @@ -118,25 +119,13 @@ namespace ControlCatalog private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) { - var themes = this.Get("Themes"); - var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight; - var newTheme = (currentTheme, e.ThemeVariant) switch - { - (CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight, - (CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark, - (CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight, - (CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark, - _ => currentTheme - }; - themes.SelectedItem = newTheme; - Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1; Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); - Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3); - Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5); - Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7); + Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3); + Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5); + Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7); static Color ChangeColorLuminosity(Color color, double luminosityFactor) { diff --git a/samples/ControlCatalog/Models/CatalogTheme.cs b/samples/ControlCatalog/Models/CatalogTheme.cs index 37224ed26e..79b3182d20 100644 --- a/samples/ControlCatalog/Models/CatalogTheme.cs +++ b/samples/ControlCatalog/Models/CatalogTheme.cs @@ -2,9 +2,7 @@ { public enum CatalogTheme { - FluentLight, - FluentDark, - SimpleLight, - SimpleDark + Fluent, + Simple } } diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml index 47753f56b6..fc3ad9b895 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -15,11 +15,11 @@ Spacing="16"> A simple DatePicker - - + @@ -31,7 +31,7 @@ - @@ -42,12 +42,12 @@ A DatePicker with day formatted and year hidden. - - + @@ -58,15 +58,15 @@ - + A simple TimePicker. - - + @@ -77,7 +77,7 @@ - @@ -88,11 +88,11 @@ A TimePicker with minute increments specified. - - + @@ -105,11 +105,11 @@ A TimePicker using a 12-hour clock. - - + @@ -122,11 +122,11 @@ A TimePicker using a 24-hour clock. - - + diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index c4d0bc3e67..54aa9d1b67 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -26,31 +26,31 @@ - - + diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml index 61bfb490b8..2edd895349 100644 --- a/samples/ControlCatalog/Pages/SplitViewPage.xaml +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml @@ -32,7 +32,7 @@ - SystemControlBackgroundChromeMediumLowBrush + CatalogChromeMediumColor Red Blue Green @@ -48,7 +48,7 @@ - - + @@ -89,11 +89,11 @@ - - - - - + + + + + diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 6bb428e2c7..6511e2136a 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -9,7 +9,7 @@ @@ -101,7 +115,7 @@ VerticalAlignment="Center" Background="{DynamicResource TabItemHeaderSelectedPipeFill}" IsVisible="False" - CornerRadius="{DynamicResource ControlCornerRadius}"/> + CornerRadius="4"/> @@ -136,18 +150,18 @@ - - - + + + diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml index f601f9f78f..cf3e5e445a 100644 --- a/samples/Sandbox/App.axaml +++ b/samples/Sandbox/App.axaml @@ -3,6 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="Sandbox.App"> - + From a0d22499cd12614daf2667b410fef62e00b7d747 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:39:11 -0500 Subject: [PATCH 031/326] Fix benchmarks build --- .../Controls/ResourceDictionary.cs | 2 +- .../Themes/ThemeBenchmark.cs | 22 +++++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 85e4487ba9..5123803f6e 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { IResourceProvider? themeResourceProvider; - if (theme is not null) + if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) && themeResourceProvider.TryGetResource(key, theme, out value)) diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 70636d1fe6..7c0a3f8bdf 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -29,26 +29,16 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] - [Arguments(FluentThemeMode.Dark)] - [Arguments(FluentThemeMode.Light)] - public bool InitFluentTheme(FluentThemeMode mode) + public bool InitFluentTheme() { - UnitTestApplication.Current.Styles[0] = new FluentTheme() - { - Mode = mode - }; + UnitTestApplication.Current.Styles[0] = new FluentTheme(); return ((IResourceHost)UnitTestApplication.Current).TryGetResource("SystemAccentColor", out _); } [Benchmark] - [Arguments(SimpleThemeMode.Dark)] - [Arguments(SimpleThemeMode.Light)] - public bool InitSimpleTheme(SimpleThemeMode mode) + public bool InitSimpleTheme() { - UnitTestApplication.Current.Styles[0] = new SimpleTheme() - { - Mode = mode - }; + UnitTestApplication.Current.Styles[0] = new SimpleTheme(); return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } @@ -58,7 +48,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(typeof(DatePicker))] public object FindFluentControlTheme(Type type) { - _reusableFluentTheme.TryGetResource(type, out var theme); + _reusableFluentTheme.TryGetResource(type, ThemeVariant.Default, out var theme); return theme; } @@ -68,7 +58,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(typeof(DatePicker))] public object FindSimpleControlTheme(Type type) { - _reusableSimpleTheme.TryGetResource(type, out var theme); + _reusableSimpleTheme.TryGetResource(type, ThemeVariant.Default, out var theme); return theme; } From de325add060f7dc42c813ff8d63b3affcb9def7c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 02:40:05 -0500 Subject: [PATCH 032/326] Fix code related warnings --- src/Avalonia.Base/StyledElement.cs | 2 +- src/Avalonia.Controls/Application.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index d23c585299..5bf022cd51 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -81,7 +81,7 @@ namespace Avalonia defaultValue: ThemeVariant.Light); /// - /// Defines the property. + /// Defines the RequestedThemeVariant property. /// public static readonly StyledProperty RequestedThemeVariantProperty = AvaloniaProperty.Register( diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 58cc02e8c5..3dcba4ded9 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -95,7 +95,7 @@ namespace Avalonia set => SetValue(RequestedThemeVariantProperty, value); } - /// + /// public ThemeVariant ActualThemeVariant { get => GetValue(ActualThemeVariantProperty); From 7788a29a966e6300af857015c59e5e2edae96532 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Nov 2022 14:17:56 +0100 Subject: [PATCH 033/326] Make BindingExpression and ExpressionObserver internal. --- src/Avalonia.Base/Avalonia.Base.csproj | 3 +++ src/Avalonia.Base/Data/Core/BindingExpression.cs | 2 +- src/Avalonia.Base/Data/Core/ExpressionObserver.cs | 2 +- .../MarkupExtensions/CompiledBindingExtension.cs | 2 +- src/Markup/Avalonia.Markup/Avalonia.Markup.csproj | 1 + src/Markup/Avalonia.Markup/Data/Binding.cs | 2 +- src/Markup/Avalonia.Markup/Data/BindingBase.cs | 12 ++++++------ .../Markup/Parsers/ExpressionObserverBuilder.cs | 2 +- 8 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index cd122a8b67..4a67191132 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -47,6 +47,9 @@ + + + diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 55caf8070e..79942cb9ce 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -13,7 +13,7 @@ namespace Avalonia.Data.Core /// that are sent and received. /// [RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] - public class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription + internal class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 0818b5fa62..ce3549c4ad 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : LightweightObservableBase, IDescription + internal class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 9990ad4731..d0a4de09ab 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions }; } - protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + private protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { if (Source != null) { diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index e3878b5bc6..ec44eeb38f 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 66907f33d0..37e7be5e3f 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -56,7 +56,7 @@ namespace Avalonia.Data /// public Func? TypeResolver { get; set; } - protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, bool enableDataValidation) + private protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, bool enableDataValidation) { _ = target ?? throw new ArgumentNullException(nameof(target)); diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 90f312a249..8c8c50763b 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -70,7 +70,7 @@ namespace Avalonia.Data public WeakReference? NameScope { get; set; } - protected abstract ExpressionObserver CreateExpressionObserver( + private protected abstract ExpressionObserver CreateExpressionObserver( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, @@ -127,7 +127,7 @@ namespace Avalonia.Data return new InstancedBinding(subject, Mode, Priority); } - protected ExpressionObserver CreateDataContextObserver( + private protected ExpressionObserver CreateDataContextObserver( AvaloniaObject target, ExpressionNode node, bool targetIsDataContext, @@ -162,7 +162,7 @@ namespace Avalonia.Data } } - protected ExpressionObserver CreateElementObserver( + private protected ExpressionObserver CreateElementObserver( StyledElement target, string elementName, ExpressionNode node) @@ -178,7 +178,7 @@ namespace Avalonia.Data return result; } - protected ExpressionObserver CreateFindAncestorObserver( + private protected ExpressionObserver CreateFindAncestorObserver( StyledElement target, RelativeSource relativeSource, ExpressionNode node) @@ -211,7 +211,7 @@ namespace Avalonia.Data null); } - protected ExpressionObserver CreateSourceObserver( + private protected ExpressionObserver CreateSourceObserver( object source, ExpressionNode node) { @@ -220,7 +220,7 @@ namespace Avalonia.Data return new ExpressionObserver(source, node); } - protected ExpressionObserver CreateTemplatedParentObserver( + private protected ExpressionObserver CreateTemplatedParentObserver( AvaloniaObject target, ExpressionNode node) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 00f40dfcd3..97e9698c13 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -6,7 +6,7 @@ using Avalonia.Utilities; namespace Avalonia.Markup.Parsers { - public static class ExpressionObserverBuilder + internal static class ExpressionObserverBuilder { [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func? typeResolver = null, From cb8a21fb836aeab380541c07b58845005a598b11 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Nov 2022 14:51:13 +0100 Subject: [PATCH 034/326] Make ExpressionNode (and derived) internal. --- src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs | 2 +- src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs | 2 +- src/Avalonia.Base/Data/Core/ExpressionNode.cs | 2 +- src/Avalonia.Base/Data/Core/IndexerNodeBase.cs | 2 +- src/Avalonia.Base/Data/Core/LogicalNotNode.cs | 2 +- src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs | 2 +- src/Avalonia.Base/Data/Core/SettableNode.cs | 2 +- src/Avalonia.Base/Data/Core/StreamNode.cs | 2 +- src/Avalonia.Base/Data/Core/TypeCastNode.cs | 2 +- .../MarkupExtensions/CompiledBindings/CompiledBindingPath.cs | 2 +- .../MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs | 2 +- src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 536c14dcf9..92fc843394 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -3,7 +3,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { - public class AvaloniaPropertyAccessorNode : SettableNode + internal class AvaloniaPropertyAccessorNode : SettableNode { private IDisposable? _subscription; private readonly bool _enableValidation; diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 4e142fbee9..b333fa9047 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -1,6 +1,6 @@ namespace Avalonia.Data.Core { - public class EmptyExpressionNode : ExpressionNode + internal class EmptyExpressionNode : ExpressionNode { public override string Description => "."; } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 5fb2bb5c13..30fc71cfb4 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Data.Core { - public abstract class ExpressionNode + internal abstract class ExpressionNode { protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 2fad96701d..9ec256225b 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -7,7 +7,7 @@ using Avalonia.Utilities; namespace Avalonia.Data.Core { - public abstract class IndexerNodeBase : SettableNode, + internal abstract class IndexerNodeBase : SettableNode, IWeakEventSubscriber, IWeakEventSubscriber { diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 45837db73d..81b07bfe85 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -3,7 +3,7 @@ using System.Globalization; namespace Avalonia.Data.Core { - public class LogicalNotNode : ExpressionNode, ITransformNode + internal class LogicalNotNode : ExpressionNode, ITransformNode { public override string Description => "!"; diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 1b79fed6e7..3898d232ec 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -5,7 +5,7 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class PropertyAccessorNode : SettableNode + internal class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessorPlugin? _customPlugin; diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 9ad9ace814..4980e4487a 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Data.Core { - public abstract class SettableNode : ExpressionNode + internal abstract class SettableNode : ExpressionNode { public bool SetTargetValue(object? value, BindingPriority priority) { diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index ba18a2173b..d3da6414ac 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -6,7 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class StreamNode : ExpressionNode + internal class StreamNode : ExpressionNode { private IStreamPlugin? _customPlugin = null; private IDisposable? _subscription; diff --git a/src/Avalonia.Base/Data/Core/TypeCastNode.cs b/src/Avalonia.Base/Data/Core/TypeCastNode.cs index 3a2ca955fa..655bfbc7a1 100644 --- a/src/Avalonia.Base/Data/Core/TypeCastNode.cs +++ b/src/Avalonia.Base/Data/Core/TypeCastNode.cs @@ -4,7 +4,7 @@ using System.Text; namespace Avalonia.Data.Core { - public class TypeCastNode : ExpressionNode + internal class TypeCastNode : ExpressionNode { public override string Description => $"as {TargetType.FullName}"; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index b7f2261324..2b62d33349 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.CompiledBindingSafeSupressWarningMessage)] - public ExpressionNode BuildExpression(bool enableValidation) + internal ExpressionNode BuildExpression(bool enableValidation) { ExpressionNode pathRoot = null; ExpressionNode path = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs index 1252ec7eca..b543fd9c01 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs @@ -3,7 +3,7 @@ using Avalonia.Data.Core; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { - public class StrongTypeCastNode : TypeCastNode + internal class StrongTypeCastNode : TypeCastNode { private Func _cast; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 4fc17e440b..e676c74879 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -6,7 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { - public class ElementNameNode : ExpressionNode + internal class ElementNameNode : ExpressionNode { private readonly WeakReference _nameScope; private readonly string _name; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index ffbd34d492..383a160814 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -5,7 +5,7 @@ using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { - public class FindAncestorNode : ExpressionNode + internal class FindAncestorNode : ExpressionNode { private readonly int _level; private readonly Type? _ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs index 1cd233c68a..2cb87efa65 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - public class SelfNode : ExpressionNode + internal class SelfNode : ExpressionNode { public override string Description => "$self"; } From 67c9221d3cf550bb17b2ad248bb7bf8b3acda858 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Dec 2022 15:21:24 +0100 Subject: [PATCH 035/326] Tweaked InstancedBinding API. - Remove `Value` from the API, will always contain an `IObservable` from now - Remove subject from the API, caller can try to cast the observable itself --- src/Avalonia.Base/Data/BindingOperations.cs | 59 ++++++++----------- src/Avalonia.Base/Data/InstancedBinding.cs | 43 +++++++------- src/Avalonia.Base/Styling/Setter.cs | 2 +- .../DataGridBoundColumn.cs | 5 +- .../Avalonia.Markup/Data/MultiBinding.cs | 4 +- .../Data/BindingTests.cs | 6 +- .../Data/BindingTests_Converters.cs | 8 +-- .../Data/BindingTests_DataValidation.cs | 6 +- .../Data/MultiBindingTests.cs | 4 +- 9 files changed, 63 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 0b737dd959..e53ae40cb1 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -41,54 +41,41 @@ namespace Avalonia.Data { case BindingMode.Default: case BindingMode.OneWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - return target.Bind(property, binding.Observable, binding.Priority); + return target.Bind(property, binding.Source, binding.Priority); case BindingMode.TwoWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + { + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); return new TwoWayBindingDisposable( - target.Bind(property, binding.Observable, binding.Priority), - target.GetObservable(property).Subscribe(binding.Subject)); + target.Bind(property, binding.Source, binding.Priority), + target.GetObservable(property).Subscribe(observer)); + } case BindingMode.OneTime: - if (binding.Observable is {} source) - { - // Perf: Avoid allocating closure in the outer scope. - var targetCopy = target; - var propertyCopy = property; - var bindingCopy = binding; - - return source - .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) - .Take(1) - .Subscribe(x => targetCopy.SetValue( - propertyCopy, - BindingNotification.ExtractValue(x), - bindingCopy.Priority)); - } - else - { - target.SetValue(property, binding.Value, binding.Priority); - return Disposable.Empty; - } + { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + + return binding.Source + .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) + .Take(1) + .Subscribe(x => targetCopy.SetValue( + propertyCopy, + BindingNotification.ExtractValue(x), + bindingCopy.Priority)); + } case BindingMode.OneWayToSource: { - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); - // Perf: Avoid allocating closure in the outer scope. - var bindingCopy = binding; - return Observable.CombineLatest( - binding.Observable, + binding.Source, target.GetObservable(property), (_, v) => v) - .Subscribe(x => bindingCopy.Subject.OnNext(x)); + .Subscribe(x => observer.OnNext(x)); } default: diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index a60c1d72ec..00e5c3d8e6 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Reactive; +using ObservableEx = Avalonia.Reactive.Observable; namespace Avalonia.Data { @@ -14,11 +15,23 @@ namespace Avalonia.Data /// public class InstancedBinding { - internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority) + /// + /// Initializes a new instance of the class. + /// + /// The binding source. + /// The binding mode. + /// The priority of the binding. + /// + /// This constructor can be used to create any type of binding and as such requires an + /// as the binding source because this is the only binding + /// source which can be used for all binding modes. If you wish to create an instance with + /// something other than a subject, use one of the static creation methods on this class. + /// + internal InstancedBinding(IObservable source, BindingMode mode, BindingPriority priority) { Mode = mode; Priority = priority; - Value = value; + Source = source ?? throw new ArgumentNullException(nameof(source)); } /// @@ -32,24 +45,12 @@ namespace Avalonia.Data public BindingPriority Priority { get; } /// - /// Gets the value or source of the binding. - /// - public object? Value { get; } - - /// - /// Gets the as an observable. + /// Gets the binding source observable. /// - public IObservable? Observable => Value as IObservable; + public IObservable Source { get; } - /// - /// Gets the as an observer. - /// - public IObserver? Observer => Value as IObserver; - - /// - /// Gets the as an subject. - /// - internal IAvaloniaSubject? Subject => Value as IAvaloniaSubject; + [Obsolete("Use Source property")] + public IObservable Observable => Source; /// /// Creates a new one-time binding with a fixed value. @@ -61,7 +62,7 @@ namespace Avalonia.Data object value, BindingPriority priority = BindingPriority.LocalValue) { - return new InstancedBinding(value, BindingMode.OneTime, priority); + return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority); } /// @@ -106,7 +107,7 @@ namespace Avalonia.Data { _ = observer ?? throw new ArgumentNullException(nameof(observer)); - return new InstancedBinding(observer, BindingMode.OneWayToSource, priority); + return new InstancedBinding((IObservable)observer, BindingMode.OneWayToSource, priority); } /// @@ -135,7 +136,7 @@ namespace Avalonia.Data /// An instance. public InstancedBinding WithPriority(BindingPriority priority) { - return new InstancedBinding(Value, Mode, priority); + return new InstancedBinding(Source, Mode, priority); } } } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index b7b44a7dfe..093597c6a0 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -109,7 +109,7 @@ namespace Avalonia.Styling if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) { - return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!); + return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source); } throw new NotSupportedException(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index e859a6e725..8f532b9803 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -7,6 +7,7 @@ using Avalonia.Data; using System; using Avalonia.Controls.Utils; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -111,9 +112,9 @@ namespace Avalonia.Controls if (result != null) { - if(result.Subject != null) + if(result.Source is IAvaloniaSubject subject) { - var bindingHelper = new CellEditBinding(result.Subject); + var bindingHelper = new CellEditBinding(subject); var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority); BindingOperations.Apply(target, property, instanceBinding, null); diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 1515ff2c90..993f63b4d3 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -85,8 +85,8 @@ namespace Avalonia.Data var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x?.Observable!) - .Where(x => x is not null) + var input = children.Select(x => x?.Source) + .Where(x => x is not null)! .CombineLatest() .Select(x => ConvertValue(x, targetType, converter)) .Where(x => x != BindingOperations.DoNothing); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 656c2cbbbc..3ba8e8354d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -334,7 +334,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.IsType(((BindingExpression)result).Converter); } @@ -350,7 +350,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.Same(converter.Object, ((BindingExpression)result).Converter); } @@ -367,7 +367,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Bar", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.Same("foo", ((BindingExpression)result).ConverterParameter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs index 2a0750b131..680c49d098 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TextProperty).Observable; + TextBlock.TextProperty).Source; Assert.Same(StringConverters.IsNullOrEmpty, expressionObserver.Converter); } @@ -46,7 +46,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TextProperty).Observable; + TextBlock.TextProperty).Source; Assert.IsType(expressionObserver.Converter); } @@ -69,7 +69,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TagProperty).Observable; + TextBlock.TagProperty).Source; Assert.IsType(expressionObserver.Converter); } @@ -92,7 +92,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.MarginProperty).Observable; + TextBlock.MarginProperty).Source; Assert.Same(DefaultValueConverter.Instance, expressionObserver.Converter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs index 45deb97f51..505eddb146 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); @@ -56,7 +56,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template }; var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index a7ef2c4e4d..bf9631760a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.UnitTests.Data }; var target = new Control { DataContext = source }; - var observable = binding.Initiate(target, null).Observable; + var observable = binding.Initiate(target, null).Source; var result = await observable.Take(1); Assert.Equal("1,2,3", result); @@ -59,7 +59,7 @@ namespace Avalonia.Markup.UnitTests.Data }; var target = new Control { DataContext = source }; - var observable = binding.Initiate(target, null).Observable; + var observable = binding.Initiate(target, null).Source; var result = await observable.Take(1); Assert.Equal("1,2,3", result); From d6a68c8af838b831ba595e22090578c8bbad91c6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 17 Jan 2023 18:34:55 +0100 Subject: [PATCH 036/326] Expose binding plugins in a different API. --- .../Data/Core/Plugins/BindingPlugins.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs new file mode 100644 index 0000000000..6d88d55774 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Avalonia.Data.Core.Plugins +{ + /// + /// Holds a registry of plugins used for bindings. + /// + public static class BindingPlugins + { + /// + /// An ordered collection of property accessor plugins that can be used to customize + /// the reading and subscription of property values on a type. + /// + public static IList PropertyAccessors => ExpressionObserver.PropertyAccessors; + + /// + /// An ordered collection of validation checker plugins that can be used to customize + /// the validation of view model and model data. + /// + public static IList DataValidators => ExpressionObserver.DataValidators; + + /// + /// An ordered collection of stream plugins that can be used to customize the behavior + /// of the '^' stream binding operator. + /// + public static IList StreamHandlers => ExpressionObserver.StreamHandlers; + } +} From 43e00b710bc55e7824a7e74661088b4d0f6cc052 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Jan 2023 11:36:33 +0100 Subject: [PATCH 037/326] Make concrete binding plugin classes internal. --- .../Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs | 2 +- .../Data/Core/Plugins/DataAnnotationsValidationPlugin.cs | 2 +- .../Data/Core/Plugins/ExceptionValidationPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs | 2 +- .../Data/Core/Plugins/InpcPropertyAccessorPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 34f8e568d4..f111d8917b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -8,7 +8,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Reads a property from a . /// - public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index bc300386b9..ba5f59ea23 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on that have s. /// - public class DataAnnotationsValidationPlugin : IDataValidationPlugin + internal class DataAnnotationsValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 2bb8da2c74..e60a341309 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - public class ExceptionValidationPlugin : IDataValidationPlugin + internal class ExceptionValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 87a2f67ee8..3384a99333 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on objects that implement . /// - public class IndeiValidationPlugin : IDataValidationPlugin + internal class IndeiValidationPlugin : IDataValidationPlugin { private static readonly WeakEvent ErrorsChangedWeakEvent = WeakEvent.Register( diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 5b19e995cc..7c2caf02b4 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core.Plugins /// Reads a property from a standard C# object that optionally supports the /// interface. /// - public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup = new Dictionary<(Type, string), PropertyInfo?>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 2397ce483d..8170edd653 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -6,7 +6,7 @@ using System.Reflection; namespace Avalonia.Data.Core.Plugins { - public class MethodAccessorPlugin : IPropertyAccessorPlugin + internal class MethodAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup = new Dictionary<(Type, string), MethodInfo?>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index b40628fd35..2b9da0a61a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class ObservableStreamPlugin : IStreamPlugin + internal class ObservableStreamPlugin : IStreamPlugin { private static MethodInfo? s_observableGeneric; private static MethodInfo? s_observableSelect; diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 715f4604cf..42a050778e 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class TaskStreamPlugin : IStreamPlugin + internal class TaskStreamPlugin : IStreamPlugin { /// /// Checks whether this plugin handles the specified value. From fce16337474b7389d976680d285c00ddcc5f8ffc Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 18 Jan 2023 10:41:53 +0000 Subject: [PATCH 038/326] add doc comments --- .../Primitives/IScrollSnapPointsInfo.cs | 31 +++++++++++++++++++ .../Primitives/SnapPointsAlignment.cs | 14 +++++++++ .../Primitives/SnapPointsType.cs | 14 +++++++++ 3 files changed, 59 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs index 2aa2382dc0..d0462aff9e 100644 --- a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs +++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs @@ -5,15 +5,46 @@ using Avalonia.Layout; namespace Avalonia.Controls.Primitives { + /// + /// Describes snap point behavior for objects that contain and present items. + /// public interface IScrollSnapPointsInfo { + /// + /// Gets a value that indicates whether the horizontal snap points for the container are equidistant from each other. + /// bool AreHorizontalSnapPointsRegular { get; } + + /// + /// Gets a value that indicates whether the vertical snap points for the container are equidistant from each other. + /// bool AreVerticalSnapPointsRegular { get; } + /// + /// Returns the set of distances between irregular snap points for a specified orientation and alignment. + /// + /// The orientation for the desired snap point set. + /// The alignment to use when applying the snap points. + /// The read-only collection of snap point distances. Returns an empty collection when no snap points are present. IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment); + + /// + /// Gets the distance between regular snap points for a specified orientation and alignment. + /// + /// The orientation for the desired snap point set. + /// The alignment to use when applying the snap points. + /// Out parameter. The offset of the first snap point. + /// The distance between the equidistant snap points. Returns 0 when no snap points are present. double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset); + /// + /// Occurs when the measurements for horizontal snap points change. + /// event EventHandler HorizontalSnapPointsChanged; + + /// + /// Occurs when the measurements for vertical snap points change. + /// event EventHandler VerticalSnapPointsChanged; } } diff --git a/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs index 9f0125f1c4..77b93c50a0 100644 --- a/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs +++ b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs @@ -1,9 +1,23 @@ namespace Avalonia.Controls.Primitives { + /// + /// Specify options for snap point alignment relative to an edge. Which edge depends on the orientation of the object where the alignment is applied + /// public enum SnapPointsAlignment { + /// + /// Use snap points grouped closer to the orientation edge. + /// Near, + + /// + /// Use snap points that are centered in the orientation. + /// Center, + + /// + /// Use snap points grouped farther from the orientation edge. + /// Far } } diff --git a/src/Avalonia.Controls/Primitives/SnapPointsType.cs b/src/Avalonia.Controls/Primitives/SnapPointsType.cs index 7e43f4c191..130fb85f77 100644 --- a/src/Avalonia.Controls/Primitives/SnapPointsType.cs +++ b/src/Avalonia.Controls/Primitives/SnapPointsType.cs @@ -1,9 +1,23 @@ namespace Avalonia.Controls.Primitives { + /// + /// Specify how panning snap points are processed for gesture input. + /// public enum SnapPointsType { + /// + /// No snapping behavior. + /// None, + + /// + /// Content always stops at the snap point closest to where inertia would naturally stop along the direction of inertia. + /// Mandatory, + + /// + /// Content always stops at the snap point closest to the release point along the direction of inertia. + /// MandatorySingle } } From ae78a2bc0330d1a3afc8dbb63112230944c7f273 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 18 Jan 2023 09:55:30 -0500 Subject: [PATCH 039/326] Revert "Add Interactive.AddHandler() for CancelRoutedEventArgs" This reverts commit cfb90c8201378dab7afc8d75316ea9c069c2c79c. --- .../Interactivity/Interactive.cs | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Base/Interactivity/Interactive.cs b/src/Avalonia.Base/Interactivity/Interactive.cs index 3bdaa60d2e..821e00d784 100644 --- a/src/Avalonia.Base/Interactivity/Interactive.cs +++ b/src/Avalonia.Base/Interactivity/Interactive.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Layout; +using Avalonia.VisualTree; #nullable enable @@ -66,38 +67,7 @@ namespace Avalonia.Interactivity typedHandler(sender, typedArgs); } - var subscription = new EventSubscription(handler, routes, handledEventsToo, InvokeAdapter); - - AddEventSubscription(routedEvent, subscription); - } - - /// - /// Adds a handler for the specified routed event. - /// - /// The routed event. - /// The handler. - /// The routing strategies to listen to. - /// Whether handled events should also be listened for. - public void AddHandler( - RoutedEvent routedEvent, - EventHandler? handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, - bool handledEventsToo = false) - { - routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent)); - - if (handler is null) - return; - - static void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) - { - var typedHandler = (EventHandler)baseHandler; - var typedArgs = (CancelRoutedEventArgs)args; - - typedHandler(sender, typedArgs); - } - - var subscription = new EventSubscription(handler, routes, handledEventsToo, InvokeAdapter); + var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args)); AddEventSubscription(routedEvent, subscription); } From c8c3b151b0ffa44d4382e98b5da04572711d9ef8 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 18 Jan 2023 09:56:45 -0500 Subject: [PATCH 040/326] Fix expander event args type --- src/Avalonia.Controls/Expander.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index c57774c70b..2ad6a58d38 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -76,16 +76,16 @@ namespace Avalonia.Controls /// /// Defines the event. /// - public static readonly RoutedEvent CollapsingEvent = - RoutedEvent.Register( + public static readonly RoutedEvent CollapsingEvent = + RoutedEvent.Register( nameof(Collapsing), RoutingStrategies.Bubble); /// /// Defines the event. /// - public static readonly RoutedEvent ExpandedEvent = - RoutedEvent.Register( + public static readonly RoutedEvent ExpandedEvent = + RoutedEvent.Register( nameof(Expanded), RoutingStrategies.Bubble); From b43bd006b07b087114ea55052c2ffc8c50af2500 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 18 Jan 2023 21:03:09 -0500 Subject: [PATCH 041/326] Fix samples build --- samples/BindingDemo/App.xaml | 7 ------- samples/MobileSandbox/App.xaml | 5 +++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/samples/BindingDemo/App.xaml b/samples/BindingDemo/App.xaml index 5a8e65ed22..84f54293ef 100644 --- a/samples/BindingDemo/App.xaml +++ b/samples/BindingDemo/App.xaml @@ -2,13 +2,6 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="BindingDemo.App"> - - - - - - - diff --git a/samples/MobileSandbox/App.xaml b/samples/MobileSandbox/App.xaml index 85c97c9dbe..6fb6ae297e 100644 --- a/samples/MobileSandbox/App.xaml +++ b/samples/MobileSandbox/App.xaml @@ -1,8 +1,9 @@ + x:Class="MobileSandbox.App" + RequestedThemeVariant="Dark"> - + From 658fd804af0835600d6c5e0fcdfa2826590f4ace Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 01:36:45 +0100 Subject: [PATCH 042/326] Removed some temporary List from text layout --- .../Media/TextFormatting/SplitResult.cs | 11 ++++ .../Media/TextFormatting/TextCharacters.cs | 9 +-- .../TextCollapsingProperties.cs | 6 +- .../TextFormatting/TextEllipsisHelper.cs | 62 ++++++++++--------- .../Media/TextFormatting/TextFormatterImpl.cs | 53 +++++++++------- .../Media/TextFormatting/TextLayout.cs | 51 ++++++++------- .../TextLeadingPrefixCharacterEllipsis.cs | 50 ++++++++------- .../Media/TextFormatting/TextLineImpl.cs | 56 ++++++++--------- .../TextTrailingCharacterEllipsis.cs | 7 +-- .../TextTrailingWordEllipsis.cs | 8 +-- 10 files changed, 169 insertions(+), 144 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 53021c4656..c1ac57ce46 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -26,5 +26,16 @@ /// The second part. /// public T? Second { get; } + + /// + /// Deconstructs the split results into its components. + /// + /// On return, contains the first part. + /// On return, contains the second part. + public void Deconstruct(out T first, out T? second) + { + first = First; + second = Second; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 2525f0dbf9..6454f9bfa3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, + ref TextRunProperties? previousProperties, List results) { - var shapeableCharacters = new List(2); var properties = Properties; while (!text.IsEmpty) { var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); - shapeableCharacters.Add(shapeableRun); + results.Add(shapeableRun); text = text.Slice(shapeableRun.Length); previousProperties = shapeableRun.Properties; } - - return shapeableCharacters; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 01804e1ce3..72882df0b5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract TextRun[]? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 528cd45581..97f8b2483b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -22,7 +25,7 @@ namespace Avalonia.Media.TextFormatting if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol - return new List(0); + return Array.Empty(); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -70,18 +73,7 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= shapedRun.Size.Width; @@ -94,18 +86,7 @@ namespace Avalonia.Media.TextFormatting //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= drawableRun.Size.Width; @@ -121,5 +102,30 @@ namespace Avalonia.Media.TextFormatting return null; } + + private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, + TextRun shapedSymbol) + { + if (collapsedLength <= 0) + { + return new[] { shapedSymbol }; + } + + // perf note: the runs are very likely to come from TextLineImpl + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + + var (preSplitRuns, _) = TextFormatterImpl.SplitTextRuns(textRunArray, collapsedLength); + + var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + + for (var i = 0; i < preSplitRuns.Count; ++i) + { + collapsedRuns[i] = preSplitRuns[i]; + } + + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8afecb09e2..5c073452f4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -23,10 +24,10 @@ namespace Avalonia.Media.TextFormatting var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, out var textEndOfLine, out var textSourceLength); - if (previousLineBreak?.RemainingRuns != null) + if (previousLineBreak?.RemainingRuns is { } remainingRuns) { resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = previousLineBreak.RemainingRuns; + textRuns = remainingRuns; nextLineBreak = previousLineBreak; } else @@ -45,7 +46,7 @@ namespace Avalonia.Media.TextFormatting { case TextWrapping.NoWrap: { - textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, + textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); @@ -160,6 +161,14 @@ namespace Avalonia.Media.TextFormatting { var flowDirection = paragraphProperties.FlowDirection; var shapedRuns = new List(); + + if (textRuns.Count == 0) + { + resolvedFlowDirection = flowDirection; + return shapedRuns; + } + + using var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) @@ -224,7 +233,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); + ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); break; } @@ -309,11 +318,9 @@ namespace Avalonia.Media.TextFormatting && x.Typeface == y.Typeface && x.BaselineAlignment == y.BaselineAlignment; - private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, ReadOnlyMemory text, TextShaperOptions options) + private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, + TextShaperOptions options, List results) { - var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) @@ -322,12 +329,10 @@ namespace Avalonia.Media.TextFormatting var splitResult = shapedBuffer.Split(currentRun.Length); - shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); + results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); shapedBuffer = splitResult.Second!; } - - return shapedRuns; } /// @@ -335,7 +340,7 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. - /// + /// A list that will be filled with the processed runs. /// private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, List processedRuns) @@ -385,7 +390,8 @@ namespace Avalonia.Media.TextFormatting if (j == runTextSpan.Length) { - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runLevel = levels[levelIndex]; @@ -398,7 +404,8 @@ namespace Avalonia.Media.TextFormatting } // End of this run - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runText = runText.Slice(j); runTextSpan = runText.Span; @@ -415,7 +422,7 @@ namespace Avalonia.Media.TextFormatting return; } - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns); } /// @@ -423,8 +430,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. - /// - /// + /// On return, the end of line, if any. + /// On return, the processed text source length. /// /// The formatted text runs. /// @@ -602,7 +609,7 @@ namespace Avalonia.Media.TextFormatting var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); - var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; + var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); } @@ -749,12 +756,10 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitTextRuns(textRuns, measuredLength); - - var remainingCharacters = splitResult.Second; + var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength); - var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : + var lineBreak = postSplitRuns?.Count > 0 ? + new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) @@ -762,7 +767,7 @@ namespace Avalonia.Media.TextFormatting lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } - var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, + var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 468623b356..55b6f14267 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; + private readonly TextLine[] _textLines; private int _textSourceLength; @@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text lines. /// - public IReadOnlyList TextLines { get; private set; } + public IReadOnlyList TextLines + => _textLines; /// /// Gets the bounds of the layout. @@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting /// The origin. public void Draw(DrawingContext context, Point origin) { - if (!TextLines.Any()) + if (_textLines.Length == 0) { return; } var (currentX, currentY) = origin; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); @@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting /// public Rect HitTestTextPosition(int textPosition) { - if (TextLines.Count == 0) + if (_textLines.Length == 0) { return new Rect(); } @@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting var currentY = 0.0; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { var end = textLine.FirstTextSourceIndex + textLine.Length; @@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting return Array.Empty(); } - var result = new List(TextLines.Count); + var result = new List(_textLines.Length); var currentY = 0d; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { //Current line isn't covered. if (textLine.FirstTextSourceIndex + textLine.Length < start) @@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting { var currentY = 0d; - var lineIndex = 0; TextLine? currentLine = null; CharacterHit characterHit; - for (; lineIndex < TextLines.Count; lineIndex++) + for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++) { - currentLine = TextLines[lineIndex]; + currentLine = _textLines[lineIndex]; if (currentY + currentLine.Height > point.Y) { @@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting if (charIndex > _textSourceLength) { - return TextLines.Count - 1; + return _textLines.Length - 1; } - for (var index = 0; index < TextLines.Count; index++) + for (var index = 0; index < _textLines.Length; index++) { - var textLine = TextLines[index]; + var textLine = _textLines[index]; if (textLine.FirstTextSourceIndex + textLine.Length < charIndex) { @@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting } } - return TextLines.Count - 1; + return _textLines.Length - 1; } private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) @@ -424,7 +424,7 @@ namespace Avalonia.Media.TextFormatting height += textLine.Height; } - private IReadOnlyList CreateTextLines() + private TextLine[] CreateTextLines() { if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { @@ -432,7 +432,7 @@ namespace Avalonia.Media.TextFormatting Bounds = new Rect(0, 0, 0, textLine.Height); - return new List { textLine }; + return new TextLine[] { textLine }; } var textLines = new List(); @@ -443,12 +443,14 @@ namespace Avalonia.Media.TextFormatting TextLine? previousLine = null; + var textFormatter = TextFormatter.Current; + while (true) { - var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if(textLine == null || textLine.Length == 0) + if (textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { @@ -524,8 +526,9 @@ namespace Avalonia.Media.TextFormatting { var whitespaceWidth = 0d; - foreach (var line in textLines) + for (var i = 0; i < textLines.Count; i++) { + var line = textLines[i]; var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; if (lineWhitespaceWidth > whitespaceWidth) @@ -549,7 +552,7 @@ namespace Avalonia.Media.TextFormatting } } - return textLines; + return textLines.ToArray(); } /// @@ -569,7 +572,7 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { - foreach (var line in TextLines) + foreach (var line in _textLines) { line.Dispose(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index e30a0fe9f4..672a15b398 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,5 +1,7 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Collections.Generic; +using System.Linq; namespace Avalonia.Media.TextFormatting { @@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting if (Width < shapedSymbol.GlyphRun.Size.Width) { - return new List(0); + return Array.Empty(); } // Overview of ellipsis structure @@ -75,41 +78,48 @@ namespace Avalonia.Media.TextFormatting { shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - var collapsedRuns = new List(textRuns.Count); - if (measuredLength > 0) { - IReadOnlyList? preSplitRuns = null; + var collapsedRuns = new List(textRuns.Count + 1); + + // perf note: the runs are very likely to come from TextLineImpl, + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + + IReadOnlyList? preSplitRuns; IReadOnlyList? postSplitRuns; if (_prefixLength > 0) { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, - Math.Min(_prefixLength, measuredLength)); + (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns( + textRunArray, Math.Min(_prefixLength, measuredLength)); - collapsedRuns.AddRange(splitResult.First); - - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; + for (var i = 0; i < preSplitRuns.Count; i++) + { + var preSplitRun = preSplitRuns[i]; + collapsedRuns.Add(preSplitRun); + } } else { - postSplitRuns = textRuns; + preSplitRuns = null; + postSplitRuns = textRunArray; } collapsedRuns.Add(shapedSymbol); if (measuredLength <= _prefixLength || postSplitRuns is null) { - return collapsedRuns; + return collapsedRuns.ToArray(); } var availableSuffixWidth = availableWidth; if (preSplitRuns is not null) { - foreach (var run in preSplitRuns) + for (var i = 0; i < preSplitRuns.Count; i++) { + var run = preSplitRuns[i]; if (run is DrawableTextRun drawableTextRun) { availableSuffixWidth -= drawableTextRun.Size.Width; @@ -143,13 +153,11 @@ namespace Avalonia.Media.TextFormatting } } } - } - else - { - collapsedRuns.Add(shapedSymbol); + + return collapsedRuns.ToArray(); } - return collapsedRuns; + return new TextRun[] { shapedSymbol }; } availableWidth -= shapedRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ab9686a34a..ae6df3a232 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,19 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - internal class TextLineImpl : TextLine + internal sealed class TextLineImpl : TextLine { - private IReadOnlyList _textRuns; + private TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { @@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, _resolvedFlowDirection, TextLineBreak, true); - if (collapsedRuns.Count > 0) + if (collapsedRuns.Length > 0) { collapsedLine.FinalizeLine(); } @@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetCharacterHitFromDistance(double distance) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Count - 1]; + var lastRun = _textRuns[_textRuns.Length - 1]; var size = 0.0; @@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i < _textRuns.Length; i++) { var currentRun = _textRuns[i]; @@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = i; currentPosition += currentRun.Length; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1) { var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; @@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if (j > _textRuns.Count - 1) + if (j > _textRuns.Length - 1) { break; } @@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting if (currentRun is DrawableTextRun drawableTextRun) { - if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) + if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance) { currentDistance += drawableTextRun.Size.Width; @@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting if (flowDirection == FlowDirection.LeftToRight) { - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { var currentRun = _textRuns[index]; @@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftWidth = shapedRun.Size.Width; - while (i + 1 <= _textRuns.Count - 1) + while (i + 1 <= _textRuns.Length - 1) { var nextRun = _textRuns[i + 1]; @@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting { currentDistance += WidthIncludingTrailingWhitespace; - for (var index = _textRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { var currentRun = _textRuns[index]; @@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = index; var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) { if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { @@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting public override void Dispose() { - for (int i = 0; i < _textRuns.Count; i++) + for (int i = 0; i < _textRuns.Length; i++) { if (_textRuns[i] is ShapedTextRun shapedTextRun) { @@ -1013,7 +1012,7 @@ namespace Avalonia.Media.TextFormatting private void BidiReorder() { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return; } @@ -1025,7 +1024,7 @@ namespace Avalonia.Media.TextFormatting var current = orderedRun; - for (var i = 1; i < _textRuns.Count; i++) + for (var i = 1; i < _textRuns.Length; i++) { run = _textRuns[i]; @@ -1044,7 +1043,7 @@ namespace Avalonia.Media.TextFormatting sbyte max = 0; var min = sbyte.MaxValue; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i < _textRuns.Length; i++) { var currentRun = _textRuns[i]; @@ -1095,13 +1094,14 @@ namespace Avalonia.Media.TextFormatting minLevelToReverse--; } - var textRuns = new List(_textRuns.Count); + var textRuns = new TextRun[_textRuns.Length]; + var index = 0; current = orderedRun; while (current != null) { - textRuns.Add(current.Run); + textRuns[index++] = current.Run; current = current.Next; } @@ -1197,7 +1197,7 @@ namespace Avalonia.Media.TextFormatting var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1346,7 +1346,7 @@ namespace Avalonia.Media.TextFormatting textPosition = FirstTextSourceIndex; TextRun? previousRun = null; - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1395,7 +1395,7 @@ namespace Avalonia.Media.TextFormatting } } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1411,7 +1411,7 @@ namespace Avalonia.Media.TextFormatting return runIndex; } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1448,14 +1448,14 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; - var lastRunIndex = _textRuns.Count - 1; + var lastRunIndex = _textRuns.Length - 1; if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) { lastRunIndex--; } - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index deecbbe476..ccae99cc75 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// A collapsing properties to collapse whole line toward the end @@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c291e1dfb9..c622c76a60 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using Avalonia.Utilities; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// a collapsing properties to collapse whole line toward the end @@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } From 96b423900f734c698932534dacefd59827557470 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Jan 2023 11:50:14 +0100 Subject: [PATCH 043/326] TextRunProperties: don't allocate if the typeface hasn't changed --- src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 86b701cb4b..7bad99f33f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -93,6 +93,9 @@ namespace Avalonia.Media.TextFormatting internal TextRunProperties WithTypeface(Typeface typeface) { + if (this is GenericTextRunProperties other && other.Typeface == typeface) + return this; + return new GenericTextRunProperties(typeface, FontRenderingEmSize, TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); } From 076d10fcaf6f14d14585b147ffdd72f578efaa7e Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Jan 2023 14:57:06 +0100 Subject: [PATCH 044/326] BiDiAlgorithm and BiDiData instances are reusable --- .../Media/TextFormatting/ShapedBuffer.cs | 43 +++++++---------- .../Media/TextFormatting/TextFormatterImpl.cs | 25 ++++++---- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 48 ++++++++----------- .../Media/TextFormatting/Unicode/BiDiData.cs | 38 +++++++++------ src/Avalonia.Base/Utilities/ArrayBuilder.cs | 18 ++----- src/Avalonia.Base/Utilities/ArraySlice.cs | 10 +--- .../TextBoxTextInputMethodClient.cs | 2 +- .../Media/TextFormatting/BiDiClassTests.cs | 6 +-- 8 files changed, 84 insertions(+), 106 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index b05fab08fa..41bba2cd09 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -8,17 +8,17 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IList, IDisposable { private static readonly IComparer s_clusterComparer = new CompareClusters(); - private bool _bufferRented; - - public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(text, - new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), - glyphTypeface, - fontRenderingEmSize, - bidiLevel) + + private GlyphInfo[]? _rentedBuffer; + + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - _bufferRented = true; - Length = bufferLength; + _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); + Text = text; + GlyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); + GlyphTypeface = glyphTypeface; + FontRenderingEmSize = fontRenderingEmSize; + BidiLevel = bidiLevel; } internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -28,12 +28,12 @@ namespace Avalonia.Media.TextFormatting GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; - Length = GlyphInfos.Length; } - internal ArraySlice GlyphInfos { get; } - - public int Length { get; } + internal ArraySlice GlyphInfos { get; private set; } + + public int Length + => GlyphInfos.Length; public IGlyphTypeface GlyphTypeface { get; } @@ -271,18 +271,11 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { - GC.SuppressFinalize(this); - if (_bufferRented) - { - GlyphInfos.ReturnRent(); - } - } - - ~ShapedBuffer() - { - if (_bufferRented) + if (_rentedBuffer is not null) { - GlyphInfos.ReturnRent(); + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + GlyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 5c073452f4..7614c8e3dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -11,6 +11,10 @@ namespace Avalonia.Media.TextFormatting internal class TextFormatterImpl : TextFormatter { private static readonly char[] s_empty = { ' ' }; + private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; + + [ThreadStatic] private static BidiData? t_bidiData; + [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; /// public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, @@ -169,21 +173,24 @@ namespace Avalonia.Media.TextFormatting } - using var biDiData = new BidiData((sbyte)flowDirection); + var biDiData = t_bidiData ??= new BidiData(); + biDiData.Reset(); + biDiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; foreach (var textRun in textRuns) { - if (textRun.Text.IsEmpty) - { - biDiData.Append(new char[textRun.Length]); - } + ReadOnlySpan text; + if (!textRun.Text.IsEmpty) + text = textRun.Text.Span; + else if (textRun.Length == TextRun.DefaultTextSourceLength) + text = s_defaultText; else - { - biDiData.Append(textRun.Text.Span); - } + text = new char[textRun.Length]; + + biDiData.Append(text); } - using var biDi = new BidiAlgorithm(); + var biDi = t_bidiAlgorithm ??= new BidiAlgorithm(); biDi.Process(biDiData); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 100d381afe..e770ba9e91 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; -using Avalonia.Collections.Pooled; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -28,7 +27,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// as much as possible. /// /// - internal struct BidiAlgorithm : IDisposable + internal sealed class BidiAlgorithm { /// /// The original BiDiClass classes as provided by the caller @@ -67,7 +66,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The forward mapping maps the start index to the end index. /// The reverse mapping maps the end index to the start index. /// - private BidiDictionary? _isolatePairs; + private readonly BidiDictionary _isolatePairs = new(); /// /// The working BiDi classes @@ -98,7 +97,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The status stack used during resolution of explicit /// embedding and isolating runs /// - private readonly Stack _statusStack = new Stack(); + private readonly Stack _statusStack = new(); /// /// Mapping used to virtually remove characters for rule X9 @@ -108,7 +107,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Re-usable list of level runs /// - private readonly List _levelRuns = new List(); + private readonly List _levelRuns = new(); /// /// Mapping for the current isolating sequence, built @@ -119,7 +118,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private Stack? _pendingIsolateOpenings; + private readonly Stack _pendingIsolateOpenings = new(); /// /// The level of the isolating run currently being processed @@ -175,12 +174,12 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Reusable list of pending opening brackets used by the /// LocatePairedBrackets method /// - private readonly List _pendingOpeningBrackets = new List(); + private readonly List _pendingOpeningBrackets = new(); /// /// Resolved list of paired brackets /// - private readonly List _pairedBrackets = new List(); + private readonly List _pairedBrackets = new(); /// /// Initializes a new instance of the class. @@ -228,7 +227,7 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs?.Clear(); + _isolatePairs.Clear(); _workingClassesBuffer.Clear(); _levelRuns.Clear(); _resolvedLevelsBuffer.Clear(); @@ -324,7 +323,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // Skip isolate pairs // (Because we're working with a slice, we need to adjust the indices // we're using for the isolatePairs map) - if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true) + if (_isolatePairs.TryGetValue(data.Start + i, out i)) { i -= data.Start; } @@ -359,7 +358,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _hasIsolates = false; // BD9... - _pendingIsolateOpenings?.Clear(); + _pendingIsolateOpenings.Clear(); for (var i = 0; i < _originalClasses.Length; i++) { @@ -371,16 +370,14 @@ namespace Avalonia.Media.TextFormatting.Unicode case BidiClass.RightToLeftIsolate: case BidiClass.FirstStrongIsolate: { - _pendingIsolateOpenings ??= new Stack(); _pendingIsolateOpenings.Push(i); _hasIsolates = true; break; } case BidiClass.PopDirectionalIsolate: { - if (_pendingIsolateOpenings?.Count > 0) + if (_pendingIsolateOpenings.Count > 0) { - _isolatePairs ??= new BidiDictionary(); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); } @@ -501,7 +498,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (resolvedIsolate == BidiClass.FirstStrongIsolate) { - if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) + if (!_isolatePairs.TryGetValue(i, out var endOfIsolate)) { endOfIsolate = _originalClasses.Length; } @@ -832,7 +829,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastType = _originalClasses[lastCharacterIndex]; if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && - _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true) + _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); @@ -855,13 +852,14 @@ namespace Avalonia.Media.TextFormatting.Unicode private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel) { // Create mappings onto the underlying data - _runResolvedClasses = new MappedArraySlice(_workingClasses, _isolatedRunMapping.AsSlice()); - _runOriginalClasses = new MappedArraySlice(_originalClasses, _isolatedRunMapping.AsSlice()); - _runLevels = new MappedArraySlice(_resolvedLevels, _isolatedRunMapping.AsSlice()); + var isolatedRunMapping = _isolatedRunMapping.AsSlice(); + _runResolvedClasses = new MappedArraySlice(_workingClasses, isolatedRunMapping); + _runOriginalClasses = new MappedArraySlice(_originalClasses, isolatedRunMapping); + _runLevels = new MappedArraySlice(_resolvedLevels, isolatedRunMapping); if (_hasBrackets) { - _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, _isolatedRunMapping.AsSlice()); - _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, _isolatedRunMapping.AsSlice()); + _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, isolatedRunMapping); + _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, isolatedRunMapping); } _runLevel = runLevel; @@ -1717,13 +1715,5 @@ namespace Avalonia.Media.TextFormatting.Unicode public BidiClass Eos { get; } } - - public void Dispose() - { - _workingClassesBuffer.Dispose(); - _resolvedLevelsBuffer.Dispose(); - _x9Map.Dispose(); - _isolatedRunMapping.Dispose(); - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 0f0b3235e1..106079de8e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// - internal struct BidiData : IDisposable + internal sealed class BidiData { private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; @@ -20,12 +20,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private ArrayBuilder _savedPairedBracketTypes; private ArrayBuilder _tempLevelBuffer; - public BidiData(sbyte paragraphEmbeddingLevel) - { - ParagraphEmbeddingLevel = paragraphEmbeddingLevel; - } - - public sbyte ParagraphEmbeddingLevel { get; private set; } + public sbyte ParagraphEmbeddingLevel { get; set; } public bool HasBrackets { get; private set; } @@ -36,7 +31,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Gets the length of the data held by the BidiData /// - public int Length{get; private set; } + public int Length { get; private set; } /// /// Gets the bidi character type of each code point @@ -182,14 +177,27 @@ namespace Avalonia.Media.TextFormatting.Unicode return _tempLevelBuffer.Add(length, false); } - public void Dispose() + /// + /// Resets the bidi data to a clean state. + /// + public void Reset() { - _classes.Dispose(); - _pairedBracketTypes.Dispose(); - _pairedBracketValues.Dispose(); - _savedClasses.Dispose(); - _savedPairedBracketTypes.Dispose(); - _tempLevelBuffer.Dispose(); + _classes.Clear(); + _pairedBracketTypes.Clear(); + _pairedBracketValues.Clear(); + _savedClasses.Clear(); + _savedPairedBracketTypes.Clear(); + _tempLevelBuffer.Clear(); + + ParagraphEmbeddingLevel = 0; + HasBrackets = false; + HasEmbeddings = false; + HasIsolates = false; + Length = 0; + + Classes = default; + PairedBracketTypes = default; + PairedBracketValues = default; } } } diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index e6b67bd383..1c11966a7d 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Runtime.CompilerServices; namespace Avalonia.Utilities @@ -12,7 +11,7 @@ namespace Avalonia.Utilities /// A helper type for avoiding allocations while building arrays. /// /// The type of item contained in the array. - internal struct ArrayBuilder : IDisposable + internal struct ArrayBuilder where T : struct { private const int DefaultCapacity = 4; @@ -136,7 +135,7 @@ namespace Avalonia.Utilities } // Same expansion algorithm as List. - var newCapacity = length == 0 ? DefaultCapacity : length * 2; + var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; if (newCapacity > MaxCoreClrArrayLength) { @@ -145,15 +144,14 @@ namespace Avalonia.Utilities if (newCapacity < min) { - newCapacity = min; + newCapacity = (uint)min; } - var array = ArrayPool.Shared.Rent(newCapacity); + var array = new T[newCapacity]; if (_size > 0) { Array.Copy(_data!, array, _size); - ArrayPool.Shared.Return(_data!); } _data = array; @@ -182,13 +180,5 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); - - public void Dispose() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index b70088a907..3cffef72c5 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -186,13 +185,6 @@ namespace Avalonia.Utilities /// int IReadOnlyCollection.Count => Length; - - public void ReturnRent() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } + diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index c1146cceda..10c2f36f43 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls { if(run.Length > 0) { -#if NET6_0 +#if NET6_0_OR_GREATER builder.Append(run.Text.Span); #else builder.Append(run.Text.Span.ToArray()); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index 9d189d1950..eb69bed1e1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -1,8 +1,6 @@ -using System; -using System.Linq; +using System.Linq; using System.Runtime.InteropServices; using System.Text; -using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -32,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private bool Run(BiDiClassData t) { var bidi = new BidiAlgorithm(); - var bidiData = new BidiData(t.ParagraphLevel); + var bidiData = new BidiData { ParagraphEmbeddingLevel = t.ParagraphLevel }; var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); From 290c8fe16953b5bb4dd7098752c8b6e693f6ef79 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 17 Jan 2023 03:02:22 +0100 Subject: [PATCH 045/326] Removed most allocations for BidiReorder --- .../Media/TextFormatting/BidiReorderer.cs | 263 ++++++++++++++++++ .../Media/TextFormatting/TextFormatterImpl.cs | 16 +- .../Media/TextFormatting/TextLineImpl.cs | 236 +--------------- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 1 - .../Media/TextFormatting/Unicode/BiDiData.cs | 1 + src/Avalonia.Base/Utilities/ArrayBuilder.cs | 12 +- 6 files changed, 289 insertions(+), 240 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs new file mode 100644 index 0000000000..3fcb7bf420 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -0,0 +1,263 @@ +using System; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Reorders text runs according to their bidi level. + /// + /// To avoid allocations, this class is designed to be reused. + internal sealed class BidiReorderer + { + private ArrayBuilder _runs; + private ArrayBuilder _ranges; + + public void BidiReorder(Span textRuns, FlowDirection flowDirection) + { + Debug.Assert(_runs.Length == 0); + Debug.Assert(_ranges.Length == 0); + + if (textRuns.IsEmpty) + { + return; + } + + try + { + _runs.Add(textRuns.Length); + + // Build up the collection of ordered runs. + for (var i = 0; i < textRuns.Length; i++) + { + var textRun = textRuns[i]; + _runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection)); + + if (i > 0) + { + _runs[i - 1].NextRunIndex = i; + } + } + + // Reorder them into visual order. + var firstIndex = LinearReorder(); + + // Now perform a recursive reversal of each run. + // From the highest level found in the text to the lowest odd level on each line, including intermediate levels + // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. + // https://unicode.org/reports/tr9/#L2 + sbyte max = 0; + var min = sbyte.MaxValue; + + for (var i = 0; i < textRuns.Length; i++) + { + var level = GetRunBidiLevel(textRuns[i], flowDirection); + if (level > max) + { + max = level; + } + + if ((level & 1) != 0 && level < min) + { + min = level; + } + } + + if (min > max) + { + min = max; + } + + if (max == 0 || (min == max && (max & 1) == 0)) + { + // Nothing to reverse. + return; + } + + // Now apply the reversal and replace the original contents. + var minLevelToReverse = max; + int currentIndex; + + while (minLevelToReverse >= min) + { + currentIndex = firstIndex; + + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + if (current.Level >= minLevelToReverse && current.Level % 2 != 0) + { + if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) + { + shapedTextCharacters.Reverse(); + } + } + + currentIndex = current.NextRunIndex; + } + + minLevelToReverse--; + } + + var index = 0; + + currentIndex = firstIndex; + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + textRuns[index++] = current.Run; + + currentIndex = current.NextRunIndex; + } + } + finally + { + _runs.Clear(); + _ranges.Clear(); + } + } + + private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) + { + if (run is ShapedTextRun shapedTextRun) + { + return shapedTextRun.BidiLevel; + } + + var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; + return (sbyte)defaultLevel; + } + + /// + /// Reorders the runs from logical to visual order. + /// + /// + /// The first run index in visual order. + private int LinearReorder() + { + var runIndex = 0; + var rangeIndex = -1; + + while (runIndex >= 0) + { + ref var run = ref _runs[runIndex]; + var nextRunIndex = run.NextRunIndex; + + while (rangeIndex >= 0 + && _ranges[rangeIndex].Level > run.Level + && _ranges[rangeIndex].PreviousRangeIndex >= 0 + && _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level) + { + + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level) + { + // Attach run to the range. + if ((run.Level & 1) != 0) + { + // Odd, range goes to the right of run. + run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex; + _ranges[rangeIndex].LeftRunIndex = runIndex; + } + else + { + // Even, range goes to the left of run. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex; + _ranges[rangeIndex].RightRunIndex = runIndex; + } + + _ranges[rangeIndex].Level = run.Level; + } + else + { + var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex); + _ranges.AddItem(r); + rangeIndex = _ranges.Length - 1; + } + + runIndex = nextRunIndex; + } + + while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0) + { + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + // Terminate. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1; + + return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex; + } + + private int MergeRangeWithPrevious(int index) + { + var previousIndex = _ranges[index].PreviousRangeIndex; + ref var previous = ref _ranges[previousIndex]; + + int leftIndex; + int rightIndex; + + if ((previous.Level & 1) != 0) + { + // Odd, previous goes to the right of range. + leftIndex = index; + rightIndex = previousIndex; + } + else + { + // Even, previous goes to the left of range. + leftIndex = previousIndex; + rightIndex = index; + } + + // Stitch them + ref var left = ref _ranges[leftIndex]; + ref var right = ref _ranges[rightIndex]; + _runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex; + previous.LeftRunIndex = left.LeftRunIndex; + previous.RightRunIndex = right.RightRunIndex; + + return previousIndex; + } + + private struct OrderedBidiRun + { + public OrderedBidiRun(int runIndex, TextRun run, sbyte level) + { + RunIndex = runIndex; + Run = run; + Level = level; + NextRunIndex = -1; + } + + public int RunIndex { get; } + + public sbyte Level { get; } + + public TextRun Run { get; } + + public int NextRunIndex { get; set; } // -1 if none + } + + private struct BidiRange + { + public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex) + { + Level = level; + LeftRunIndex = leftRunIndex; + RightRunIndex = rightRunIndex; + PreviousRangeIndex = previousRangeIndex; + } + + public sbyte Level { get; set; } + + public int LeftRunIndex { get; set; } + + public int RightRunIndex { get; set; } + + public int PreviousRangeIndex { get; } // -1 if none + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7614c8e3dc..f3cc0a714e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -173,9 +173,9 @@ namespace Avalonia.Media.TextFormatting } - var biDiData = t_bidiData ??= new BidiData(); - biDiData.Reset(); - biDiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; + var bidiData = t_bidiData ??= new BidiData(); + bidiData.Reset(); + bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; foreach (var textRun in textRuns) { @@ -187,21 +187,21 @@ namespace Avalonia.Media.TextFormatting else text = new char[textRun.Length]; - biDiData.Append(text); + bidiData.Append(text); } - var biDi = t_bidiAlgorithm ??= new BidiAlgorithm(); + var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm(); - biDi.Process(biDiData); + bidiAlgorithm.Process(bidiData); - var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes); + var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes); resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; var processedRuns = new List(textRuns.Count); - CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns); for (var index = 0; index < processedRuns.Count; index++) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ae6df3a232..7fa9155b02 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Threading; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal sealed class TextLineImpl : TextLine { - private TextRun[] _textRuns; + private static readonly ThreadLocal s_bidiReorderer = new(() => new BidiReorderer()); + + private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; @@ -993,185 +996,12 @@ namespace Avalonia.Media.TextFormatting { _textLineMetrics = CreateLineMetrics(); - BidiReorder(); + var bidiReorderer = s_bidiReorderer.Value!; + bidiReorderer.BidiReorder(_textRuns, _resolvedFlowDirection); return this; } - private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) - { - if (run is ShapedTextRun shapedTextCharacters) - { - return shapedTextCharacters.BidiLevel; - } - - var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; - - return (sbyte)defaultLevel; - } - - private void BidiReorder() - { - if (_textRuns.Length == 0) - { - return; - } - - // Build up the collection of ordered runs. - var run = _textRuns[0]; - - OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection)); - - var current = orderedRun; - - for (var i = 1; i < _textRuns.Length; i++) - { - run = _textRuns[i]; - - current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection)); - - current = current.Next; - } - - // Reorder them into visual order. - orderedRun = LinearReOrder(orderedRun); - - // Now perform a recursive reversal of each run. - // From the highest level found in the text to the lowest odd level on each line, including intermediate levels - // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. - // https://unicode.org/reports/tr9/#L2 - sbyte max = 0; - var min = sbyte.MaxValue; - - for (var i = 0; i < _textRuns.Length; i++) - { - var currentRun = _textRuns[i]; - - var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection); - - if (level > max) - { - max = level; - } - - if ((level & 1) != 0 && level < min) - { - min = level; - } - } - - if (min > max) - { - min = max; - } - - if (max == 0 || (min == max && (max & 1) == 0)) - { - // Nothing to reverse. - return; - } - - // Now apply the reversal and replace the original contents. - var minLevelToReverse = max; - - while (minLevelToReverse >= min) - { - current = orderedRun; - - while (current != null) - { - if (current.Level >= minLevelToReverse && current.Level % 2 != 0) - { - if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) - { - shapedTextCharacters.Reverse(); - } - } - - current = current.Next; - } - - minLevelToReverse--; - } - - var textRuns = new TextRun[_textRuns.Length]; - var index = 0; - - current = orderedRun; - - while (current != null) - { - textRuns[index++] = current.Run; - - current = current.Next; - } - - _textRuns = textRuns; - } - - /// - /// Reorders a series of runs from logical to visual order, returning the left most run. - /// - /// - /// The ordered bidi run. - /// The . - private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run) - { - BidiRange? range = null; - - while (run != null) - { - var next = run.Next; - - while (range != null && range.Level > run.Level - && range.Previous != null && range.Previous.Level >= run.Level) - { - range = BidiRange.MergeWithPrevious(range); - } - - if (range != null && range.Level >= run.Level) - { - // Attach run to the range. - if ((run.Level & 1) != 0) - { - // Odd, range goes to the right of run. - run.Next = range.Left; - range.Left = run; - } - else - { - // Even, range goes to the left of run. - range.Right!.Next = run; - range.Right = run; - } - - range.Level = run.Level; - } - else - { - var r = new BidiRange(); - - r.Left = r.Right = run; - r.Level = run.Level; - r.Previous = range; - - range = r; - } - - run = next; - } - - while (range?.Previous != null) - { - range = BidiRange.MergeWithPrevious(range); - } - - // Terminate. - range!.Right!.Next = null; - - return range.Left!; - } - /// /// Tries to find the next character hit. /// @@ -1620,59 +1450,5 @@ namespace Avalonia.Media.TextFormatting return 0; } } - - private sealed class OrderedBidiRun - { - public OrderedBidiRun(TextRun run, sbyte level) - { - Run = run; - Level = level; - } - - public sbyte Level { get; } - - public TextRun Run { get; } - - public OrderedBidiRun? Next { get; set; } - } - - private sealed class BidiRange - { - public int Level { get; set; } - - public OrderedBidiRun? Left { get; set; } - - public OrderedBidiRun? Right { get; set; } - - public BidiRange? Previous { get; set; } - - public static BidiRange MergeWithPrevious(BidiRange range) - { - var previous = range.Previous; - - BidiRange left; - BidiRange right; - - if ((previous!.Level & 1) != 0) - { - // Odd, previous goes to the right of range. - left = range; - right = previous; - } - else - { - // Even, previous goes to the left of range. - left = previous; - right = range; - } - - // Stitch them - left.Right!.Next = right.Left; - previous.Left = left.Left; - previous.Right = right.Right; - - return previous; - } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index e770ba9e91..e960a510a9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Threading; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 106079de8e..226e5ad6bd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,6 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// + /// To avoid allocations, this class is designed to be reused. internal sealed class BidiData { private ArrayBuilder _classes; diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 1c11966a7d..3a22fc7b9c 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -18,7 +18,7 @@ namespace Avalonia.Utilities private const int MaxCoreClrArrayLength = 0x7FeFFFFF; // Starts out null, initialized on first Add. - private T[] _data; + private T[]? _data; private int _size; /// @@ -115,6 +115,16 @@ namespace Avalonia.Utilities return slice; } + /// + /// Appends an item. + /// + /// The item to append. + public void AddItem(T value) + { + var index = Length++; + _data![index] = value; + } + /// /// Clears the array. /// Allocated memory is left intact for future usage. From 4144be11fefdfbe632c49752dd5276395b5a41ae Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 17 Jan 2023 17:42:54 +0100 Subject: [PATCH 046/326] Pass GlyphInfo directly to GlyphRun --- src/Avalonia.Base/Media/GlyphRun.cs | 518 +++++++----------- .../Media/TextFormatting/GlyphInfo.cs | 36 ++ .../TextFormatting/InterWordJustification.cs | 2 +- .../Media/TextFormatting/ShapedBuffer.cs | 189 +------ .../Media/TextFormatting/ShapedTextRun.cs | 24 +- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- .../Platform/IPlatformRenderInterface.cs | 9 +- .../Composition/Server/FpsCounter.cs | 2 +- .../Utilities/BinarySearchExtension.cs | 3 +- .../HeadlessPlatformRenderInterface.cs | 3 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 83 +-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 42 +- .../Media/GlyphRunTests.cs | 14 +- .../VisualTree/MockRenderInterface.cs | 3 +- .../NullRenderingPlatform.cs | 3 +- .../Media/GlyphRunTests.cs | 10 +- .../Media/GlyphRunTests.cs | 21 +- .../TextFormatting/TextFormatterTests.cs | 10 +- .../Media/TextFormatting/TextLayoutTests.cs | 55 +- .../Media/TextFormatting/TextLineTests.cs | 30 +- .../Media/TextFormatting/TextShaperTests.cs | 10 +- .../MockPlatformRenderInterface.cs | 3 +- ...ould_Render_GlyphRun_Geometry.expected.png | Bin 4149 -> 4222 bytes 23 files changed, 390 insertions(+), 682 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index fc4bc6aa1c..b637c94d88 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -11,64 +13,112 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { - private static readonly IComparer s_ascendingComparer = Comparer.Default; - private static readonly IComparer s_descendingComparer = new ReverseComparer(); - private IGlyphRunImpl? _glyphRunImpl; - private IGlyphTypeface _glyphTypeface; private double _fontRenderingEmSize; private int _biDiLevel; private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private ReadOnlyMemory _characters; - private IReadOnlyList _glyphIndices; - private IReadOnlyList? _glyphAdvances; - private IReadOnlyList? _glyphOffsets; - private IReadOnlyList? _glyphClusters; + private IReadOnlyList _glyphInfos; + private bool _hasOneCharPerCluster; // if true, character index and cluster are similar /// - /// Initializes a new instance of the class by specifying properties of the class. + /// Initializes a new instance of the class by specifying properties of the class. /// /// The glyph typeface. /// The rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. /// The characters. - /// The glyph clusters. + /// The glyph indices. /// The bidi level. public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphIndices, - IReadOnlyList? glyphAdvances = null, - IReadOnlyList? glyphOffsets = null, - IReadOnlyList? glyphClusters = null, + int biDiLevel = 0) + : this(glyphTypeface, fontRenderingEmSize, characters, + CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel) + { + _hasOneCharPerCluster = true; + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The characters. + /// The list of glyphs used. + /// The bidi level. + public GlyphRun( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlyMemory characters, + IReadOnlyList glyphInfos, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + GlyphTypeface = glyphTypeface; _fontRenderingEmSize = fontRenderingEmSize; _characters = characters; - _glyphIndices = glyphIndices; + _glyphInfos = glyphInfos; - _glyphAdvances = glyphAdvances; + _biDiLevel = biDiLevel; + } - _glyphOffsets = glyphOffsets; + private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, + double fontRenderingEmSize, IGlyphTypeface glyphTypeface) + { + var glyphIndexSpan = ListToSpan(glyphIndices); + var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan); - _glyphClusters = glyphClusters; + var glyphInfos = new GlyphInfo[glyphIndexSpan.Length]; + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; - _biDiLevel = biDiLevel; + for (var i = 0; i < glyphIndexSpan.Length; ++i) + { + glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale); + } + + return glyphInfos; + } + + private static ReadOnlySpan ListToSpan(IReadOnlyList list) + { + var count = list.Count; + + if (count == 0) + { + return default; + } + + if (list is ushort[] array) + { + return array.AsSpan(); + } + +#if NET6_0_OR_GREATER + if (list is List concreteList) + { + return CollectionsMarshal.AsSpan(concreteList); + } +#endif + + array = new ushort[count]; + for (var i = 0; i < count; ++i) + { + array[i] = list[i]; + } + + return array.AsSpan(); } /// /// Gets the for the . /// - public IGlyphTypeface GlyphTypeface => _glyphTypeface; + public IGlyphTypeface GlyphTypeface { get; } /// /// Gets or sets the em size used for rendering the . @@ -88,56 +138,17 @@ namespace Avalonia.Media /// /// public GlyphRunMetrics Metrics - { - get - { - _glyphRunMetrics ??= CreateGlyphRunMetrics(); - - return _glyphRunMetrics.Value; - } - } + => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// /// Gets or sets the baseline origin of the. /// public Point BaselineOrigin { - get - { - _baselineOrigin ??= CalculateBaselineOrigin(); - - return _baselineOrigin.Value; - } + get => _baselineOrigin ??= CalculateBaselineOrigin(); set => Set(ref _baselineOrigin, value); } - /// - /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. - /// - public IReadOnlyList GlyphIndices - { - get => _glyphIndices; - set => Set(ref _glyphIndices, value); - } - - /// - /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. - /// - public IReadOnlyList? GlyphAdvances - { - get => _glyphAdvances; - set => Set(ref _glyphAdvances, value); - } - - /// - /// Gets or sets an array of values representing the offsets of the glyphs in the . - /// - public IReadOnlyList? GlyphOffsets - { - get => _glyphOffsets; - set => Set(ref _glyphOffsets, value); - } - /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// @@ -148,12 +159,16 @@ namespace Avalonia.Media } /// - /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// Gets or sets the list of glyphs to use to render this run. /// - public IReadOnlyList? GlyphClusters + public IReadOnlyList GlyphInfos { - get => _glyphClusters; - set => Set(ref _glyphClusters, value); + get => _glyphInfos; + set + { + Set(ref _glyphInfos, value); + _hasOneCharPerCluster = false; + } } /// @@ -179,17 +194,7 @@ namespace Avalonia.Media /// The platform implementation of the . /// public IGlyphRunImpl GlyphRunImpl - { - get - { - if (_glyphRunImpl == null) - { - Initialize(); - } - - return _glyphRunImpl!; - } - } + => _glyphRunImpl ??= CreateGlyphRunImpl(); /// /// Obtains geometry for the glyph run. @@ -221,38 +226,32 @@ namespace Avalonia.Media if (IsLeftToRight) { - if (GlyphClusters != null) + if (characterIndex < Metrics.FirstCluster) { - if (characterIndex < Metrics.FirstCluster) - { - return 0; - } + return 0; + } - if (characterIndex > Metrics.LastCluster) - { - return Metrics.WidthIncludingTrailingWhitespace; - } + if (characterIndex > Metrics.LastCluster) + { + return Metrics.WidthIncludingTrailingWhitespace; } var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null) - { - var currentCluster = GlyphClusters[glyphIndex]; + var currentCluster = _glyphInfos[glyphIndex].GlyphCluster; - //Move to the end of the glyph cluster - if (characterHit.TrailingLength > 0) + //Move to the end of the glyph cluster + if (characterHit.TrailingLength > 0) + { + while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster) { - while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster) - { - glyphIndex++; - } + glyphIndex++; } } for (var i = 0; i < glyphIndex; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return distance; @@ -262,22 +261,19 @@ namespace Avalonia.Media //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null && GlyphClusters.Count > 0) + if (characterIndex > Metrics.LastCluster) { - if (characterIndex > Metrics.LastCluster) - { - return 0; - } + return 0; + } - if (characterIndex <= Metrics.FirstCluster) - { - return Size.Width; - } + if (characterIndex <= Metrics.FirstCluster) + { + return Size.Width; } - for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++) + for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return Size.Width - distance; @@ -322,11 +318,12 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; if (distance > currentX && distance <= currentX + advance) { @@ -340,11 +337,12 @@ namespace Avalonia.Media { currentX = Size.Width; - for (var index = GlyphIndices.Count - 1; index >= 0; index--) + for (var index = _glyphInfos.Count - 1; index >= 0; index--) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; var offsetX = currentX - advance; @@ -424,7 +422,7 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { - if (GlyphClusters == null || GlyphClusters.Count == 0) + if (_hasOneCharPerCluster) { return characterIndex; } @@ -433,7 +431,7 @@ namespace Avalonia.Media { if (IsLeftToRight) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return 0; @@ -446,15 +444,13 @@ namespace Avalonia.Media return 0; } - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } - var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; - - var clusters = GlyphClusters; + var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer; // Find the start of the cluster at the character index. - var start = clusters.BinarySearch(characterIndex, comparer); + var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); // No cluster found. if (start < 0) @@ -463,40 +459,38 @@ namespace Avalonia.Media { characterIndex--; - start = clusters.BinarySearch(characterIndex, comparer); + start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); } if (start < 0) { - goto result; + return 0; } } if (IsLeftToRight) { - while (start > 0 && clusters[start - 1] == clusters[start]) + while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start--; } } else { - while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start]) + while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start++; } } - result: - if (start < 0) { return 0; } - if (start > GlyphIndices.Count - 1) + if (start > _glyphInfos.Count - 1) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return start; @@ -516,14 +510,14 @@ namespace Avalonia.Media var glyphIndex = FindGlyphIndex(index); - if (GlyphClusters == null) + if (_hasOneCharPerCluster) { - width = GetGlyphAdvance(index, out _); + width = _glyphInfos[index].GlyphAdvance; return new CharacterHit(glyphIndex, 1); } - var cluster = GlyphClusters[glyphIndex]; + var cluster = _glyphInfos[glyphIndex].GlyphCluster; var nextCluster = cluster; @@ -531,13 +525,13 @@ namespace Avalonia.Media while (nextCluster == cluster) { - width += GetGlyphAdvance(currentIndex, out _); + width += _glyphInfos[currentIndex].GlyphAdvance; if (IsLeftToRight) { currentIndex++; - if (currentIndex == GlyphClusters.Count) + if (currentIndex == _glyphInfos.Count) { break; } @@ -552,7 +546,7 @@ namespace Avalonia.Media } } - nextCluster = GlyphClusters[currentIndex]; + nextCluster = _glyphInfos[currentIndex].GlyphCluster; } var clusterLength = Math.Max(0, nextCluster - cluster); @@ -565,9 +559,9 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (int i = 1; i < GlyphClusters.Count; i++) + for (int i = 1; i < _glyphInfos.Count; i++) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -583,9 +577,9 @@ namespace Avalonia.Media } else { - for (int i = GlyphClusters.Count - 1; i >= 0; i--) + for (int i = _glyphInfos.Count - 1; i >= 0; i--) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -613,26 +607,6 @@ namespace Avalonia.Media return new CharacterHit(cluster, clusterLength); } - /// - /// Gets a glyph's width. - /// - /// The glyph index. - /// The current cluster. - /// The glyph's width. - private double GetGlyphAdvance(int index, out int cluster) - { - cluster = GlyphClusters != null ? GlyphClusters[index] : index; - - if (GlyphAdvances != null) - { - return GlyphAdvances[index]; - } - - var glyph = GlyphIndices[index]; - - return GlyphTypeface.GetGlyphAdvance(glyph) * Scale; - } - /// /// Calculates the default baseline origin of the . /// @@ -644,20 +618,17 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { - int firstCluster = 0, lastCluster = 0; + int firstCluster, lastCluster; - if (_glyphClusters != null && _glyphClusters.Count > 0) + if (Characters.IsEmpty) { - firstCluster = _glyphClusters[0]; - lastCluster = _glyphClusters[_glyphClusters.Count - 1]; + firstCluster = 0; + lastCluster = 0; } else { - if (!Characters.IsEmpty) - { - firstCluster = 0; - lastCluster = Characters.Length - 1; - } + firstCluster = _glyphInfos[0].GlyphCluster; + lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster; } if (!IsLeftToRight) @@ -671,9 +642,9 @@ namespace Avalonia.Media var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out _); + var advance = _glyphInfos[index].GlyphAdvance; widthIncludingTrailingWhitespace += advance; } @@ -684,14 +655,14 @@ namespace Avalonia.Media { for (var index = 0; index < glyphCount; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } else { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } @@ -710,7 +681,7 @@ namespace Avalonia.Media { if (isReversed) { - return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); + return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } glyphCount = 0; @@ -720,84 +691,59 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = charactersSpan.Length - 1; i >= 0;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); - - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; + var characterIndex = charactersSpan.Length - 1; - i -= count; - glyphCount++; - } - } - else + for (var i = _glyphInfos.Count - 1; i >= 0; i--) { - var characterIndex = charactersSpan.Length - 1; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = GlyphClusters.Count - 1; i >= 0; i--) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex -= characterLength; - characterIndex -= characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + while (i - 1 >= 0) + { + var nextCluster = _glyphInfos[i - 1].GlyphCluster; - while (i - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[i - 1]; + clusterLength++; + i--; - if (currentCluster == nextCluster) + if(characterIndex >= 0) { - clusterLength++; - i--; - - if(characterIndex >= 0) - { - codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); + codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - characterIndex -= characterLength; - } - - continue; + characterIndex -= characterLength; } - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount++; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } return trailingWhitespaceLength; } - private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) { glyphCount = 0; newLineLength = 0; @@ -806,71 +752,46 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = 0; i < charactersSpan.Length;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); + var characterIndex = 0; - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; - - i += count; - glyphCount++; - } - } - else + for (var i = 0; i < _glyphInfos.Count; i++) { - var characterIndex = 0; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = 0; i < GlyphClusters.Count; i++) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex += characterLength; - characterIndex += characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + var j = i; - var j = i; + while (j - 1 >= 0) + { + var nextCluster = _glyphInfos[--j].GlyphCluster; - while (j - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[--j]; - - if (currentCluster == nextCluster) - { - clusterLength++; + clusterLength++; - continue; - } - - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount += clusterLength; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount += clusterLength; } } @@ -890,44 +811,17 @@ namespace Avalonia.Media field = value; } - /// - /// Initializes the . - /// - private void Initialize() + private IGlyphRunImpl CreateGlyphRunImpl() { - if (GlyphIndices == null) - { - throw new InvalidOperationException(); - } - - var glyphCount = GlyphIndices.Count; - - if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount) - { - throw new InvalidOperationException(); - } - - if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount) - { - throw new InvalidOperationException(); - } - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); } public void Dispose() { _glyphRunImpl?.Dispose(); - } - - private class ReverseComparer : IComparer - { - public int Compare(T? x, T? y) - { - return Comparer.Default.Compare(y, x); - } + _glyphRunImpl = null; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs new file mode 100644 index 0000000000..36a07721a6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a single glyph. + /// + public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default) + { + internal static Comparer ClusterAscendingComparer { get; } = + Comparer.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster)); + + internal static Comparer ClusterDescendingComparer { get; } = + Comparer.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster)); + + /// + /// Get the glyph index. + /// + public ushort GlyphIndex { get; } = GlyphIndex; + + /// + /// Get the glyph cluster. + /// + public int GlyphCluster { get; } = GlyphCluster; + + /// + /// Get the glyph advance. + /// + public double GlyphAdvance { get; } = GlyphAdvance; + + /// + /// Get the glyph offset. + /// + public Vector GlyphOffset { get; } = GlyphOffset; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index b518d47a6d..6bfcfc06f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -111,7 +111,7 @@ namespace Avalonia.Media.TextFormatting shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } - glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; + glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; } currentPosition += textRun.Length; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 41bba2cd09..f29bdd4459 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,14 +1,13 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - public sealed class ShapedBuffer : IList, IDisposable + public sealed class ShapedBuffer : IReadOnlyList, IDisposable { - private static readonly IComparer s_clusterComparer = new CompareClusters(); - private GlyphInfo[]? _rentedBuffer; public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting public sbyte BidiLevel { get; } public bool IsLeftToRight => (BidiLevel & 1) == 0; - - public IReadOnlyList GlyphIndices => new GlyphIndexList(GlyphInfos); - - public IReadOnlyList GlyphClusters => new GlyphClusterList(GlyphInfos); - - public IReadOnlyList GlyphAdvances => new GlyphAdvanceList(GlyphInfos); - - public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); public ReadOnlyMemory Text { get; } @@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting } - var comparer = s_clusterComparer; + var comparer = GlyphInfo.ClusterAscendingComparer; - var clusters = GlyphInfos.Span; + var glyphInfos = GlyphInfos.Span; - var searchValue = new GlyphInfo(0, characterIndex); + var searchValue = new GlyphInfo(default, characterIndex, default); - var start = clusters.BinarySearch(searchValue, comparer); + var start = glyphInfos.BinarySearch(searchValue, comparer); if (start < 0) { @@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting { characterIndex--; - searchValue = new GlyphInfo(0, characterIndex); + searchValue = new GlyphInfo(default, characterIndex, default); - start = clusters.BinarySearch(searchValue, comparer); + start = glyphInfos.BinarySearch(searchValue, comparer); } if (start < 0) @@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting } } - while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) + while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster) { start--; } @@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(this, null); } - var firstCluster = GlyphClusters[0]; - var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + var firstCluster = GlyphInfos[0].GlyphCluster; + var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster; @@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(first, second); } - int ICollection.Count => throw new NotImplementedException(); - - bool ICollection.IsReadOnly => true; + int IReadOnlyCollection.Count => GlyphInfos.Length; public GlyphInfo this[int index] { @@ -144,130 +133,9 @@ namespace Avalonia.Media.TextFormatting set => GlyphInfos[index] = value; } - int IList.IndexOf(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.Insert(int index, GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.RemoveAt(int index) - { - throw new NotImplementedException(); - } - - void ICollection.Add(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.Clear() - { - throw new NotImplementedException(); - } - - bool ICollection.Contains(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.CopyTo(GlyphInfo[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection.Remove(GlyphInfo item) - { - throw new NotImplementedException(); - } public IEnumerator GetEnumerator() => GlyphInfos.GetEnumerator(); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - - private class CompareClusters : IComparer - { - private static readonly Comparer s_intClusterComparer = Comparer.Default; - - public int Compare(GlyphInfo x, GlyphInfo y) - { - return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster); - } - } - - private readonly struct GlyphAdvanceList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphAdvanceList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public double this[int index] => _glyphInfos[index].GlyphAdvance; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphIndexList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphIndexList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public ushort this[int index] => _glyphInfos[index].GlyphIndex; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphClusterList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphClusterList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public int this[int index] => _glyphInfos[index].GlyphCluster; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphOffsetList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphOffsetList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public Vector this[int index] => _glyphInfos[index].GlyphOffset; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Dispose() { @@ -279,35 +147,4 @@ namespace Avalonia.Media.TextFormatting } } } - - public readonly record struct GlyphInfo - { - public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) - { - GlyphIndex = glyphIndex; - GlyphAdvance = glyphAdvance; - GlyphCluster = glyphCluster; - GlyphOffset = glyphOffset; - } - - /// - /// Get the glyph index. - /// - public ushort GlyphIndex { get; } - - /// - /// Get the glyph cluster. - /// - public int GlyphCluster { get; } - - /// - /// Get the glyph advance. - /// - public double GlyphAdvance { get; } - - /// - /// Get the glyph offset. - /// - public Vector GlyphOffset { get; } - } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 583f2e49f1..d444a58297 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting public override Size Size => GlyphRun.Size; - public GlyphRun GlyphRun - { - get - { - if(_glyphRun is null) - { - _glyphRun = CreateGlyphRun(); - } - - return _glyphRun; - } - } + public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { - if (GlyphRun.GlyphIndices.Count == 0) + if (GlyphRun.GlyphInfos.Count == 0) { return; } @@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < ShapedBuffer.Length; i++) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (currentWidth + advance > availableWidth) { @@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (width + advance > availableWidth) { @@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, Text, - ShapedBuffer.GlyphIndices, - ShapedBuffer.GlyphAdvances, - ShapedBuffer.GlyphOffsets, - ShapedBuffer.GlyphClusters, + ShapedBuffer, BidiLevel); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index f3cc0a714e..8ffe3e5da2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -611,7 +611,7 @@ namespace Avalonia.Media.TextFormatting var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.Typeface.GlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); - var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; + var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 1828f24aff..9f4e96da25 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; namespace Avalonia.Platform @@ -166,11 +167,9 @@ namespace Avalonia.Platform /// /// The glyph typeface. /// The font rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. - /// - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// The list of glyphs. + /// An . + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos); /// /// Creates a backend-specific object using a low-level API graphics context diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 18cb7a6308..32923a5257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -32,7 +32,7 @@ internal class FpsCounter { var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph }); } } diff --git a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs index b7060d2e21..defc9b1639 100644 --- a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs +++ b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs @@ -14,7 +14,6 @@ // under the License. // Copied from: https://github.com/toptensoftware/RichTextKit -using System; using System.Collections.Generic; namespace Avalonia.Utilities @@ -39,7 +38,7 @@ namespace Avalonia.Utilities /// The value to search for /// The comparer /// The index of the found item; otherwise the bitwise complement of the index of the next larger item - public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) where T : IComparable + public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) { return list.BinarySearch(0, list.Count, value, comparer); } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index cb79cc85db..e368ddc373 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -9,6 +9,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Headless { @@ -118,7 +119,7 @@ namespace Avalonia.Headless return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new HeadlessGlyphRunStub(); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b202b60cdf..3fb7491898 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,16 +1,11 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading; - -using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using SkiaSharp; namespace Avalonia.Skia @@ -88,9 +83,9 @@ namespace Avalonia.Skia var (currentX, currentY) = glyphRun.BaselineOrigin; - for (var i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var glyph = glyphRun.GlyphIndices[i]; + var glyph = glyphRun.GlyphInfos[i].GlyphIndex; var glyphPath = skFont.GetGlyphPath(glyph); if (!glyphPath.IsEmpty) @@ -98,14 +93,7 @@ namespace Avalonia.Skia path.AddPath(glyphPath, (float)currentX, (float)currentY); } - if (glyphRun.GlyphAdvances != null) - { - currentX += glyphRun.GlyphAdvances[i]; - } - else - { - currentX += glyphPath.Bounds.Right; - } + currentX += glyphRun.GlyphInfos[i].GlyphAdvance; } return new StreamGeometryImpl(path); @@ -213,17 +201,16 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { if (glyphTypeface == null) { throw new ArgumentNullException(nameof(glyphTypeface)); } - if (glyphIndices == null) + if (glyphInfos == null) { - throw new ArgumentNullException(nameof(glyphIndices)); + throw new ArgumentNullException(nameof(glyphInfos)); } var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; @@ -242,59 +229,25 @@ namespace Avalonia.Skia var builder = new SKTextBlobBuilder(); - var count = glyphIndices.Count; + var count = glyphInfos.Count; - if (glyphOffsets != null && glyphAdvances != null) - { - var runBuffer = builder.AllocatePositionedRun(font, count); + var runBuffer = builder.AllocatePositionedRun(font, count); - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); + var glyphSpan = runBuffer.GetGlyphSpan(); + var positionSpan = runBuffer.GetPositionSpan(); - var currentX = 0.0; + var currentX = 0.0; - for (int i = 0; i < glyphOffsets.Count; i++) - { - var offset = glyphOffsets[i]; - - glyphSpan[i] = glyphIndices[i]; - - positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - - currentX += glyphAdvances[i]; - } - } - else + for (int i = 0; i < count; i++) { - if (glyphAdvances != null) - { - var runBuffer = builder.AllocateHorizontalRun(font, count, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); - - var currentX = 0.0; + var glyphInfo = glyphInfos[i]; + var offset = glyphInfo.GlyphOffset; - for (int i = 0; i < glyphAdvances.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; + glyphSpan[i] = glyphInfo.GlyphIndex; - positionSpan[i] = (float)currentX; + positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - currentX += glyphAdvances[i]; - } - } - else - { - var runBuffer = builder.AllocateRun(font, count, 0, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - - for (int i = 0; i < glyphIndices.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; - } - } + currentX += glyphInfo.GlyphAdvance; } return new GlyphRunImpl(builder.Build()); diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5887ba2172..d9cd0590fc 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -7,6 +7,7 @@ using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; @@ -157,12 +158,11 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; - var glyphCount = glyphIndices.Count; + var glyphCount = glyphInfos.Count; var run = new SharpDX.DirectWrite.GlyphRun { @@ -174,44 +174,23 @@ namespace Avalonia.Direct2D1 for (var i = 0; i < glyphCount; i++) { - indices[i] = (short)glyphIndices[i]; + indices[i] = (short)glyphInfos[i].GlyphIndex; } run.Indices = indices; run.Advances = new float[glyphCount]; - var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight); - - if (glyphAdvances == null) - { - for (var i = 0; i < glyphCount; i++) - { - var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale; - - run.Advances[i] = advance; - } - } - else - { - for (var i = 0; i < glyphCount; i++) - { - var advance = (float)glyphAdvances[i]; - - run.Advances[i] = advance; - } - } - - if (glyphOffsets == null) + for (var i = 0; i < glyphCount; i++) { - return new GlyphRunImpl(run); + run.Advances[i] = (float)glyphInfos[i].GlyphAdvance; } run.Offsets = new GlyphOffset[glyphCount]; for (var i = 0; i < glyphCount; i++) { - var (x, y) = glyphOffsets[i]; + var (x, y) = glyphInfos[i].GlyphOffset; run.Offsets[i] = new GlyphOffset { @@ -254,11 +233,12 @@ namespace Avalonia.Direct2D1 using (var sink = pathGeometry.Open()) { - var glyphs = new short[glyphRun.GlyphIndices.Count]; + var glyphInfos = glyphRun.GlyphInfos; + var glyphs = new short[glyphInfos.Count]; - for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (int i = 0; i < glyphInfos.Count; i++) { - glyphs[i] = (short)glyphRun.GlyphIndices[i]; + glyphs[i] = (short)glyphInfos[i].GlyphIndex; } glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 363fb3f5b3..3573ba6b07 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System; +using System.Linq; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Utilities; @@ -179,12 +181,14 @@ namespace Avalonia.Base.UnitTests.Media private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0) { var count = glyphAdvances.Length; - var glyphIndices = new ushort[count]; - var characters = Enumerable.Repeat('a', count).ToArray(); + var glyphInfos = new GlyphInfo[count]; + for (var i = 0; i < count; ++i) + { + glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); + } - return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, - glyphClusters: glyphClusters, biDiLevel: bidiLevel); + return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel); } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 42e33729ac..93d97e6cfb 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Base.UnitTests.VisualTree { @@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a272d89b8a..a802cd0958 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks @@ -120,7 +121,7 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new MockGlyphRun(); } diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 31e485448e..772d6e1023 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Documents; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Xunit; #if AVALONIA_SKIA @@ -170,11 +171,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale; - var advances = new[] { advance, advance, advance}; + var glyphInfos = new[] + { + new GlyphInfo(glyphIndices[0], 0, advance), + new GlyphInfo(glyphIndices[1], 1, advance), + new GlyphInfo(glyphIndices[2], 2, advance) + }; var characters = new[] { 'A', 'B', 'C' }; - GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices, advances); + GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphInfos); } public GlyphRun GlyphRun { get; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 04bc401479..f2d6670be5 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -134,11 +134,11 @@ namespace Avalonia.Skia.UnitTests.Media foreach (var rect in rects) { - var currentCluster = glyphRun.GlyphClusters[index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphClusters.Count) + while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphInfos.Count) { - currentCluster = glyphRun.GlyphClusters[++index]; + currentCluster = glyphRun.GlyphInfos[++index].GlyphCluster; } //Non trailing edge @@ -161,15 +161,15 @@ namespace Avalonia.Skia.UnitTests.Media var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace; - var rects = new List(glyphRun.GlyphAdvances!.Count); + var rects = new List(glyphRun.GlyphInfos!.Count); var lastCluster = -1; - for (var index = 0; index < glyphRun.GlyphAdvances.Count; index++) + for (var index = 0; index < glyphRun.GlyphInfos.Count; index++) { - var currentCluster = glyphRun.GlyphClusters![index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - var advance = glyphRun.GlyphAdvances[index]; + var advance = glyphRun.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { @@ -216,10 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, shapedBuffer.Text, - shapedBuffer.GlyphIndices, - shapedBuffer.GlyphAdvances, - shapedBuffer.GlyphOffsets, - shapedBuffer.GlyphClusters, + shapedBuffer.GlyphInfos, shapedBuffer.BidiLevel); if(shapedBuffer.BidiLevel == 1) @@ -233,7 +230,7 @@ namespace Avalonia.Skia.UnitTests.Media private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index b90752861c..6b9fb579b1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -585,7 +585,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedRuns = expectedTextLine.TextRuns.Cast().ToList(); - var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var expectedGlyphs = expectedRuns + .SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); for (var i = 0; i < text.Length; i++) { @@ -604,7 +606,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var actualGlyphs = shapedRuns + .SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); Assert.Equal(expectedGlyphs, actualGlyphs); } @@ -706,7 +710,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl())); AvaloniaLocator.CurrentMutable diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 1fd26748cd..a24a0fcf70 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -142,8 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting black, textWrapping: TextWrapping.Wrap); - var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var expectedGlyphs = GetGlyphs(expected); var outer = new GraphemeEnumerator(text); var inner = new GraphemeEnumerator(text); @@ -175,8 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textWrapping: TextWrapping.Wrap, textStyleOverrides: spans); - var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var actualGlyphs = GetGlyphs(actual); Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count); @@ -196,6 +194,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting i += outer.Current.Text.Length; } } + + static List GetGlyphs(TextLayout textLayout) + => textLayout.TextLines + .Select(line => string.Join('|', line.TextRuns + .Cast() + .SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex))) + .ToList(); } [Fact] @@ -484,13 +489,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters; + var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray(); - var expected = clusters.Skip(index).Take(glyphClusters.Count).ToArray(); + var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray(); Assert.Equal(expected, glyphClusters); - index += glyphClusters.Count; + index += glyphClusters.Length; } } } @@ -515,13 +520,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, layout.TextLines[0].TextRuns.Count); - Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count); + Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count); - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster); if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster); } } } @@ -562,9 +567,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); - foreach (var glyph in textRun.GlyphRun.GlyphIndices) + foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos) { - Assert.Equal(replacementGlyph, glyph); + Assert.Equal(replacementGlyph, glyphInfo.GlyphIndex); } } } @@ -776,8 +781,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width); } - var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast()) - .SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray(); + var rects = layout.TextLines + .SelectMany(x => x.TextRuns.Cast()) + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance) + .ToArray(); for (var i = 0; i < SingleLineText.Length; i++) { @@ -865,10 +872,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentX = 0.0; - for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < firstRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = firstRun.GlyphRun.GlyphClusters[i]; - var advance = firstRun.GlyphRun.GlyphAdvances[i]; + var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -895,10 +902,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentX = firstRun.Size.Width + 0.5; - for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < secondRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = secondRun.GlyphRun.GlyphClusters[i]; - var advance = secondRun.GlyphRun.GlyphAdvances[i]; + var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -932,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = (ShapedTextRun)textLine.TextRuns[0]; - var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; + var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster; var characterHit = textLine.GetCharacterHitFromDistance(0); @@ -946,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); - var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; + var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance; Assert.Equal(firstAdvance, distance, 5); @@ -991,9 +998,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); + var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList(); - var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList(); + var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList(); var currentX = 0.0; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 6993c70e8b..544b84912e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -95,9 +95,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); @@ -142,9 +142,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } clusters.Reverse(); @@ -247,7 +247,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) + var clusters = textLine.TextRuns + .Cast() + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster) .ToArray(); var previousCharacterHit = new CharacterHit(text.Length); @@ -313,11 +315,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = textRun.GlyphRun; - for (var i = 0; i < glyphRun.GlyphClusters!.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var cluster = glyphRun.GlyphClusters[i]; + var cluster = glyphRun.GlyphInfos[i].GlyphCluster; - var advance = glyphRun.GlyphAdvances[i]; + var advance = glyphRun.GlyphInfos[i].GlyphAdvance; var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); @@ -750,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - var currentClusters = shapedBuffer.GlyphClusters.ToList(); + var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList(); foreach (var currentCluster in currentClusters) { @@ -783,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) + for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++) { - var currentCluster = shapedBuffer.GlyphClusters[index]; + var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster; - var advance = shapedBuffer.GlyphAdvances[index]; + var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 834dce4a90..3f02867aa9 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -19,10 +19,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); - Assert.Equal(0, shapedBuffer.GlyphClusters[0]); - Assert.Equal(1, shapedBuffer.GlyphClusters[1]); - Assert.Equal(1, shapedBuffer.GlyphClusters[2]); + Assert.Equal(shapedBuffer.GlyphInfos.Length, text.Length); + Assert.Equal(0, shapedBuffer.GlyphInfos[0].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[1].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[2].GlyphCluster); } } @@ -36,7 +36,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(100, shapedBuffer.GlyphAdvances[0]); + Assert.Equal(100, shapedBuffer.GlyphInfos[0].GlyphAdvance); } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index f9e1e45098..d56e360e22 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -4,6 +4,7 @@ using System.IO; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Rendering; using Moq; @@ -146,7 +147,7 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return Mock.Of(); } diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 407b67b8a0d49afd3cd0f616c45c01d9fd480222..547e9b276380f69573a79583543ae3419d6a4c0f 100644 GIT binary patch literal 4222 zcmbuD_dgVlq-?gRwfx=G} zVod!{q13Q=5)uY!h`Ne#VE%rAfd}WTzw%hY1INLDG|6!)@ucQ585zSr{NLI@9#x3A z4~c>{rPxa9bU|xr6E{(a2Bk*0#^2_L+5@`c-%yw+QC1f+@6)U<Pj3O*xh#eu__8-l}SQ5Yg ze<3Ug+M_WO1G;M=U+{dWjptq|j^`lNsb*XuVOU{UnQFHEt463jed5GRaigI4u|(GM z&#lQzuicbJ-#{}z%W_V2S*?{#z;ZH&Y76nq4Ofc(^-P1EQ4cV8CEBq#3wXp1bSIE{ zawghp`jCD1Iq%qNxh9;L(^XaKdl}GDV%`*}`A-JXTOHuHW}W%+A7pS78FY0GI1+fo z3=0#zM{|}krN9!^x-nDCRlt=P<$cboM}Rf{+Xsh9yfF-z!k{h2JeN(&-4CanJaIUBgLV>jj^q$r9S z!yPBoecqDu7BIcXJPsJQ^1ENN+?fIgLX_CtFwmUB`z8pzto}2rm9LyvX``x9pFlZ$) z>^%NfLfg@WMQ=Sc{LmjvYA*K447mQEq9{QC{63OT5wZ0&OctGula^OfZ3pU|;=9g) zRi4R!?Wd-CN>*ANY;()!Xe7x0ns4{R6Z4LRZboz2g)b!*hDfXdpiA_)GJ(I9W% z^LC$rD4QSI`^CFWu<$t4`*9u{aXbe(>lKS3;p}T^?b~WFaI@gm1Y(+3{4HzbvAhMV z#8KRpWCO)~61%v4hwNt&hN&Ep{PAxL6gEC>ZagYI7X6Tb>uYKF&vIY#GqtWJXLfj5 zBTCyY7Dy#_!R_7on8xWgx@W&k03q1eFjBB3=PGbfPp?WHGn4OX@m-kJeA})os(~ue z^v>bZ2y)(D^o1L~6zw&_Wi%0J`;@V9FX58CCzjpFPH7Dgr9u}DeWHcM$uk?ZTR_cp zxBYF9&jx3!A~^Fj6K!dc;yN1dhW3WKD3oOUdY`+n5U#bEfLniXU-*42pD&f&7iNu| zFGb4Y=ndDhGuWvoR0fdm0`{(D;OWuLXf^pZ;m&^djrH>;Gn|*CTx0frI9u_u5CQ$_ zFJP%2f4OpxIQ8EtO2LAt2i5T@cSyNUjQEh8(EOH^<~rh-&dR~>=Nl}`;$j}p^^u|! zTk;}eLcnD za|QC)*q&Vzpt!NN>Rob>e;ZOR0i`tX@~qw4XsQNM+lp0O$jw@RKptN_;eKPldF%a`1-pKrPh6-|~LjvxU3+IO^Ru`OnHShO8;*s*eGw|p#TTFF^ZPfNAc{{hD zzoE`;BTetVrG;F_rA2#tIag^@?DK|$tPTo1>^l^@z&d`!1MS9lol-zr0|kEqD?dnU zC#!I~`#HbA%w-wXX;R*8o9r)i`fu#$Z&%&McCK9NaUm{cRd)q)|8X!HPnmALP20x! z73`FI9y23ZVQkt9)`qvyf}XUZqgqN`wcc4$+N4%x$Yf>*s5=O*Fql|mvM}WVUh}t= zazE<_r^vu9XZF|hiby>q=M|w>+fIEh{`{OaKA{yGZqJ0GyDzf&b;$1plkAdniazmW zx&l}dHKTINwky6(A-pE;eeUb@g)v_kXFFYkBEh<5;C-;D0Lzm8IFc=tSC5--d!()t z*UxzSHg-g6#fKZ`s?|IuZO@bwmdlhyACa7GXA{d0=IC^VZRP~JdG<>luxZKu>G@LD z$X-(A<=qvM)?fl6EDg7Is-_|Pz>rht)oM%zP2MyIr?&UTj@kSr?qOSvPv6J9)tD_g zxf9l5gEx+CtfhXH0&@MwZB_J!?snG zhSp0DkyMp&XCE_BKI5;z&mwROJ3&cNh5@Nf65X>wC~Qx)NX0Ile1=+V=HaMLL1G^? zI#wXMU2bu-Uf}b)K1K5A)~HO%`oiT{gwn?MCyp%r1mSGfT)Oafmn?M6z;C-<1*ID9 z4L4mT`xDA!L>Xq1h7>w3_jcx0Zgx_J!h;ibr^%O+g9}sdl0fi^^UXH|wYH`8dRb%4 z#En@cEKPi##=feut(RcC<}pZyiWWm3uf1gbEMv+z(A@{7xs%+)aFrTk9I#J<(YbNR zO@}nETWyUUWh)NU_33OlP=#G2)P<%czWh3r?HfiDo&;_O2elV&-49Bwu4W4HOJ}K{ zlmBtcuG2xG+hV%4xNC_T#IiX`hDV7X)E~*|eMu;khHy~_KFE7c-uUNh{-3efI_Zu< zx{bc4oxYVtr2{Rryq{E}s5sj(8IL2rRb!lU8ZvV}q%Y5t9p=3dBb`*BXC`wFmh}=C zFh0`9BXe<9m;Dr3=xqkO^yPrfLHm-tPBVXTN+BRR{($hVt=+iC*v`C4d}->t zWzkl17M_8^!vn&b(w@V<0y(W`*yp--NuWmNv36-*rtTYIC1@bx_a{kd`0`mEnbj*y z(U!dkFY~v?^!`A8?MW}j0iCs!D1Af^X9yo3=vBGMN_p5W+bIXDHBkI_fU`*Jjl1op z%a<1w&2aATS_=W~i-x%2%N9laU;N_hFG@p{a?R??iW;q6*@rkX_Pl0zyqma=pf24j zc%avBma99Me)SCN-;US(P((;BzB>Kj&H)KLrQjuZ@nH8)0gHG00*+QcxeqYEGuoRW zJ$xlR9b+8UuVYkMf6MsO(>2EaP0omUr|^_d(A-V1wXm!@Ewd5q)Wnq!7CCuv!9HuM zBjxwQkm5`$Q9N35eeUiPhn+=9m5_&gc5Qar^y|gi|5}1h$)&ZOy%E!Y*!p8!m!uwY zP(1KVs+a+nWOHUKt)DYRlj6RHXa&~hhns#Yd5Q2sOBnAsC%sXpazNlm#cl}=e2UNz zi4z~Ys7vZ)`s~~dwnBrRQzMxxdOyPGN4TUdrM(zW*Rcr?*@8WK{R}fHxdavHI?S(z zZGYsyuA~8s6dVrLSUO>)EwZLhiD#|C4`6K_`^kE$%b8<3xqr z7TspQ67RHG!d8fo1Kl5-8neVQ17gKZc#dFQT52S(6MCe;eA9~e705i(aIU?tq!BO4 zEQ|~aT=jO;pcTDRa0|^45xV?C_+q;{$!*?UTpDyLYmb{?q!bmi9PG0%RC)vCRyWs8< z4Y1}FO|vO*Rm)j^hPLdl5#6M)3x=zT#$6Ek@O=&&TfNe7sCPo__qz|G z6COUpV(N~6q2mqXgyKcF7gSL7q3PUPW~iM!=xX2OgXZ@!Gny^$tD~nVav?+WOT;c`QhYxKX8L5zh@x@FCM^rw-^nBj`TH=Dt&0i#M%!8~&+Mr)k;w zHG$J=In$FhZ2JV>LiF@1+vmG08Ar+IS-QPTQO~o6hiA-QzSy8a75m6GLDFlv;LaIISh5#3JS*cv=77M}$AXBfaqkujq7{$Yc6ic)Fo>NeM_e!uL*Qu0MWmZYg zVOzMaX#MT8R8u)cq$L^UR1;J&JZJNDnAgxQ*nS-Lt(Iq_VN6guQO%#7?V?$MkLc?C@G!9B4|qU?mY#;t!(A1SIb zxQ|eBWhoV4V(<;=Wxt$qK;hl@qtn#g*rMb))YyUc!9qyx8Ybf*2x%v z?ju@28OIktww&-ZrDN+=b=kc^lJ*fV6KG(s(MjEo2vI_UC6pBV^l zPcGq}FiMbLb1tm*XSs2lCmS#(V*U7<`*6{-*@x@Iw+fHnd9`=?m*B8-A*T;TFOBkH zNqYS9fo&~f0O?BhUcxA4iu5_jEZHq5!}ZIT%3x+y4R@XutpyWzn_BhsNq~iz+yM#k zGNJWjks|G?)WK~=y<+A38TEK?cE+jJETEzX+09#bvGxDk>V|x~A&*d7Cy9!yIl7Sdso@G zEHFzuBdh&~K^ppH`=^)1fq_eWnqMA#D`)h4odK%rYAFbo5()Ib# zDukn7jP?&Vh@kf8gl@T*H*{Ckj!NhL+bdOn2xC%AIsRG}c9kZJM%OjdCTPBI6hcQ6 zU7+WQHwIZSS*HTrC$~=(aO^AQp8B;}c~*2+>GIqy;lVFUmqGn!o!o})=@-Zd8R?bl zY}cHbV_wGCXu#yIJf4k|qqAq~&@UEG&g--ed1|dx+dWf& z&m_~-W1-P%QLjF3*GfjHY+RAGe|sj@&8y{>Y-v{^n^Mi4}mB8K~ z4QHTzRdpPq+^N{J$KRvF*466-$-H2bW(aM<3(HuT+Qlk5N)0I9$NZ@#Ga$N5BR|k( zHxFJ2*C_1r4G9Zqz5Yt$E4-D(y5mQM*YmN7U@3c1sIO2`u4g%vUKmxNS3X}^DuY&K zu&?;J^^+aaUFp(lGbATYn58Ah3-gOn$;z7Pm`#e21@l;8j)u)Y3QhEfWKOmh51Ho~ z`4=X30TiTU3iygIL^{)XE|_Teai7oAme%8phB;P_}B8lVS1v z=7M@vnsf4}gBqkOUT+GvkNuZvG5LM)ZtEd`J$(1sU}=lN7`w(1JwIUAGK9+A{;9*k z-Ox@hAEu^)S`N3`Lzypq2;z2#@UvgW1xdxpj7cDWEJCv{+l^i2_kh43BPp$@;1|*O zW@yv66Kwl-gtJIoTjSHbsnxYVHtv@DBRYOc@)AU_q>iqDIha~2(DwPVdZ{PcX1(~1 z*r(KBA?{sPpv6lJuyy9`bg#{%?4RxIXr*H9Aa7aMT==1?AD1r~w}IP?sCzWdxl z-i!zY?+}L6i~zqcG6*8f0XsUZQcu75>FZ@tujffHY4 zacn+QRFlR#rxbsSt$(MsvI^O`YB(|E;)uTB;J?kO#lkvL5rY))`%Dz66 z`Ju$bCd%wf9f?^%h6n>hn&Hg+r2F<(45IDSeognKOxsqD>-s%Eu+~hQN3Z*G70>Zh zB}hfS0dqH=2cSdvK6}#Lo{8Q}h9k&nuMnYmT@XxuB@U4+M{7=LJdVA`Mre8C<*EA| zz`0Ta5`+sYe1;&$kGIkg&Q1{ivPqoEH|E|sPLN0f&4yHpsU`eIZ!N~t_OF*xT|xs? zX(Jr%6PIP@5Eg5yB=HoQ8!gtx^@lfh)M1f?V@cvGu%}hL(t>X8G^4vz)bJ{ie} zdk_D5+gAlEZNT4z$mW z=>$|%%bu92iF1F)&?4t?Ou&M!#1a5qZH&gy(wc{5017=(JFYGsrT3=9Zr|A4#4gp` z%@_2vcw%hnwPs}F`O)BuQDQ@!Cg|K${A7vmWD4r9B(K|AUM9!~6B(Y)as3S$mAG8$w3syjF`91VAsR8kk7F&@7qtc z43;9!RJ=o8PU?7g>t^)Tir34^Mrhi?_Eq)Riw;UvG9RbEEj(xB&5-du zb=s*h+Z~u1dmao)dN7I07>=G)ggTJ7g=WQo3L96oYN!Bci9jXH@&x{!Pw2dsgG7XA zy_3M%E=trL@fgDIsG)ED<4?~s!8QT^0_=jitOe}Ls%ncA^Vb3MPS*ltu{4LnPT~;n zR!M8aU$N^jW4RqVPV8z`Qa{k$F8D3sVVMm7Ve|Kd-L3bm=t!c|kinAxNK(If%mQ!K zrphS|RJ3n;;+8l7AB6QFnLz9G-MPSir~oGb7%HdhKLce^9x*1bI!@>1SKRTjGI*6# zEPJ4(nO=Bdkobns`$_a%O80|Hbjj=a<$GS)(j%Yd06HFKVft+^NA1t*XC?g2Q;ffC zow~b*>we2aVCf(LHQ$j!>dBUj~ubO}v(a0^T#l3s*%hMsQ$=1?8 z`5fM&c4@TgKVVDwWKeVg)M#^>KITVc{_@@MTQl&CMq(qGVtdC^<{{ilVuM6d3SVge+^Pj7vNW--AJb$vas zX2YpWD;E(b$ z>uS<|it#6?#l0wkM(49b5|-XhV?VCW98qI5MHKKOe06Xn@Jw`8hkavJe}`1(>(8DA z>55be`P=i##QF-Lax`D8@*S+2q8JF!9dm^CRZX86|ajl(g%J zStfco&0FL%`K<61;T+&ap78Wo)U~dG2>~RkzcSURR~VN1^MQ%v{61-MByGB5h{{jI zv9B?MPvf#PyqWpFik^Vn)x@5d$0!rOnZ}RT{z+HH4xq(hg#yKGcPUxLSlw zJxlgOkYDpDrFiB-PCr33~sUx5ZgozNAR=Vb9}a z4p9{c^Dek3L<>gfTei%$phyuo#ZR+wyEM^4&L1e{K$24KJd2`YyZj+y@799^cUQyG zASaI3or2y1@)M!^u3q3q6r+>N;@HU(6CQ_CLk}XsBLLl0eJ*sN__=ZPoGP!YO~kIB=W zvcD0pe0_S*P*m^W4K=mHPhv}O>rjcArEo|_i-mDL_)|(FE9zQ5BT(9`E2X(jcc$qm z2vqOl#+}{kJUPm|v6K`$=jpxqLt+Gv+`jJvxxy+qW0ff>iMFvPziw7X5_-0*wwCYc zl~ee_v`VA@I?N7~XxoYFV7?J`9%H#~h}0M|TA+7mbZf@mf3IA9T%k5qXy$EhD--5h z$-fXu!FG?`klwu~U*a3uxPzM=Up}^hRbDW&dTudkynXjkGw($}d^V_^QcEJ8K_Jb= z?46&tL^D%-Zqo4H9P1Fl%d0f2;eQWylFrje6?grNI+Rh|3>GHd2wU0&xujz0XD;cf zLo>oY2~q&M!9)GO;HLifkBkJdFGEg#qe+EkIAzA5jTe-oUN}XgY6?ynChbkgHk{(= zv6$twf^P}k0wG~ZFAiYM(o`mipAH&rDC0r|-MFS_4M1qT#I;!ZZV420$Xa(0H@WdCxf(Bd^&gsfPNv z(PUMiZB+Xr!)pj5!Nb4U%f*RCM_=z&6tR64`yu|bgsRVrdzwAQlIf3@e7{_LM7{O Date: Wed, 18 Jan 2023 12:48:46 +0100 Subject: [PATCH 047/326] Added FormattingObjectPool to reduce temp allocs during text layout --- .../Media/TextFormatting/BidiReorderer.cs | 5 + .../TextFormatting/FormattingObjectPool.cs | 135 ++++++++++++++++ .../TextFormatting/InterWordJustification.cs | 3 +- .../Media/TextFormatting/TextCharacters.cs | 4 +- .../TextFormatting/TextEllipsisHelper.cs | 17 +- .../Media/TextFormatting/TextFormatterImpl.cs | 149 +++++++++++------- .../Media/TextFormatting/TextLayout.cs | 25 ++- .../TextLeadingPrefixCharacterEllipsis.cs | 90 ++++++----- .../Media/TextFormatting/TextLineImpl.cs | 26 ++- src/Avalonia.Base/Utilities/ArrayBuilder.cs | 36 ++++- src/Avalonia.Base/Utilities/ArraySlice.cs | 1 - .../Avalonia.UnitTests/MockTextShaperImpl.cs | 5 +- 12 files changed, 359 insertions(+), 137 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 3fcb7bf420..2c6db4b753 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -10,9 +10,14 @@ namespace Avalonia.Media.TextFormatting /// To avoid allocations, this class is designed to be reused. internal sealed class BidiReorderer { + [ThreadStatic] private static BidiReorderer? t_instance; + private ArrayBuilder _runs; private ArrayBuilder _ranges; + public static BidiReorderer Instance + => t_instance ??= new(); + public void BidiReorder(Span textRuns, FlowDirection flowDirection) { Debug.Assert(_runs.Length == 0); diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs new file mode 100644 index 0000000000..0468d8f413 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Contains various list pools that are commonly used during text layout. + /// + /// This class provides an instance per thread. + /// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout. + /// + /// + /// + internal sealed class FormattingObjectPool + { + [ThreadStatic] private static FormattingObjectPool? t_instance; + + /// + /// Gets an instance of this class for the current thread. + /// + /// + /// Since this is backed by a thread static field which is slower than a normal static field, + /// prefer passing the instance around when possible instead of calling this property each time. + /// + public static FormattingObjectPool Instance + => t_instance ??= new(); + + public ListPool TextRunLists { get; } = new(); + + public ListPool UnshapedTextRunLists { get; } = new(); + + public ListPool TextLines { get; } = new(); + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + TextRunLists.VerifyAllReturned(); + UnshapedTextRunLists.VerifyAllReturned(); + TextLines.VerifyAllReturned(); + } + + internal sealed class ListPool + { + // we don't need a big number here, these are for temporary usages only which should quickly be returned + private const int MaxSize = 16; + + private readonly RentedList[] _lists = new RentedList[MaxSize]; + private int _size; + private int _pendingReturnCount; + + /// + /// Rents a list. + /// See for the intended usages. + /// + /// A rented list instance that must be returned to the pool. + /// + public RentedList Rent() + { + var list = _size > 0 ? _lists[--_size] : new RentedList(); + + Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!"); + + ++_pendingReturnCount; + return list; + } + + /// + /// Returns a rented list to the pool. + /// + /// + /// On input, the list to return. + /// On output, the reference is set to null to avoid misuse. + /// + public void Return(ref RentedList? rentedList) + { + if (rentedList is null) + { + return; + } + + --_pendingReturnCount; + rentedList.Clear(); + + if (_size < MaxSize) + { + _lists[_size++] = rentedList; + } + + rentedList = null; + } + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + if (_pendingReturnCount > 0) + { + throw new InvalidOperationException( + $"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!"); + } + + if (_pendingReturnCount < 0) + { + throw new InvalidOperationException( + $"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!"); + } + } + } + + /// + /// Represents a list that has been rented through . + /// + /// This class can be used when a temporary list is needed to store some items during text layout. + /// It can also be used as a reusable array builder by calling when done. + /// + /// + /// NEVER use an instance of this type after it's been returned to the pool. + /// AVOID storing an instance of this type into a field or property. + /// AVOID casting an instance of this type to another type. + /// + /// AVOID passing an instance of this type as an argument to a method expecting a standard list, + /// unless you're absolutely sure it won't store it. + /// + /// + /// If you call a method returning an instance of this type, + /// you're now responsible for returning it to the pool. + /// + /// + /// + /// The type of elements in the list. + internal sealed class RentedList : List + { + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 6bfcfc06f8..7afb758038 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -48,8 +48,9 @@ namespace Avalonia.Media.TextFormatting var currentPosition = textLine.FirstTextSourceIndex; - foreach (var textRun in lineImpl.TextRuns) + for (var i = 0; i < lineImpl.TextRuns.Count; ++i) { + var textRun = lineImpl.TextRuns[i]; var text = textRun.Text; if (text.IsEmpty) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 6454f9bfa3..c1f3816e54 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting /// /// The shapeable text characters. internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties, List results) + ref TextRunProperties? previousProperties, RentedList results) { var properties = Properties; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 97f8b2483b..e6743f5533 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -111,20 +109,17 @@ namespace Avalonia.Media.TextFormatting return new[] { shapedSymbol }; } - // perf note: the runs are very likely to come from TextLineImpl - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + var objectPool = FormattingObjectPool.Instance; - var (preSplitRuns, _) = TextFormatterImpl.SplitTextRuns(textRunArray, collapsedLength); + var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - for (var i = 0; i < preSplitRuns.Count; ++i) - { - collapsedRuns[i] = preSplitRuns[i]; - } + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; return collapsedRuns; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8ffe3e5da2..c25d530472 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,14 +1,16 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { - internal class TextFormatterImpl : TextFormatter + internal sealed class TextFormatterImpl : TextFormatter { private static readonly char[] s_empty = { ' ' }; private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; @@ -23,20 +25,25 @@ namespace Avalonia.Media.TextFormatting var textWrapping = paragraphProperties.TextWrapping; FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - IReadOnlyList textRuns; + IReadOnlyList? textRuns; + var objectPool = FormattingObjectPool.Instance; - var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, + var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); + RentedList? shapedTextRuns; + if (previousLineBreak?.RemainingRuns is { } remainingRuns) { resolvedFlowDirection = previousLineBreak.FlowDirection; textRuns = remainingRuns; nextLineBreak = previousLineBreak; + shapedTextRuns = null; } else { - textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); + textRuns = shapedTextRuns; if (nextLineBreak == null && textEndOfLine != null) { @@ -49,25 +56,32 @@ namespace Avalonia.Media.TextFormatting switch (textWrapping) { case TextWrapping.NoWrap: - { - textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, textSourceLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); + { + // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); - textLine.FinalizeLine(); + textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); - break; - } + textLine.FinalizeLine(); + + break; + } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: - { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, - resolvedFlowDirection, nextLineBreak); - break; - } + { + textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + break; + } default: throw new ArgumentOutOfRangeException(nameof(textWrapping)); } + objectPool.TextRunLists.Return(ref shapedTextRuns); + objectPool.TextRunLists.Return(ref fetchedRuns); + return textLine; } @@ -76,9 +90,12 @@ namespace Avalonia.Media.TextFormatting /// /// The text run's. /// The length to split at. + /// A pool used to get reusable formatting objects. /// The split text runs. - internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length, + FormattingObjectPool objectPool) { + var first = objectPool.TextRunLists.Rent(); var currentLength = 0; for (var i = 0; i < textRuns.Count; i++) @@ -94,8 +111,6 @@ namespace Avalonia.Media.TextFormatting var firstCount = currentRun.Length >= 1 ? i + 1 : i; - var first = new List(firstCount); - if (firstCount > 1) { for (var j = 0; j < i; j++) @@ -108,7 +123,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.Length == length) { - var second = secondCount > 0 ? new List(secondCount) : null; + var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; if (second != null) { @@ -122,13 +137,13 @@ namespace Avalonia.Media.TextFormatting first.Add(currentRun); - return new SplitResult>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(secondCount); + var second = objectPool.TextRunLists.Rent(); if (currentRun is ShapedTextRun shapedTextCharacters) { @@ -144,11 +159,16 @@ namespace Avalonia.Media.TextFormatting second.Add(textRuns[i + j]); } - return new SplitResult>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + for (var i = 0; i < textRuns.Count; i++) + { + first.Add(textRuns[i]); + } + + return new SplitResult>(first, null); } /// @@ -157,14 +177,16 @@ namespace Avalonia.Media.TextFormatting /// The text runs to shape. /// The default paragraph properties. /// The resolved flow direction. + /// A pool used to get reusable formatting objects. /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, + private static RentedList ShapeTextRuns(IReadOnlyList textRuns, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var shapedRuns = new List(); + var shapedRuns = objectPool.TextRunLists.Rent(); if (textRuns.Count == 0) { @@ -172,13 +194,14 @@ namespace Avalonia.Media.TextFormatting return shapedRuns; } - - var bidiData = t_bidiData ??= new BidiData(); + var bidiData = t_bidiData ??= new(); bidiData.Reset(); bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; - foreach (var textRun in textRuns) + for (var i = 0; i < textRuns.Count; ++i) { + var textRun = textRuns[i]; + ReadOnlySpan text; if (!textRun.Text.IsEmpty) text = textRun.Text.Span; @@ -190,8 +213,7 @@ namespace Avalonia.Media.TextFormatting bidiData.Append(text); } - var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm(); - + var bidiAlgorithm = t_bidiAlgorithm ??= new(); bidiAlgorithm.Process(bidiData); var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes); @@ -199,9 +221,11 @@ namespace Avalonia.Media.TextFormatting resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; - var processedRuns = new List(textRuns.Count); + var processedRuns = objectPool.TextRunLists.Rent(); + + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); - CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); for (var index = 0; index < processedRuns.Count; index++) { @@ -210,8 +234,9 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case UnshapedTextRun shapeableRun: - { - var groupedRuns = new List(2) { shapeableRun }; + { + groupedRuns.Clear(); + groupedRuns.Add(shapeableRun); var text = shapeableRun.Text; while (index + 1 < processedRuns.Count) @@ -253,6 +278,9 @@ namespace Avalonia.Media.TextFormatting } } + objectPool.TextRunLists.Return(ref processedRuns); + objectPool.UnshapedTextRunLists.Return(ref groupedRuns); + return shapedRuns; } @@ -319,14 +347,13 @@ namespace Avalonia.Media.TextFormatting } } - private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) && x.Typeface == y.Typeface && x.BaselineAlignment == y.BaselineAlignment; private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, - TextShaperOptions options, List results) + TextShaperOptions options, RentedList results) { var shapedBuffer = TextShaper.Current.ShapeText(text, options); @@ -349,8 +376,8 @@ namespace Avalonia.Media.TextFormatting /// The bidi levels. /// A list that will be filled with the processed runs. /// - private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, - List processedRuns) + private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, + RentedList processedRuns) { if (levels.Length == 0) { @@ -437,19 +464,20 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. + /// A pool used to get reusable formatting objects. /// On return, the end of line, if any. /// On return, the processed text source length. /// /// The formatted text runs. /// - private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, - out TextEndOfLine? endOfLine, out int textSourceLength) + private static RentedList FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, + FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength) { textSourceLength = 0; endOfLine = null; - var textRuns = new List(); + var textRuns = objectPool.TextRunLists.Rent(); var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); @@ -543,8 +571,10 @@ namespace Avalonia.Media.TextFormatting measuredLength = 0; var currentWidth = 0.0; - foreach (var currentRun in textRuns) + for (var i = 0; i < textRuns.Count; ++i) { + var currentRun = textRuns[i]; + switch (currentRun) { case ShapedTextRun shapedTextCharacters: @@ -554,15 +584,15 @@ namespace Avalonia.Media.TextFormatting var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; - for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) + for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) { - var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; + var glyphInfo = shapedTextCharacters.ShapedBuffer[j]; if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { measuredLength += Math.Max(0, lastCluster - firstCluster); - goto found; + return measuredLength != 0; } lastCluster = glyphInfo.GlyphCluster; @@ -579,7 +609,7 @@ namespace Avalonia.Media.TextFormatting { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { - goto found; + return measuredLength != 0; } measuredLength += currentRun.Length; @@ -596,8 +626,6 @@ namespace Avalonia.Media.TextFormatting } } - found: - return measuredLength != 0; } @@ -605,7 +633,8 @@ namespace Avalonia.Media.TextFormatting /// Creates an empty text line. /// /// The empty text line. - public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) + public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; @@ -618,7 +647,9 @@ namespace Avalonia.Media.TextFormatting var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); + var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); + line.FinalizeLine(); + return line; } /// @@ -630,14 +661,15 @@ namespace Avalonia.Media.TextFormatting /// The text paragraph properties. /// /// The current line break if the line was explicitly broken. + /// A pool used to get reusable formatting objects. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak) + TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -763,10 +795,10 @@ namespace Avalonia.Media.TextFormatting break; } - var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength); + var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); var lineBreak = postSplitRuns?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) : + new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) @@ -778,7 +810,12 @@ namespace Avalonia.Media.TextFormatting paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); - return textLine.FinalizeLine(); + textLine.FinalizeLine(); + + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + + return textLine; } private struct TextRunEnumerator diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 55b6f14267..7a74dc89ae 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -426,16 +426,19 @@ namespace Avalonia.Media.TextFormatting private TextLine[] CreateTextLines() { + var objectPool = FormattingObjectPool.Instance; + if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, + FormattingObjectPool.Instance); Bounds = new Rect(0, 0, 0, textLine.Height); return new TextLine[] { textLine }; } - var textLines = new List(); + var textLines = objectPool.TextLines.Rent(); double left = double.PositiveInfinity, width = 0.0, height = 0.0; @@ -447,14 +450,15 @@ namespace Avalonia.Media.TextFormatting while (true) { - var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, - _paragraphProperties, previousLine?.TextLineBreak); + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, + previousLine?.TextLineBreak); if (textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { - var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties); + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, + _paragraphProperties, objectPool); textLines.Add(emptyTextLine); @@ -496,7 +500,7 @@ namespace Avalonia.Media.TextFormatting //Fulfill max lines constraint if (MaxLines > 0 && textLines.Count >= MaxLines) { - if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null) + if(textLine.TextLineBreak?.RemainingRuns is not null) { textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); } @@ -513,7 +517,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool); textLines.Add(textLine); @@ -552,7 +556,12 @@ namespace Avalonia.Media.TextFormatting } } - return textLines.ToArray(); + var result = textLines.ToArray(); + + objectPool.TextLines.Return(ref textLines); + objectPool.VerifyAllReturned(); + + return result; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 672a15b398..0d777ad043 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,7 +1,7 @@ // ReSharper disable ForCanBeConvertedToForeach using System; using System.Collections.Generic; -using System.Linq; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -80,60 +80,64 @@ namespace Avalonia.Media.TextFormatting if (measuredLength > 0) { - var collapsedRuns = new List(textRuns.Count + 1); + var objectPool = FormattingObjectPool.Instance; - // perf note: the runs are very likely to come from TextLineImpl, - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + var collapsedRuns = objectPool.TextRunLists.Rent(); - IReadOnlyList? preSplitRuns; - IReadOnlyList? postSplitRuns; + RentedList? rentedPreSplitRuns = null; + RentedList? rentedPostSplitRuns = null; + TextRun[]? results; - if (_prefixLength > 0) + try { - (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns( - textRunArray, Math.Min(_prefixLength, measuredLength)); + IReadOnlyList? effectivePostSplitRuns; - for (var i = 0; i < preSplitRuns.Count; i++) + if (_prefixLength > 0) { - var preSplitRun = preSplitRuns[i]; - collapsedRuns.Add(preSplitRun); + (rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns( + textRuns, Math.Min(_prefixLength, measuredLength), objectPool); + + effectivePostSplitRuns = rentedPostSplitRuns; + + foreach (var preSplitRun in rentedPreSplitRuns) + { + collapsedRuns.Add(preSplitRun); + } + } + else + { + effectivePostSplitRuns = textRuns; } - } - else - { - preSplitRuns = null; - postSplitRuns = textRunArray; - } - collapsedRuns.Add(shapedSymbol); + collapsedRuns.Add(shapedSymbol); - if (measuredLength <= _prefixLength || postSplitRuns is null) - { - return collapsedRuns.ToArray(); - } + if (measuredLength <= _prefixLength || effectivePostSplitRuns is null) + { + results = collapsedRuns.ToArray(); + objectPool.TextRunLists.Return(ref collapsedRuns); + return results; + } - var availableSuffixWidth = availableWidth; + var availableSuffixWidth = availableWidth; - if (preSplitRuns is not null) - { - for (var i = 0; i < preSplitRuns.Count; i++) + if (rentedPreSplitRuns is not null) { - var run = preSplitRuns[i]; - if (run is DrawableTextRun drawableTextRun) + foreach (var run in rentedPreSplitRuns) { - availableSuffixWidth -= drawableTextRun.Size.Width; + if (run is DrawableTextRun drawableTextRun) + { + availableSuffixWidth -= drawableTextRun.Size.Width; + } } } - } - - for (var i = postSplitRuns.Count - 1; i >= 0; i--) - { - var run = postSplitRuns[i]; - switch (run) + for (var i = effectivePostSplitRuns.Count - 1; i >= 0; i--) { - case ShapedTextRun endShapedRun: + var run = effectivePostSplitRuns[i]; + + switch (run) + { + case ShapedTextRun endShapedRun: { if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, out var suffixCount, out var suffixWidth)) @@ -151,10 +155,18 @@ namespace Avalonia.Media.TextFormatting break; } + } } } + finally + { + objectPool.TextRunLists.Return(ref rentedPreSplitRuns); + objectPool.TextRunLists.Return(ref rentedPostSplitRuns); + } - return collapsedRuns.ToArray(); + results = collapsedRuns.ToArray(); + objectPool.TextRunLists.Return(ref collapsedRuns); + return results; } return new TextRun[] { shapedSymbol }; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7fa9155b02..245104f8fe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; -using System.Threading; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal sealed class TextLineImpl : TextLine { - private static readonly ThreadLocal s_bidiReorderer = new(() => new BidiReorderer()); - private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; @@ -570,7 +567,7 @@ namespace Avalonia.Media.TextFormatting { var characterIndex = firstTextSourceIndex + textLength; - var result = new List(TextRuns.Count); + var result = new List(_textRuns.Length); var lastDirection = FlowDirection.LeftToRight; var currentDirection = lastDirection; @@ -583,9 +580,9 @@ namespace Avalonia.Media.TextFormatting TextRunBounds lastRunBounds = default; - for (var index = 0; index < TextRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { - if (TextRuns[index] is not DrawableTextRun currentRun) + if (_textRuns[index] is not DrawableTextRun currentRun) { continue; } @@ -671,12 +668,12 @@ namespace Avalonia.Media.TextFormatting for (int i = rightToLeftIndex - 1; i >= index; i--) { - if (TextRuns[i] is not ShapedTextRun) + if (_textRuns[i] is not ShapedTextRun shapedRun) { continue; } - currentShapedRun = (ShapedTextRun)TextRuns[i]; + currentShapedRun = shapedRun; currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); @@ -786,7 +783,7 @@ namespace Avalonia.Media.TextFormatting { var characterIndex = firstTextSourceIndex + textLength; - var result = new List(TextRuns.Count); + var result = new List(_textRuns.Length); var lastDirection = FlowDirection.LeftToRight; var currentDirection = lastDirection; @@ -797,9 +794,9 @@ namespace Avalonia.Media.TextFormatting double currentWidth = 0; var currentRect = default(Rect); - for (var index = TextRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { - if (TextRuns[index] is not DrawableTextRun currentRun) + if (_textRuns[index] is not DrawableTextRun currentRun) { continue; } @@ -992,14 +989,11 @@ namespace Avalonia.Media.TextFormatting } } - public TextLineImpl FinalizeLine() + public void FinalizeLine() { _textLineMetrics = CreateLineMetrics(); - var bidiReorderer = s_bidiReorderer.Value!; - bidiReorderer.BidiReorder(_textRuns, _resolvedFlowDirection); - - return this; + BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); } /// diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 3a22fc7b9c..bbbcc39ecc 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -12,7 +12,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the array. internal struct ArrayBuilder - where T : struct { private const int DefaultCapacity = 4; private const int MaxCoreClrArrayLength = 0x7FeFFFFF; @@ -48,6 +47,12 @@ namespace Avalonia.Utilities } } + /// + /// Gets the current capacity of the array. + /// + public int Capacity + => _data?.Length ?? 0; + /// /// Returns a reference to specified element of the array. /// @@ -131,8 +136,28 @@ namespace Avalonia.Utilities /// public void Clear() { - // No need to actually clear since we're not allowing reference types. +#if NET6_0_OR_GREATER + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ClearArray(); + } + else + { + _size = 0; + } +#else + ClearArray(); +#endif + } + + private void ClearArray() + { + var size = _size; _size = 0; + if (size > 0) + { + Array.Clear(_data!, 0, size); + } } private void EnsureCapacity(int min) @@ -190,5 +215,12 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); + + /// + /// Returns the current state of the array as a span. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AsSpan() => _data.AsSpan(0, _size); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 3cffef72c5..17e75f2f95 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -17,7 +17,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the slice. internal readonly struct ArraySlice : IReadOnlyList - where T : struct { /// /// Gets an empty diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 3218139251..b5f4777192 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -24,7 +24,10 @@ namespace Avalonia.UnitTests var glyphIndex = typeface.GetGlyph(codepoint); - shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10); + for (var j = 0; j < count; ++j) + { + shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); + } i += count; } From fd0720fc5661932a9f4e9246ebcbd56fa0a513bf Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 00:11:28 +0100 Subject: [PATCH 048/326] Updated text layout benchmark with trimming/wrapping --- .../Avalonia.Benchmarks/Text/HugeTextLayout.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index bf0253e9ab..8b23855fde 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -9,6 +9,8 @@ using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Text; [MemoryDiagnoser] +[MinIterationTime(150)] +[MaxWarmupCount(15)] public class HugeTextLayout : IDisposable { private readonly IDisposable _app; @@ -44,7 +46,13 @@ Admitting that the possibility of achieving the results of the constructive crit Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; - + + [Params(false, true)] + public bool UseWrapping { get; set; } + + [Params(false, true)] + public bool UseTrimming { get; set; } + [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -91,9 +99,12 @@ In respect that the structure of the sufficient amount poses problems and challe } } - private static TextLayout MakeLayout(string str) + private TextLayout MakeLayout(string str) { - var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120); + var wrapping = UseWrapping ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = UseTrimming ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, + textTrimming: trimming, textWrapping: wrapping); layout.Dispose(); return layout; } From 7fcfc82be0d6375e61b7b92c7307afa04633ecc3 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 00:39:50 +0100 Subject: [PATCH 049/326] Fixed TextShaperImpl when the text is backed by an array --- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 2 ++ src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs | 2 ++ tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e0f95bac60..def2482af3 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Skia if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index fffa5ce490..ac441108e3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Direct2D1.Media if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 566cb0f1ac..baf5ffb07c 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.UnitTests if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } From d714f37fce8b01b38f2ad61fc2bd2d7fb981b88a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 19 Jan 2023 11:12:09 +0600 Subject: [PATCH 050/326] Implemented interop with externally managed GPU memory --- Avalonia.Desktop.slnf | 1 + Avalonia.sln | 7 + build/Base.props | 1 + build/SharpDX.props | 15 +- .../ControlCatalog/Pages/OpenGlPage.xaml.cs | 21 +- samples/GpuInterop/App.axaml | 8 + samples/GpuInterop/App.axaml.cs | 22 + .../GpuInterop/D3DDemo/D3D11DemoControl.cs | 147 ++++ samples/GpuInterop/D3DDemo/D3D11Swapchain.cs | 119 +++ samples/GpuInterop/D3DDemo/D3DContent.cs | 110 +++ samples/GpuInterop/D3DDemo/MiniCube.fx | 47 + samples/GpuInterop/DrawingSurfaceDemoBase.cs | 141 +++ samples/GpuInterop/GpuDemo.axaml | 32 + samples/GpuInterop/GpuDemo.axaml.cs | 116 +++ samples/GpuInterop/GpuInterop.csproj | 50 ++ samples/GpuInterop/MainWindow.axaml | 13 + samples/GpuInterop/MainWindow.axaml.cs | 21 + samples/GpuInterop/Program.cs | 15 + .../VulkanDemo/Assets/Shaders/Makefile | 12 + .../VulkanDemo/Assets/Shaders/frag.glsl | 42 + .../VulkanDemo/Assets/Shaders/frag.spirv | Bin 0 -> 4276 bytes .../VulkanDemo/Assets/Shaders/vert.glsl | 36 + .../VulkanDemo/Assets/Shaders/vert.spirv | Bin 0 -> 3276 bytes samples/GpuInterop/VulkanDemo/ByteString.cs | 47 + .../GpuInterop/VulkanDemo/D3DMemoryHelper.cs | 54 ++ .../VulkanDemo/VulkanBufferHelper.cs | 80 ++ .../VulkanDemo/VulkanCommandBufferPool.cs | 224 +++++ .../GpuInterop/VulkanDemo/VulkanContent.cs | 829 ++++++++++++++++++ .../GpuInterop/VulkanDemo/VulkanContext.cs | 335 +++++++ .../VulkanDemo/VulkanDemoControl.cs | 101 +++ .../GpuInterop/VulkanDemo/VulkanExtensions.cs | 12 + samples/GpuInterop/VulkanDemo/VulkanImage.cs | 276 ++++++ .../VulkanDemo/VulkanMemoryHelper.cs | 59 ++ .../VulkanDemo/VulkanSemaphorePair.cs | 58 ++ .../GpuInterop/VulkanDemo/VulkanSwapchain.cs | 154 ++++ ...nalObjectsRenderInterfaceContextFeature.cs | 53 ++ .../Platform/IOptionalFeatureProvider.cs | 11 +- .../Platform/IPlatformRenderInterface.cs | 5 + .../PlatformGraphicsExternalMemory.cs | 64 ++ .../Composition/CompositingRenderer.cs | 6 +- .../Composition/CompositionDrawingSurface.cs | 60 ++ .../Composition/CompositionExternalMemory.cs | 142 +++ .../Composition/CompositionInterop.cs | 150 ++++ .../Composition/CompositionSurface.cs | 10 + .../Composition/Compositor.Factories.cs | 6 +- .../Rendering/Composition/Compositor.cs | 88 +- .../Server/ServerCompositionDrawingSurface.cs | 74 ++ .../Server/ServerCompositionSurface.cs | 9 +- .../Server/ServerCompositionSurfaceVisual.cs | 48 + .../Composition/Server/ServerCompositor.cs | 19 +- .../PlatformRenderInterfaceContextManager.cs | 2 + src/Avalonia.Base/Rendering/SwapchainBase.cs | 89 ++ src/Avalonia.Base/composition-schema.xml | 7 +- .../HeadlessPlatformRenderInterface.cs | 1 + src/Avalonia.OpenGL/Avalonia.OpenGL.csproj | 4 +- .../Controls/CompositionOpenGlSwapchain.cs | 163 ++++ .../Controls/OpenGlControlBase.cs | 302 +++---- .../Controls/OpenGlControlResources.cs | 170 ++++ src/Avalonia.OpenGL/Egl/EglConsts.cs | 12 +- src/Avalonia.OpenGL/Egl/EglContext.cs | 16 +- src/Avalonia.OpenGL/Egl/EglDisplay.cs | 2 +- src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs | 2 +- src/Avalonia.OpenGL/Egl/EglInterface.cs | 3 + .../ExternalObjectsOpenGlExtensionFeature.cs | 282 ++++++ src/Avalonia.OpenGL/GlConsts.cs | 14 +- .../IGlContextExternalObjectsFeature.cs | 50 ++ ...ureSharingRenderInterfaceContextFeature.cs | 13 +- .../IPlatformGraphicsOpenGlContextFactory.cs | 8 + .../Imaging/IOpenGlBitmapImpl.cs | 19 - src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs | 40 - src/Avalonia.OpenGL/OpenGlException.cs | 3 + src/Avalonia.X11/Glx/GlxContext.cs | 13 +- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 7 - .../OpenGl/GlSkiaExternalObjectsFeature.cs | 191 ++++ .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 42 +- .../GlSkiaSharedTextureForComposition.cs | 44 + .../Gpu/OpenGl/OpenGlBitmapImpl.cs | 210 ----- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 7 + .../Avalonia.Skia/PlatformRenderInterface.cs | 5 - src/Skia/Avalonia.Skia/SkiaBackendContext.cs | 4 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 1 + .../Avalonia.Win32/DirectX/DirectXEnums.cs | 38 + .../Avalonia.Win32/DirectX/DirectXStructs.cs | 4 +- .../Avalonia.Win32/DirectX/directx.idl | 33 +- .../Angle/AngleExternalD3D11Texture2D.cs | 107 +++ .../Angle/AngleExternalObjectsFeature.cs | 103 +++ .../OpenGl/Angle/AngleWin32EglDisplay.cs | 132 ++- .../Angle/AngleWin32PlatformGraphics.cs | 18 +- src/Windows/Avalonia.Win32/Win32GlManager.cs | 3 + .../VisualTree/MockRenderInterface.cs | 2 + .../NullRenderingPlatform.cs | 2 + .../MockPlatformRenderInterface.cs | 2 + 92 files changed, 5541 insertions(+), 579 deletions(-) create mode 100644 samples/GpuInterop/App.axaml create mode 100644 samples/GpuInterop/App.axaml.cs create mode 100644 samples/GpuInterop/D3DDemo/D3D11DemoControl.cs create mode 100644 samples/GpuInterop/D3DDemo/D3D11Swapchain.cs create mode 100644 samples/GpuInterop/D3DDemo/D3DContent.cs create mode 100644 samples/GpuInterop/D3DDemo/MiniCube.fx create mode 100644 samples/GpuInterop/DrawingSurfaceDemoBase.cs create mode 100644 samples/GpuInterop/GpuDemo.axaml create mode 100644 samples/GpuInterop/GpuDemo.axaml.cs create mode 100644 samples/GpuInterop/GpuInterop.csproj create mode 100644 samples/GpuInterop/MainWindow.axaml create mode 100644 samples/GpuInterop/MainWindow.axaml.cs create mode 100644 samples/GpuInterop/Program.cs create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv create mode 100644 samples/GpuInterop/VulkanDemo/ByteString.cs create mode 100644 samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanContent.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanContext.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanExtensions.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanImage.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs create mode 100644 src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs create mode 100644 src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs create mode 100644 src/Avalonia.Base/Rendering/SwapchainBase.cs create mode 100644 src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs create mode 100644 src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs create mode 100644 src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs create mode 100644 src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs create mode 100644 src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs delete mode 100644 src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs delete mode 100644 src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs create mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs create mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs delete mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3fa8e969c8..2f034bd083 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -5,6 +5,7 @@ "packages\\Avalonia\\Avalonia.csproj", "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj", "samples\\ControlCatalog\\ControlCatalog.csproj", + "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\SampleControls\\ControlSamples.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index fc42a5d63b..ce9a37a3ce 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,6 +231,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -542,6 +544,10 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -606,6 +612,7 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/Base.props b/build/Base.props index 9ec1c3c2d3..433ce8e950 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,7 @@  + diff --git a/build/SharpDX.props b/build/SharpDX.props index 69aa817a01..ff521977fd 100644 --- a/build/SharpDX.props +++ b/build/SharpDX.props @@ -1,9 +1,14 @@  + + 4.0.1 + - - - - - + + + + + + + diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index ded02330d5..c77d65ddf1 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -78,12 +78,7 @@ namespace ControlCatalog.Pages get => _info; private set => SetAndRaise(InfoProperty, ref _info, value); } - - static OpenGlPageControl() - { - AffectsRender(YawProperty, PitchProperty, RollProperty, DiscoProperty); - } - + private int _vertexShader; private int _fragmentShader; private int _shaderProgram; @@ -254,7 +249,7 @@ namespace ControlCatalog.Pages Console.WriteLine(err); } - protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) + protected override unsafe void OnOpenGlInit(GlInterface GL) { CheckError(GL); @@ -309,7 +304,7 @@ namespace ControlCatalog.Pages } - protected override void OnOpenGlDeinit(GlInterface GL, int fb) + protected override void OnOpenGlDeinit(GlInterface GL) { // Unbind everything GL.BindBuffer(GL_ARRAY_BUFFER, 0); @@ -366,7 +361,15 @@ namespace ControlCatalog.Pages CheckError(GL); if (_disco > 0.01) - Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + RequestNextFrameRendering(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == YawProperty || change.Property == RollProperty || change.Property == PitchProperty || + change.Property == DiscoProperty) + RequestNextFrameRendering(); + base.OnPropertyChanged(change); } } } diff --git a/samples/GpuInterop/App.axaml b/samples/GpuInterop/App.axaml new file mode 100644 index 0000000000..6b18dbe520 --- /dev/null +++ b/samples/GpuInterop/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/GpuInterop/App.axaml.cs b/samples/GpuInterop/App.axaml.cs new file mode 100644 index 0000000000..85973d59bb --- /dev/null +++ b/samples/GpuInterop/App.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace GpuInterop +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow(); + } + } + } +} diff --git a/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs b/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs new file mode 100644 index 0000000000..c7ee2bb9a4 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs @@ -0,0 +1,147 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using SharpDX; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; +using Vector3 = SharpDX.Vector3; + +namespace GpuInterop.D3DDemo; + +public class D3D11DemoControl : DrawingSurfaceDemoBase +{ + private D3DDevice _device; + private D3D11Swapchain _swapchain; + private SharpDX.Direct3D11.DeviceContext _context; + private Matrix _view; + private PixelSize _lastSize; + private Texture2D _depthBuffer; + private DepthStencilView _depthView; + private Matrix _proj; + private Buffer _constantBuffer; + private Stopwatch _st = Stopwatch.StartNew(); + + protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface surface, ICompositionGpuInterop interop) + { + if (interop?.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) != true) + return (false, "DXGI shared handle import is not supported by the current graphics backend"); + + var factory = new DxgiFactory1(); + using var adapter = factory.GetAdapter1(0); + _device = new D3DDevice(adapter, DeviceCreationFlags.None, new[] + { + FeatureLevel.Level_12_1, + FeatureLevel.Level_12_0, + FeatureLevel.Level_11_1, + FeatureLevel.Level_11_0, + FeatureLevel.Level_10_0, + FeatureLevel.Level_9_3, + FeatureLevel.Level_9_2, + FeatureLevel.Level_9_1, + }); + _swapchain = new D3D11Swapchain(_device, interop, surface); + _context = _device.ImmediateContext; + _constantBuffer = D3DContent.CreateMesh(_device); + _view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY); + return (true, $"D3D11 ({_device.FeatureLevel}) {adapter.Description1.Description}"); + } + + protected override void FreeGraphicsResources() + { + _swapchain.DisposeAsync(); + _swapchain = null!; + Utilities.Dispose(ref _depthView); + Utilities.Dispose(ref _depthBuffer); + Utilities.Dispose(ref _constantBuffer); + Utilities.Dispose(ref _context); + Utilities.Dispose(ref _device); + } + + protected override bool SupportsDisco => true; + + protected override void RenderFrame(PixelSize pixelSize) + { + if (pixelSize == default) + return; + if (pixelSize != _lastSize) + Resize(pixelSize); + using (_swapchain.BeginDraw(pixelSize, out var renderView)) + { + + _device.ImmediateContext.OutputMerger.SetTargets(_depthView, renderView); + var viewProj = Matrix.Multiply(_view, _proj); + var context = _device.ImmediateContext; + + var now = _st.Elapsed.TotalSeconds * 5; + var scaleX = (float)(1f + Disco * (Math.Sin(now) + 1) / 6); + var scaleY = (float)(1f + Disco * (Math.Cos(now) + 1) / 8); + var colorOff =(float) (Math.Sin(now) + 1) / 2 * Disco; + + + // Clear views + context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0); + context.ClearRenderTargetView(renderView, + new RawColor4(1 - colorOff, colorOff, (float)0.5 + colorOff / 2, 1)); + + + var ypr = Matrix4x4.CreateFromYawPitchRoll(Yaw, Pitch, Roll); + // Update WorldViewProj Matrix + var worldViewProj = Matrix.RotationX((float)Yaw) * Matrix.RotationY((float)Pitch) + * Matrix.RotationZ((float)Roll) + * Matrix.Scaling(new Vector3(scaleX, scaleY, 1)) + * viewProj; + worldViewProj.Transpose(); + context.UpdateSubresource(ref worldViewProj, _constantBuffer); + + // Draw the cube + context.Draw(36, 0); + + + _context.Flush(); + } + } + + private void Resize(PixelSize size) + { + Utilities.Dispose(ref _depthBuffer); + _depthBuffer = new Texture2D(_device, + new Texture2DDescription() + { + Format = Format.D32_Float_S8X24_UInt, + ArraySize = 1, + MipLevels = 1, + Width = (int)size.Width, + Height = (int)size.Height, + SampleDescription = new SampleDescription(1, 0), + Usage = ResourceUsage.Default, + BindFlags = BindFlags.DepthStencil, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }); + + Utilities.Dispose(ref _depthView); + _depthView = new DepthStencilView(_device, _depthBuffer); + + // Setup targets and viewport for rendering + _device.ImmediateContext.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f)); + + // Setup new projection matrix with correct aspect ratio + _proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f); + } +} diff --git a/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs new file mode 100644 index 0000000000..30a4c19d35 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; + +namespace GpuInterop.D3DDemo; + +class D3D11Swapchain : SwapchainBase +{ + private readonly D3DDevice _device; + + public D3D11Swapchain(D3DDevice device, ICompositionGpuInterop interop, CompositionDrawingSurface target) + : base(interop, target) + { + _device = device; + } + + protected override D3D11SwapchainImage CreateImage(PixelSize size) => new(_device, size, Interop, Target); + + public IDisposable BeginDraw(PixelSize size, out RenderTargetView view) + { + var rv = BeginDrawCore(size, out var image); + view = image.RenderTargetView; + return rv; + } +} + +public class D3D11SwapchainImage : ISwapchainImage +{ + public PixelSize Size { get; } + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly Texture2D _texture; + private readonly KeyedMutex _mutex; + private readonly IntPtr _handle; + private PlatformGraphicsExternalImageProperties _properties; + private ICompositionImportedGpuImage? _imported; + public Task? LastPresent { get; private set; } + public RenderTargetView RenderTargetView { get; } + + public D3D11SwapchainImage(D3DDevice device, PixelSize size, + ICompositionGpuInterop interop, + CompositionDrawingSurface target) + { + Size = size; + _interop = interop; + _target = target; + _texture = new Texture2D(device, + new Texture2DDescription + { + Format = Format.R8G8B8A8_UNorm, + Width = size.Width, + Height = size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDescription = new SampleDescription { Count = 1, Quality = 0 }, + CpuAccessFlags = default, + OptionFlags = ResourceOptionFlags.SharedKeyedmutex, + BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource + }); + _mutex = _texture.QueryInterface(); + using (var res = _texture.QueryInterface()) + _handle = res.SharedHandle; + _properties = new PlatformGraphicsExternalImageProperties + { + Width = size.Width, Height = size.Height, Format = PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm + }; + + RenderTargetView = new RenderTargetView(device, _texture); + } + + public void BeginDraw() + { + _mutex.Acquire(0, int.MaxValue); + } + + public void Present() + { + _mutex.Release(1); + _imported ??= _interop.ImportImage( + new PlatformHandle(_handle, KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle), + _properties); + LastPresent = _target.UpdateWithKeyedMutexAsync(_imported, 1, 0); + } + + + public async ValueTask DisposeAsync() + { + if (LastPresent != null) + try + { + await LastPresent; + } + catch + { + // Ignore + } + + RenderTargetView.Dispose(); + _mutex.Dispose(); + _texture.Dispose(); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/D3DDemo/D3DContent.cs b/samples/GpuInterop/D3DDemo/D3DContent.cs new file mode 100644 index 0000000000..f670a9a8c9 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3DContent.cs @@ -0,0 +1,110 @@ +using SharpDX; +using SharpDX.D3DCompiler; +using SharpDX.Direct3D; + +using System; +using System.Linq; +using System.Threading.Tasks; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; +using InputElement = SharpDX.Direct3D11.InputElement; + + +namespace GpuInterop.D3DDemo; + +public class D3DContent +{ + + public static Buffer CreateMesh(D3DDevice device) + { + // Compile Vertex and Pixel shaders + var vertexShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "VS", "vs_4_0"); + var vertexShader = new VertexShader(device, vertexShaderByteCode); + + var pixelShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "PS", "ps_4_0"); + var pixelShader = new PixelShader(device, pixelShaderByteCode); + + var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode); + + var inputElements = new[] + { + new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0), + new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0) + }; + + // Layout from VertexShader input signature + var layout = new InputLayout( + device, + signature, + inputElements); + + // Instantiate Vertex buffer from vertex data + using var vertices = Buffer.Create( + device, + BindFlags.VertexBuffer, + new[] + { + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + }); + + // Create Constant Buffer + var constantBuffer = new Buffer(device, Utilities.SizeOf(), ResourceUsage.Default, + BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); + + var context = device.ImmediateContext; + + // Prepare All the stages + context.InputAssembler.InputLayout = layout; + context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; + context.InputAssembler.SetVertexBuffers(0, + new VertexBufferBinding(vertices, Utilities.SizeOf() * 2, 0)); + context.VertexShader.SetConstantBuffer(0, constantBuffer); + context.VertexShader.Set(vertexShader); + context.PixelShader.Set(pixelShader); + return constantBuffer; + } +} diff --git a/samples/GpuInterop/D3DDemo/MiniCube.fx b/samples/GpuInterop/D3DDemo/MiniCube.fx new file mode 100644 index 0000000000..c6064ea56c --- /dev/null +++ b/samples/GpuInterop/D3DDemo/MiniCube.fx @@ -0,0 +1,47 @@ +// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +struct VS_IN +{ + float4 pos : POSITION; + float4 col : COLOR; +}; + +struct PS_IN +{ + float4 pos : SV_POSITION; + float4 col : COLOR; +}; + +float4x4 worldViewProj; + +PS_IN VS( VS_IN input ) +{ + PS_IN output = (PS_IN)0; + + output.pos = mul(input.pos, worldViewProj); + output.col = input.col; + + return output; +} + +float4 PS( PS_IN input ) : SV_Target +{ + return input.col; +} diff --git a/samples/GpuInterop/DrawingSurfaceDemoBase.cs b/samples/GpuInterop/DrawingSurfaceDemoBase.cs new file mode 100644 index 0000000000..aad813ea82 --- /dev/null +++ b/samples/GpuInterop/DrawingSurfaceDemoBase.cs @@ -0,0 +1,141 @@ +using System; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Rendering.Composition; +using Avalonia.VisualTree; + +namespace GpuInterop; + +public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo +{ + private CompositionSurfaceVisual? _visual; + private Compositor? _compositor; + private Action _update; + private string _info; + private bool _updateQueued; + private bool _initialized; + + protected CompositionDrawingSurface Surface { get; private set; } + + public DrawingSurfaceDemoBase() + { + _update = UpdateFrame; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + Initialize(); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_initialized) + FreeGraphicsResources(); + _initialized = false; + base.OnDetachedFromLogicalTree(e); + } + + async void Initialize() + { + try + { + var selfVisual = ElementComposition.GetElementVisual(this)!; + _compositor = selfVisual.Compositor; + + Surface = _compositor.CreateDrawingSurface(); + _visual = _compositor.CreateSurfaceVisual(); + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + _visual.Surface = Surface; + ElementComposition.SetElementChildVisual(this, _visual); + var (res, info) = await DoInitialize(_compositor, Surface); + _info = info; + if (ParentControl != null) + ParentControl.Info = info; + _initialized = res; + QueueNextFrame(); + } + catch (Exception e) + { + if (ParentControl != null) + ParentControl.Info = e.ToString(); + } + } + + void UpdateFrame() + { + _updateQueued = false; + var root = this.GetVisualRoot(); + if (root == null) + return; + + _visual!.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + var size = PixelSize.FromSize(Bounds.Size, root.RenderScaling); + RenderFrame(size); + if (SupportsDisco && Disco > 0) + QueueNextFrame(); + } + + void QueueNextFrame() + { + if (_initialized && !_updateQueued && _compositor != null) + { + _updateQueued = true; + _compositor?.RequestCompositionUpdate(_update); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if(change.Property == BoundsProperty) + QueueNextFrame(); + base.OnPropertyChanged(change); + } + + async Task<(bool success, string info)> DoInitialize(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface) + { + var interop = await compositor.TryGetCompositionGpuInterop(); + if (interop == null) + return (false, "Compositor doesn't support interop for the current backend"); + return InitializeGraphicsResources(compositor, compositionDrawingSurface, interop); + } + + protected abstract (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop); + + protected abstract void FreeGraphicsResources(); + + + protected abstract void RenderFrame(PixelSize pixelSize); + protected virtual bool SupportsDisco => false; + + public void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco) + { + ParentControl = parent; + if (ParentControl != null) + { + ParentControl.Info = _info; + ParentControl.DiscoVisible = true; + } + + Yaw = yaw; + Pitch = pitch; + Roll = roll; + Disco = disco; + QueueNextFrame(); + } + + public GpuDemo? ParentControl { get; private set; } + + public float Disco { get; private set; } + + public float Roll { get; private set; } + + public float Pitch { get; private set; } + + public float Yaw { get; private set; } +} diff --git a/samples/GpuInterop/GpuDemo.axaml b/samples/GpuInterop/GpuDemo.axaml new file mode 100644 index 0000000000..6ca51abadb --- /dev/null +++ b/samples/GpuInterop/GpuDemo.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + Yaw + + Pitch + + Roll + + + + D + I + S + C + O + + + + + + + diff --git a/samples/GpuInterop/GpuDemo.axaml.cs b/samples/GpuInterop/GpuDemo.axaml.cs new file mode 100644 index 0000000000..f7acac09e1 --- /dev/null +++ b/samples/GpuInterop/GpuDemo.axaml.cs @@ -0,0 +1,116 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace GpuInterop; + +public class GpuDemo : UserControl +{ + public GpuDemo() + { + AvaloniaXamlLoader.Load(this); + } + + private float _yaw = 5; + + public static readonly DirectProperty YawProperty = + AvaloniaProperty.RegisterDirect("Yaw", o => o.Yaw, (o, v) => o.Yaw = v); + + public float Yaw + { + get => _yaw; + set => SetAndRaise(YawProperty, ref _yaw, value); + } + + private float _pitch = 5; + + public static readonly DirectProperty PitchProperty = + AvaloniaProperty.RegisterDirect("Pitch", o => o.Pitch, (o, v) => o.Pitch = v); + + public float Pitch + { + get => _pitch; + set => SetAndRaise(PitchProperty, ref _pitch, value); + } + + + private float _roll = 5; + + public static readonly DirectProperty RollProperty = + AvaloniaProperty.RegisterDirect("Roll", o => o.Roll, (o, v) => o.Roll = v); + + public float Roll + { + get => _roll; + set => SetAndRaise(RollProperty, ref _roll, value); + } + + + private float _disco; + + public static readonly DirectProperty DiscoProperty = + AvaloniaProperty.RegisterDirect("Disco", o => o.Disco, (o, v) => o.Disco = v); + + public float Disco + { + get => _disco; + set => SetAndRaise(DiscoProperty, ref _disco, value); + } + + private string _info = string.Empty; + + public static readonly DirectProperty InfoProperty = + AvaloniaProperty.RegisterDirect("Info", o => o.Info, (o, v) => o.Info = v); + + public string Info + { + get => _info; + set => SetAndRaise(InfoProperty, ref _info, value); + } + + private bool _discoVisible; + + public static readonly DirectProperty DiscoVisibleProperty = + AvaloniaProperty.RegisterDirect("DiscoVisible", o => o.DiscoVisible, + (o, v) => o._discoVisible = v); + + public bool DiscoVisible + { + get => _discoVisible; + set => SetAndRaise(DiscoVisibleProperty, ref _discoVisible, value); + } + + private IGpuDemo _demo; + + public static readonly DirectProperty DemoProperty = + AvaloniaProperty.RegisterDirect("Demo", o => o.Demo, + (o, v) => o._demo = v); + + public IGpuDemo Demo + { + get => _demo; + set => SetAndRaise(DemoProperty, ref _demo, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == YawProperty + || change.Property == PitchProperty + || change.Property == RollProperty + || change.Property == DiscoProperty + || change.Property == DemoProperty + ) + { + if (change.Property == DemoProperty) + ((IGpuDemo)change.OldValue)?.Update(null, 0, 0, 0, 0); + _demo?.Update(this, Yaw, Pitch, Roll, Disco); + } + + base.OnPropertyChanged(change); + } +} + +public interface IGpuDemo +{ + void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco); +} diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj new file mode 100644 index 0000000000..c201d9bf85 --- /dev/null +++ b/samples/GpuInterop/GpuInterop.csproj @@ -0,0 +1,50 @@ + + + + Exe + net7.0 + true + enable + false + true + true + true + + + + + + + + + + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/GpuInterop/MainWindow.axaml b/samples/GpuInterop/MainWindow.axaml new file mode 100644 index 0000000000..afb025ec27 --- /dev/null +++ b/samples/GpuInterop/MainWindow.axaml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/samples/GpuInterop/MainWindow.axaml.cs b/samples/GpuInterop/MainWindow.axaml.cs new file mode 100644 index 0000000000..8fc8926783 --- /dev/null +++ b/samples/GpuInterop/MainWindow.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace GpuInterop +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + this.Renderer.DrawFps = true; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/GpuInterop/Program.cs b/samples/GpuInterop/Program.cs new file mode 100644 index 0000000000..86fd239b4c --- /dev/null +++ b/samples/GpuInterop/Program.cs @@ -0,0 +1,15 @@ +using Avalonia; + +namespace GpuInterop +{ + public class Program + { + static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile b/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile new file mode 100644 index 0000000000..900e35a93e --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile @@ -0,0 +1,12 @@ +#!/usr/bin/make -f + +all: vert.spirv frag.spirv +.PHONY: all + +vert.spirv: vert.glsl + glslc -fshader-stage=vert vert.glsl -o vert.spirv + +frag.spirv: frag.glsl + glslc -fshader-stage=frag frag.glsl -o frag.spirv + + diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl new file mode 100644 index 0000000000..31e9f00390 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl @@ -0,0 +1,42 @@ +#version 450 + layout(location = 0) in vec3 FragPos; + layout(location = 1) in vec3 VecPos; + layout(location = 2) in vec3 Normal; + layout(push_constant) uniform constants{ + layout(offset = 0) float maxY; + layout(offset = 4) float minY; + layout(offset = 8) float time; + layout(offset = 12) float disco; + }; + layout(location = 0) out vec4 outFragColor; + + void main() + { + float y = (VecPos.y - minY) / (maxY - minY); + float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + time * 40.0 + y * 50.0); + float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - time * 20.0 - y * 30.0); + + vec3 discoColor = vec3( + 0.5 + abs(0.5 - y) * cos(time * 10.0), + 0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)), + 0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0)))); + + vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25); + objectColor = objectColor * (1.0 - disco) + discoColor * disco; + + float ambientStrength = 0.3; + vec3 lightColor = vec3(1.0, 1.0, 1.0); + vec3 lightPos = vec3(maxY * 2.0, maxY * 2.0, maxY * 2.0); + vec3 ambient = ambientStrength * lightColor; + + + vec3 norm = normalize(Normal); + vec3 lightDir = normalize(lightPos - FragPos); + + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + vec3 result = (ambient + diffuse) * objectColor; + outFragColor = vec4(result, 1.0); + + } \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv new file mode 100644 index 0000000000000000000000000000000000000000..dbac67c368fb0ce01b53b33504c8ae970f6e94f2 GIT binary patch literal 4276 zcmZ9OS9erp6owCwAiaoyqGAFT6hu@&6oG^m&8ud{s6D>zj*15Yxz89&JN?8wKn_R?=IioUzspu@bnv!WKc398J+x*l)v%GV3;Hs znG8*JQ}g=f**&F>*>mU3F=9+IBn>oXY%)BlK`UKt`GSs-$Z6ypaupdzynYq}4WW9H z{7G^Rt2Q;aHmzOT)V!j#eb1iOp3;HNTx(~(kZbM8cjwwm`Mo*ghE>Gn3+foHQD_{5@XaA*q z7oA;?b4NPzJ?%xb>rc?hC$2}gXLvr^HSJ>+I?Y*D>@0Sdxm|F@ZMzt~OxDs#A6#44 zwtTKoT3_nU6?T+%rum2AJM%ksW}zu}G`MtHYt|&EuIHzAKX#$m-GxeR&uR^JTW7h4 zGuUa}<$1a)$9R@2yW4hH)|%(BJM!DNhyPM$_x9vc|7Gm%Tu*Oj$^4<|Jt+2;ERju{ zBu&}bOeNNRUPYv5<2Svm1~~n4o^!-`4sxD#a6>X~6uODMx1MpX<(#e+wlit_>G!UN zJ%K3W>ZxH(@g&etbX_Vn>3#N2%S|SKc!rIy!ybXQZ~29p?Ol|ch;5H`h~Lc29u3xS z{+J9~e=4gui?~vZyAjSc^t+C>_fO7sZbH`~)^#22iJ5KwByjW|{wZ*N(bu%HIbh#~ z?VjkjpL>Od`s{4M49hKO%X{nr+Sy|ru)P*v4O~vi;35u8}EyJ7bZNh;M2UGrM==5P4&~_mdIditiy}ZqE24#F%pnVxD~D z-3m6(cM^HGf#oCSc5roVe_!OI_8nmJ)}4Ie&2P>GYjlIGm)ryKUhBU9stYv-utpz zbHLWq7kLkYoh$Ml2Fv-o5P5UK=IR?o$M!rAv1iX$zxz{k*?Wpr4cr_v)b(+D}sX_F+kG`L$ zkm?!NWPHrnf-SceiF)h6*7F?oN9@yJ*U}&NU_ID=^hM9lfbDq{|9UT;MXcu?+K6mI zjEVTo;Cdw9*mGdHElAA$JlJ*gML#cqt*tNmc@ew;(HHgGz?J=M!;XGh!CMhyBEB8$ zIYd7lV7bbEa@h8xFZ$UIwzj_5%N=0X@@(`+pF6?!slSC?8jj8*?x!>8_dcvb%SUgW zU}uSa=>qRYXPs9NYw3&HuY%pDsQntay7udE z@=@m?*gSb>v-Tmx`ubu9IdLD_yDL77K7z!0$H1;P1&LYS0L#Z-o&d`oMsY|5qUT%;`tVN6uNWeT4l9_#EOru*Ro|G4fIOGjP<^Z{0I! z`Ka?b*tNpG05+zdx!TpwTgV8Pgf8+}Kcf@?-_3uVp^D0{3zf@R&{0DbqS7HDF literal 0 HcmV?d00001 diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl new file mode 100644 index 0000000000..c700d78d25 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl @@ -0,0 +1,36 @@ +#version 450 + layout(location = 0) in vec3 aPos; + layout(location = 1) in vec3 aNormal; + + layout(location = 0) out vec3 FragPos; + layout(location = 1) out vec3 VecPos; + layout(location = 2) out vec3 Normal; + + layout(push_constant) uniform constants{ + float maxY; + float minY; + float time; + float disco; + mat4 model; + }; + + layout(binding = 0) uniform UniformBufferObject { + mat4 projection; + } ubo; + + void main() + { + float discoScale = sin(time * 10.0) / 10.0; + float distortionX = 1.0 + disco * cos(time * 20.0) / 10.0; + + float scale = 1.0 + disco * discoScale; + + vec3 scaledPos = aPos; + scaledPos.x = scaledPos.x * distortionX; + + scaledPos *= scale; + gl_Position = ubo.projection * model * vec4(scaledPos, 1.0); + FragPos = vec3(model * vec4(aPos, 1.0)); + VecPos = aPos; + Normal = normalize(vec3(model * vec4(aNormal, 1.0))); + } \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv new file mode 100644 index 0000000000000000000000000000000000000000..3bb2ba938342b011a5860a1d243c47065c78a7dc GIT binary patch literal 3276 zcmZ9MYj;ys5QYzJQUtk($VJ84QoMoI8z735UZAO3X|YhjTS$|%9!!&(q%QQ6esC>6 zxt9C_e~Z7$FRtbDoScC}cGq-f-kCk~&ffcEbYyC8lI%(LCkK;mGFV5F5s)N#IvLA! zWqECRy4P<^pTF>q8HbY5EYY09WVuSnb}emdPa`wPEu@CjkxgWjzY+95Y%I~SvRtjK ztW=h-SL-`F)n5PSR2;Np)ZALDPes;kZJdb8Va zKEMSX;M&GIz0@Y4d$W9J%J8(^UrT?<=NQDCjo3mf-MI$6TD#tSw9grh?Y3HvV$S9L zy2{j_Mc;0xo1O0V{O;yvv%9i!pZm)=8FTG)J7Q8|&uB6aw!6{6E@kgzv0K}6M19}P zdb6HQtnd4|LGc=gs_$E><-iii-~}eh6u2ueXBP8L0&`X|-$-DiId&k!N?ASklnXif z?t5kkwUzVV}U5ub#&b7&vO8beQH zQsNaQnTVaB-afRp^Pc4YezchWIGLV>@uHYBL5BBet~r-;-|tZD82%yj1R}R{xqld2 zUhemfqrY)`97TxEg?}>hf4v&}pT~E$h!4){?>T2V3vLbL)<1`KUUB`m&~nk2ufFd` zOuqi;P9QGp>%W_MkNuDE%|Au_GTMG(<5<3xn8%o6-z#W28Mm+gOpe7|SMgorWX_`F zjOVaDFERJ(&m&^iUdV0pqt+s}_h-CGeXQJ;LG{=7WwIRP=@IAv# zWDjzYFGF7Tc3Ie&$2Ml}42q>F>Cm4-bVb6jCo)Ba`4{XL&W@!qUZZL9{zcJxd(>}*ye~w zor~B}$3FVzMU4-zp{#DmLc*mAi^j&E@p5pxChIkuSZA+Q@c<~OyAnrHcxcBlG zb&ahe_BTh)e(PT#{?<5`bL*QIGu_4>o@pISJotZ!?fnL?JJ@2b!0uv;75RUaV@3X7 zgSp>)<3;}8;M?1rsQWFpxGS*lu*HhKsyR03HJ07^Juvs1Z`}EBq3 (byte*)h.Pointer; +} + +unsafe class ByteStringList : IDisposable +{ + private List _inner; + private byte** _ptr; + + public ByteStringList(IEnumerable items) + { + _inner = items.Select(x => new ByteString(x)).ToList(); + _ptr = (byte**)Marshal.AllocHGlobal(IntPtr.Size * _inner.Count + 1); + for (var c = 0; c < _inner.Count; c++) + _ptr[c] = (byte*)_inner[c].Pointer; + } + + public int Count => _inner.Count; + public uint UCount => (uint)_inner.Count; + + public void Dispose() + { + Marshal.FreeHGlobal(new IntPtr(_ptr)); + } + + public static implicit operator byte**(ByteStringList h) => h._ptr; +} diff --git a/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs new file mode 100644 index 0000000000..a0b7d32d3b --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +namespace GpuInterop.VulkanDemo; + +public class D3DMemoryHelper +{ + public static D3DDevice CreateDeviceByLuid(Span luid) + { + var factory = new DxgiFactory1(); + var longLuid = MemoryMarshal.Cast(luid)[0]; + for (var c = 0; c < factory.GetAdapterCount1(); c++) + { + using var adapter = factory.GetAdapter1(0); + if (adapter.Description1.Luid != longLuid) + continue; + + return new D3DDevice(adapter, DeviceCreationFlags.None, + new[] + { + FeatureLevel.Level_12_1, FeatureLevel.Level_12_0, FeatureLevel.Level_11_1, + FeatureLevel.Level_11_0, FeatureLevel.Level_10_0, FeatureLevel.Level_9_3, + FeatureLevel.Level_9_2, FeatureLevel.Level_9_1, + }); + } + + throw new ArgumentException("Device with the corresponding LUID not found"); + } + + public static Texture2D CreateMemoryHandle(D3DDevice device, PixelSize size, Silk.NET.Vulkan.Format format) + { + if (format != Silk.NET.Vulkan.Format.R8G8B8A8Unorm) + throw new ArgumentException("Not supported format"); + return new Texture2D(device, + new Texture2DDescription + { + Format = Format.R8G8B8A8_UNorm, + Width = size.Width, + Height = size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDescription = new SampleDescription { Count = 1, Quality = 0 }, + CpuAccessFlags = default, + OptionFlags = ResourceOptionFlags.SharedKeyedmutex, + BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource + }); + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs new file mode 100644 index 0000000000..949a951a36 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.CompilerServices; +using Silk.NET.Vulkan; +using SilkNetDemo; +using Buffer = Silk.NET.Vulkan.Buffer; +using SystemBuffer = System.Buffer; + +namespace GpuInterop.VulkanDemo; + +static class VulkanBufferHelper +{ + public unsafe static void AllocateBuffer(VulkanContext vk, + BufferUsageFlags bufferUsageFlags, + out Buffer buffer, out DeviceMemory memory, + Span initialData) where T:unmanaged + { + var api = vk.Api; + var device = vk.Device; + + var size = Unsafe.SizeOf() * initialData.Length; + var bufferInfo = new BufferCreateInfo() + { + SType = StructureType.BufferCreateInfo, + Size = (ulong)size, + Usage = bufferUsageFlags, + SharingMode = SharingMode.Exclusive + }; + api.CreateBuffer(device, bufferInfo, null, out buffer).ThrowOnError(); + + api.GetBufferMemoryRequirements(device, buffer, out var memoryRequirements); + + var physicalDevice = vk.PhysicalDevice; + + var memoryAllocateInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, + physicalDevice, + memoryRequirements.MemoryTypeBits, + MemoryPropertyFlags.MemoryPropertyHostCoherentBit | + MemoryPropertyFlags.MemoryPropertyHostVisibleBit) + }; + + api.AllocateMemory(device, memoryAllocateInfo, null, out memory).ThrowOnError(); + api.BindBufferMemory(device, buffer, memory, 0); + UpdateBufferMemory(vk, memory, initialData); + } + + public static unsafe void UpdateBufferMemory(VulkanContext vk, DeviceMemory memory, + Span data) where T : unmanaged + { + var api = vk.Api; + var device = vk.Device; + + var size = data.Length * Unsafe.SizeOf(); + void* pointer = null; + api.MapMemory(device, memory, 0, (ulong)size, 0, ref pointer); + + data.CopyTo(new Span(pointer, size)); + + api.UnmapMemory(device, memory); + + } + + private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs new file mode 100644 index 0000000000..65a05c5226 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using Avalonia.Input; +using Silk.NET.Vulkan; +using SilkNetDemo; + +namespace Avalonia.Vulkan +{ + public class VulkanCommandBufferPool : IDisposable + { + private readonly Vk _api; + private readonly Device _device; + private readonly Queue _queue; + private readonly CommandPool _commandPool; + + private readonly List _usedCommandBuffers = new(); + private object _lock = new object(); + + public unsafe VulkanCommandBufferPool(Vk api, Device device, Queue queue, uint queueFamilyIndex) + { + _api = api; + _device = device; + _queue = queue; + + var commandPoolCreateInfo = new CommandPoolCreateInfo + { + SType = StructureType.CommandPoolCreateInfo, + Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit, + QueueFamilyIndex = queueFamilyIndex + }; + + _api.CreateCommandPool(_device, commandPoolCreateInfo, null, out _commandPool) + .ThrowOnError(); + } + + public unsafe void Dispose() + { + lock (_lock) + { + FreeUsedCommandBuffers(); + _api.DestroyCommandPool(_device, _commandPool, null); + } + } + + private CommandBuffer AllocateCommandBuffer() + { + var commandBufferAllocateInfo = new CommandBufferAllocateInfo + { + SType = StructureType.CommandBufferAllocateInfo, + CommandPool = _commandPool, + CommandBufferCount = 1, + Level = CommandBufferLevel.Primary + }; + + lock (_lock) + { + _api.AllocateCommandBuffers(_device, commandBufferAllocateInfo, out var commandBuffer); + + return commandBuffer; + } + } + + public VulkanCommandBuffer CreateCommandBuffer() + { + return new(_api, _device, _queue, this); + } + + public void FreeUsedCommandBuffers() + { + lock (_lock) + { + foreach (var usedCommandBuffer in _usedCommandBuffers) usedCommandBuffer.Dispose(); + + _usedCommandBuffers.Clear(); + } + } + + private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer) + { + lock (_lock) + { + _usedCommandBuffers.Add(commandBuffer); + } + } + + public class VulkanCommandBuffer : IDisposable + { + private readonly VulkanCommandBufferPool _commandBufferPool; + private readonly Vk _api; + private readonly Device _device; + private readonly Queue _queue; + private readonly Fence _fence; + private bool _hasEnded; + private bool _hasStarted; + + public IntPtr Handle => InternalHandle.Handle; + + internal CommandBuffer InternalHandle { get; } + + internal unsafe VulkanCommandBuffer(Vk api, Device device, Queue queue, VulkanCommandBufferPool commandBufferPool) + { + _api = api; + _device = device; + _queue = queue; + _commandBufferPool = commandBufferPool; + + InternalHandle = _commandBufferPool.AllocateCommandBuffer(); + + var fenceCreateInfo = new FenceCreateInfo() + { + SType = StructureType.FenceCreateInfo, + Flags = FenceCreateFlags.FenceCreateSignaledBit + }; + + api.CreateFence(device, fenceCreateInfo, null, out _fence); + } + + public unsafe void Dispose() + { + _api.WaitForFences(_device, 1, _fence, true, ulong.MaxValue); + lock (_commandBufferPool._lock) + { + _api.FreeCommandBuffers(_device, _commandBufferPool._commandPool, 1, InternalHandle); + } + _api.DestroyFence(_device, _fence, null); + } + + public void BeginRecording() + { + if (!_hasStarted) + { + _hasStarted = true; + + var beginInfo = new CommandBufferBeginInfo + { + SType = StructureType.CommandBufferBeginInfo, + Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit + }; + + _api.BeginCommandBuffer(InternalHandle, beginInfo); + } + } + + public void EndRecording() + { + if (_hasStarted && !_hasEnded) + { + _hasEnded = true; + + _api.EndCommandBuffer(InternalHandle); + } + } + + public void Submit() + { + Submit(null, null, null, _fence); + } + + public class KeyedMutexSubmitInfo + { + public ulong? AcquireKey { get; set; } + public ulong? ReleaseKey { get; set; } + public DeviceMemory DeviceMemory { get; set; } + } + + public unsafe void Submit( + ReadOnlySpan waitSemaphores, + ReadOnlySpan waitDstStageMask = default, + ReadOnlySpan signalSemaphores = default, + Fence? fence = null, + KeyedMutexSubmitInfo keyedMutex = null) + { + EndRecording(); + + if (!fence.HasValue) + fence = _fence; + + + ulong acquireKey = keyedMutex?.AcquireKey ?? 0, releaseKey = keyedMutex?.ReleaseKey ?? 0; + DeviceMemory devMem = keyedMutex?.DeviceMemory ?? default; + uint timeout = uint.MaxValue; + Win32KeyedMutexAcquireReleaseInfoKHR mutex = default; + if (keyedMutex != null) + mutex = new Win32KeyedMutexAcquireReleaseInfoKHR + { + SType = StructureType.Win32KeyedMutexAcquireReleaseInfoKhr, + AcquireCount = keyedMutex.AcquireKey.HasValue ? 1u : 0u, + ReleaseCount = keyedMutex.ReleaseKey.HasValue ? 1u : 0u, + PAcquireKeys = &acquireKey, + PReleaseKeys = &releaseKey, + PAcquireSyncs = &devMem, + PReleaseSyncs = &devMem, + PAcquireTimeouts = &timeout + }; + + fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores) + { + fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask) + { + var commandBuffer = InternalHandle; + var submitInfo = new SubmitInfo + { + PNext = keyedMutex != null ? &mutex : null, + SType = StructureType.SubmitInfo, + WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0, + PWaitSemaphores = pWaitSemaphores, + PWaitDstStageMask = pWaitDstStageMask, + CommandBufferCount = 1, + PCommandBuffers = &commandBuffer, + SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0, + PSignalSemaphores = pSignalSemaphores, + }; + + _api.ResetFences(_device, 1, fence.Value); + + _api.QueueSubmit(_queue, 1, submitInfo, fence.Value); + } + } + + _commandBufferPool.DisposeCommandBuffer(this); + } + } + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanContent.cs b/samples/GpuInterop/VulkanDemo/VulkanContent.cs new file mode 100644 index 0000000000..b16343190a --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanContent.cs @@ -0,0 +1,829 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Threading; +using Silk.NET.Vulkan; +using SilkNetDemo; +using Buffer = System.Buffer; +using Image = Silk.NET.Vulkan.Image; + +namespace GpuInterop.VulkanDemo; + +unsafe class VulkanContent : IDisposable +{ + private readonly VulkanContext _context; + private ShaderModule _vertShader; + private ShaderModule _fragShader; + private PipelineLayout _pipelineLayout; + private RenderPass _renderPass; + private Pipeline _pipeline; + private DescriptorSetLayout _descriptorSetLayout; + private Silk.NET.Vulkan.Buffer _vertexBuffer; + private DeviceMemory _vertexBufferMemory; + private Silk.NET.Vulkan.Buffer _indexBuffer; + private DeviceMemory _indexBufferMemory; + private Silk.NET.Vulkan.Buffer _uniformBuffer; + private DeviceMemory _uniformBufferMemory; + private Framebuffer _framebuffer; + + private Image _depthImage; + private DeviceMemory _depthImageMemory; + private ImageView _depthImageView; + + public VulkanContent(VulkanContext context) + { + _context = context; + var name = typeof(VulkanContent).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin")); + using (var sr = new BinaryReader(typeof(VulkanContent).Assembly.GetManifestResourceStream(name))) + { + var buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + var points = new float[buf.Length / 4]; + Buffer.BlockCopy(buf, 0, points, 0, buf.Length); + buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + _indices = new ushort[buf.Length / 2]; + Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length); + _points = new Vertex[points.Length / 3]; + for (var primitive = 0; primitive < points.Length / 3; primitive++) + { + var srci = primitive * 3; + _points[primitive] = new Vertex + { + Position = new Vector3(points[srci], points[srci + 1], points[srci + 2]) + }; + } + + for (int i = 0; i < _indices.Length; i += 3) + { + Vector3 a = _points[_indices[i]].Position; + Vector3 b = _points[_indices[i + 1]].Position; + Vector3 c = _points[_indices[i + 2]].Position; + var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b)); + + _points[_indices[i]].Normal += normal; + _points[_indices[i + 1]].Normal += normal; + _points[_indices[i + 2]].Normal += normal; + } + + for (int i = 0; i < _points.Length; i++) + { + _points[i].Normal = Vector3.Normalize(_points[i].Normal); + _maxY = Math.Max(_maxY, _points[i].Position.Y); + _minY = Math.Min(_minY, _points[i].Position.Y); + } + } + + var api = _context.Api; + var device = _context.Device; + var vertShaderData = GetShader(false); + var fragShaderData = GetShader(true); + + fixed (byte* ptr = vertShaderData) + { + var shaderCreateInfo = new ShaderModuleCreateInfo() + { + SType = StructureType.ShaderModuleCreateInfo, + CodeSize = (nuint)vertShaderData.Length, + PCode = (uint*)ptr, + }; + + api.CreateShaderModule(device, shaderCreateInfo, null, out _vertShader); + } + + fixed (byte* ptr = fragShaderData) + { + var shaderCreateInfo = new ShaderModuleCreateInfo() + { + SType = StructureType.ShaderModuleCreateInfo, + CodeSize = (nuint)fragShaderData.Length, + PCode = (uint*)ptr, + }; + + api.CreateShaderModule(device, shaderCreateInfo, null, out _fragShader); + } + + CreateBuffers(); + } + + private byte[] GetShader(bool fragment) + { + var name = typeof(VulkanContent).Assembly.GetManifestResourceNames() + .First(x => x.Contains((fragment ? "frag" : "vert") + ".spirv")); + using (var sr = typeof(VulkanContent).Assembly.GetManifestResourceStream(name)) + { + using (var mem = new MemoryStream()) + { + sr.CopyTo(mem); + return mem.ToArray(); + } + } + } + + private PixelSize? _previousImageSize = PixelSize.Empty; + + + public void Render(VulkanImage image, + double yaw, double pitch, double roll, double disco) + { + + var api = _context.Api; + + if (image.Size != _previousImageSize) + CreateTemporalObjects(image.Size); + + _previousImageSize = image.Size; + + + var model = Matrix4x4.CreateFromYawPitchRoll((float)yaw, (float)pitch, (float)roll); + var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); + var projection = + Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)image.Size.Width / image.Size.Height), + 0.01f, 1000); + + var vertexConstant = new VertextPushConstant() + { + Disco = (float)disco, + MinY = _minY, + MaxY = _maxY, + Model = model, + Time = (float)St.Elapsed.TotalSeconds + }; + + var commandBuffer = _context.Pool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + + _colorAttachment.TransitionLayout(commandBuffer.InternalHandle, + ImageLayout.Undefined, AccessFlags.None, + ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentWriteBit); + + var commandBufferHandle = new CommandBuffer(commandBuffer.Handle); + + api.CmdSetViewport(commandBufferHandle, 0, 1, + new Viewport() + { + Width = (float)image.Size.Width, + Height = (float)image.Size.Height, + MaxDepth = 1, + MinDepth = 0, + X = 0, + Y = 0 + }); + + var scissor = new Rect2D + { + Extent = new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height) + }; + + api.CmdSetScissor(commandBufferHandle, 0, 1, &scissor); + + var clearColor = new ClearValue(new ClearColorValue(1, 0, 0, 0.1f), new ClearDepthStencilValue(1, 0)); + + var clearValues = new[] { clearColor, clearColor }; + + + fixed (ClearValue* clearValue = clearValues) + { + var beginInfo = new RenderPassBeginInfo() + { + SType = StructureType.RenderPassBeginInfo, + RenderPass = _renderPass, + Framebuffer = _framebuffer, + RenderArea = new Rect2D(new Offset2D(0, 0), new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height)), + ClearValueCount = 2, + PClearValues = clearValue + }; + + api.CmdBeginRenderPass(commandBufferHandle, beginInfo, SubpassContents.Inline); + } + + api.CmdBindPipeline(commandBufferHandle, PipelineBindPoint.Graphics, _pipeline); + + var dset = _descriptorSet; + api.CmdBindDescriptorSets(commandBufferHandle, PipelineBindPoint.Graphics, + _pipelineLayout,0,1, &dset, null); + + api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.ShaderStageVertexBit | ShaderStageFlags.FragmentBit, 0, + (uint)Marshal.SizeOf(), &vertexConstant); + api.CmdBindVertexBuffers(commandBufferHandle, 0, 1, _vertexBuffer, 0); + api.CmdBindIndexBuffer(commandBufferHandle, _indexBuffer, 0, IndexType.Uint16); + + api.CmdDrawIndexed(commandBufferHandle, (uint)_indices.Length, 1, 0, 0, 0); + + + api.CmdEndRenderPass(commandBufferHandle); + + _colorAttachment.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit); + image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit); + + + var srcBlitRegion = new ImageBlit + { + SrcOffsets = new ImageBlit.SrcOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1), + }, + DstOffsets = new ImageBlit.DstOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1), + }, + SrcSubresource = + new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + }, + DstSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + } + }; + + api.CmdBlitImage(commandBuffer.InternalHandle, _colorAttachment.InternalHandle.Value, + ImageLayout.TransferSrcOptimal, + image.InternalHandle.Value, ImageLayout.TransferDstOptimal, 1, srcBlitRegion, Filter.Linear); + + commandBuffer.Submit(); + } + + public unsafe void Dispose() + { + if (_isInit) + { + var api = _context.Api; + var device = _context.Device; + + DestroyTemporalObjects(); + + api.DestroyShaderModule(device, _vertShader, null); + api.DestroyShaderModule(device, _fragShader, null); + + api.DestroyBuffer(device, _vertexBuffer, null); + api.FreeMemory(device, _vertexBufferMemory, null); + + api.DestroyBuffer(device, _indexBuffer, null); + api.FreeMemory(device, _indexBufferMemory, null); + + + } + + _isInit = false; + } + + public unsafe void DestroyTemporalObjects() + { + if (_isInit) + { + if (_renderPass.Handle != 0) + { + var api = _context.Api; + var device = _context.Device; + api.FreeDescriptorSets(_context.Device, _context.DescriptorPool, new[] { _descriptorSet }); + + api.DestroyImageView(device, _depthImageView, null); + api.DestroyImage(device, _depthImage, null); + api.FreeMemory(device, _depthImageMemory, null); + + api.DestroyFramebuffer(device, _framebuffer, null); + api.DestroyPipeline(device, _pipeline, null); + api.DestroyPipelineLayout(device, _pipelineLayout, null); + api.DestroyRenderPass(device, _renderPass, null); + api.DestroyDescriptorSetLayout(device, _descriptorSetLayout, null); + + api.DestroyBuffer(device, _uniformBuffer, null); + api.FreeMemory(device, _uniformBufferMemory, null); + _colorAttachment?.Dispose(); + + _colorAttachment = null; + _depthImage = default; + _depthImageView = default; + _depthImageView = default; + _framebuffer = default; + _pipeline = default; + _renderPass = default; + _pipelineLayout = default; + _descriptorSetLayout = default; + _uniformBuffer = default; + _uniformBufferMemory = default; + } + } + } + + private unsafe void CreateDepthAttachment(PixelSize size) + { + var imageCreateInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = Format.D32Sfloat, + Extent = + new Extent3D((uint?)size.Width, + (uint?)size.Height, 1), + MipLevels = 1, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = ImageTiling.Optimal, + Usage = ImageUsageFlags.ImageUsageDepthStencilAttachmentBit, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + var api = _context.Api; + var device = _context.Device; + api + .CreateImage(device, imageCreateInfo, null, out _depthImage).ThrowOnError(); + + api.GetImageMemoryRequirements(device, _depthImage, + out var memoryRequirements); + + var memoryAllocateInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, + _context.PhysicalDevice, + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + }; + + api.AllocateMemory(device, memoryAllocateInfo, null, + out _depthImageMemory).ThrowOnError(); + + api.BindImageMemory(device, _depthImage, _depthImageMemory, 0); + + var componentMapping = new ComponentMapping( + ComponentSwizzle.R, + ComponentSwizzle.G, + ComponentSwizzle.B, + ComponentSwizzle.A); + + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectDepthBit, + 0, 1, 0, 1); + + var imageViewCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = _depthImage, + ViewType = ImageViewType.ImageViewType2D, + Format = Format.D32Sfloat, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + api + .CreateImageView(device, imageViewCreateInfo, null, out _depthImageView) + .ThrowOnError(); + } + + private unsafe void CreateTemporalObjects(PixelSize size) + { + DestroyTemporalObjects(); + + var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); + var projection = + Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)size.Width / size.Height), + 0.01f, 1000); + + _colorAttachment = new VulkanImage(_context, (uint)Format.R8G8B8A8Unorm, size, false); + CreateDepthAttachment(size); + + var api = _context.Api; + var device = _context.Device; + + // create renderpasses + var colorAttachment = new AttachmentDescription() + { + Format = Format.R8G8B8A8Unorm, + Samples = SampleCountFlags.SampleCount1Bit, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.Store, + InitialLayout = ImageLayout.Undefined, + FinalLayout = ImageLayout.ColorAttachmentOptimal, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare + }; + + var depthAttachment = new AttachmentDescription() + { + Format = Format.D32Sfloat, + Samples = SampleCountFlags.SampleCount1Bit, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.DontCare, + InitialLayout = ImageLayout.Undefined, + FinalLayout = ImageLayout.DepthStencilAttachmentOptimal, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare + }; + + var subpassDependency = new SubpassDependency() + { + SrcSubpass = Vk.SubpassExternal, + DstSubpass = 0, + SrcStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, + SrcAccessMask = 0, + DstStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, + DstAccessMask = AccessFlags.AccessColorAttachmentWriteBit + }; + + var colorAttachmentReference = new AttachmentReference() + { + Attachment = 0, Layout = ImageLayout.ColorAttachmentOptimal + }; + + var depthAttachmentReference = new AttachmentReference() + { + Attachment = 1, Layout = ImageLayout.DepthStencilAttachmentOptimal + }; + + var subpassDescription = new SubpassDescription() + { + PipelineBindPoint = PipelineBindPoint.Graphics, + ColorAttachmentCount = 1, + PColorAttachments = &colorAttachmentReference, + PDepthStencilAttachment = &depthAttachmentReference + }; + + var attachments = new[] { colorAttachment, depthAttachment }; + + fixed (AttachmentDescription* atPtr = attachments) + { + var renderPassCreateInfo = new RenderPassCreateInfo() + { + SType = StructureType.RenderPassCreateInfo, + AttachmentCount = (uint)attachments.Length, + PAttachments = atPtr, + SubpassCount = 1, + PSubpasses = &subpassDescription, + DependencyCount = 1, + PDependencies = &subpassDependency + }; + + api.CreateRenderPass(device, renderPassCreateInfo, null, out _renderPass).ThrowOnError(); + + + // create framebuffer + var frameBufferAttachments = new[] { new ImageView(_colorAttachment.ViewHandle), _depthImageView }; + + fixed (ImageView* frAtPtr = frameBufferAttachments) + { + var framebufferCreateInfo = new FramebufferCreateInfo() + { + SType = StructureType.FramebufferCreateInfo, + RenderPass = _renderPass, + AttachmentCount = (uint)frameBufferAttachments.Length, + PAttachments = frAtPtr, + Width = (uint)size.Width, + Height = (uint)size.Height, + Layers = 1 + }; + + api.CreateFramebuffer(device, framebufferCreateInfo, null, out _framebuffer).ThrowOnError(); + } + } + + // Create pipeline + var pname = Marshal.StringToHGlobalAnsi("main"); + var vertShaderStageInfo = new PipelineShaderStageCreateInfo() + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.ShaderStageVertexBit, + Module = _vertShader, + PName = (byte*)pname, + }; + var fragShaderStageInfo = new PipelineShaderStageCreateInfo() + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.ShaderStageFragmentBit, + Module = _fragShader, + PName = (byte*)pname, + }; + + var stages = new[] { vertShaderStageInfo, fragShaderStageInfo }; + + var bindingDescription = Vertex.VertexInputBindingDescription; + var attributeDescription = Vertex.VertexInputAttributeDescription; + + fixed (VertexInputAttributeDescription* attrPtr = attributeDescription) + { + var vertextInputInfo = new PipelineVertexInputStateCreateInfo() + { + SType = StructureType.PipelineVertexInputStateCreateInfo, + VertexAttributeDescriptionCount = (uint)attributeDescription.Length, + VertexBindingDescriptionCount = 1, + PVertexAttributeDescriptions = attrPtr, + PVertexBindingDescriptions = &bindingDescription + }; + + var inputAssembly = new PipelineInputAssemblyStateCreateInfo() + { + SType = StructureType.PipelineInputAssemblyStateCreateInfo, + Topology = PrimitiveTopology.TriangleList, + PrimitiveRestartEnable = false + }; + + var viewport = new Viewport() + { + X = 0, + Y = 0, + Width = (float)size.Width, + Height = (float)size.Height, + MinDepth = 0, + MaxDepth = 1 + }; + + var scissor = new Rect2D() + { + Offset = new Offset2D(0, 0), Extent = new Extent2D((uint)viewport.Width, (uint)viewport.Height) + }; + + var pipelineViewPortCreateInfo = new PipelineViewportStateCreateInfo() + { + SType = StructureType.PipelineViewportStateCreateInfo, + ViewportCount = 1, + PViewports = &viewport, + ScissorCount = 1, + PScissors = &scissor + }; + + var rasterizerStateCreateInfo = new PipelineRasterizationStateCreateInfo() + { + SType = StructureType.PipelineRasterizationStateCreateInfo, + DepthClampEnable = false, + RasterizerDiscardEnable = false, + PolygonMode = PolygonMode.Fill, + LineWidth = 1, + CullMode = CullModeFlags.CullModeNone, + DepthBiasEnable = false + }; + + var multisampleStateCreateInfo = new PipelineMultisampleStateCreateInfo() + { + SType = StructureType.PipelineMultisampleStateCreateInfo, + SampleShadingEnable = false, + RasterizationSamples = SampleCountFlags.SampleCount1Bit + }; + + var depthStencilCreateInfo = new PipelineDepthStencilStateCreateInfo() + { + SType = StructureType.PipelineDepthStencilStateCreateInfo, + StencilTestEnable = false, + DepthCompareOp = CompareOp.Less, + DepthTestEnable = true, + DepthWriteEnable = true, + DepthBoundsTestEnable = false, + }; + + var colorBlendAttachmentState = new PipelineColorBlendAttachmentState() + { + ColorWriteMask = ColorComponentFlags.ColorComponentABit | + ColorComponentFlags.ColorComponentRBit | + ColorComponentFlags.ColorComponentGBit | + ColorComponentFlags.ColorComponentBBit, + BlendEnable = false + }; + + var colorBlendState = new PipelineColorBlendStateCreateInfo() + { + SType = StructureType.PipelineColorBlendStateCreateInfo, + LogicOpEnable = false, + AttachmentCount = 1, + PAttachments = &colorBlendAttachmentState + }; + + var dynamicStates = new DynamicState[] { DynamicState.Viewport, DynamicState.Scissor }; + + fixed (DynamicState* states = dynamicStates) + { + var dynamicStateCreateInfo = new PipelineDynamicStateCreateInfo() + { + SType = StructureType.PipelineDynamicStateCreateInfo, + DynamicStateCount = (uint)dynamicStates.Length, + PDynamicStates = states + }; + + var vertexPushConstantRange = new PushConstantRange() + { + Offset = 0, + Size = (uint)Marshal.SizeOf(), + StageFlags = ShaderStageFlags.ShaderStageVertexBit + }; + + var fragPushConstantRange = new PushConstantRange() + { + //Offset = vertexPushConstantRange.Size, + Size = (uint)Marshal.SizeOf(), + StageFlags = ShaderStageFlags.ShaderStageFragmentBit + }; + + var layoutBindingInfo = new DescriptorSetLayoutBinding + { + Binding = 0, + StageFlags = ShaderStageFlags.VertexBit, + DescriptorCount = 1, + DescriptorType = DescriptorType.UniformBuffer, + }; + + var layoutInfo = new DescriptorSetLayoutCreateInfo + { + SType = StructureType.DescriptorSetLayoutCreateInfo, + BindingCount = 1, + PBindings = &layoutBindingInfo + }; + + api.CreateDescriptorSetLayout(device, &layoutInfo, null, out _descriptorSetLayout).ThrowOnError(); + + var projView = view * projection; + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.UniformBufferBit, + out _uniformBuffer, + out _uniformBufferMemory, new[] + { + new UniformBuffer + { + Projection = projView + } + }); + + var descriptorSetLayout = _descriptorSetLayout; + var descriptorCreateInfo = new DescriptorSetAllocateInfo + { + SType = StructureType.DescriptorSetAllocateInfo, + DescriptorPool = _context.DescriptorPool, + DescriptorSetCount = 1, + PSetLayouts = &descriptorSetLayout + }; + api.AllocateDescriptorSets(device, &descriptorCreateInfo, out _descriptorSet).ThrowOnError(); + + var descriptorBufferInfo = new DescriptorBufferInfo + { + Buffer = _uniformBuffer, + Range = (ulong)Unsafe.SizeOf(), + }; + var descriptorWrite = new WriteDescriptorSet + { + SType = StructureType.WriteDescriptorSet, + DstSet = _descriptorSet, + DescriptorType = DescriptorType.UniformBuffer, + DescriptorCount = 1, + PBufferInfo = &descriptorBufferInfo, + }; + api.UpdateDescriptorSets(device, 1, &descriptorWrite, 0, null); + + var constants = new[] { vertexPushConstantRange, fragPushConstantRange }; + + fixed (PushConstantRange* constant = constants) + { + var setLayout = _descriptorSetLayout; + var pipelineLayoutCreateInfo = new PipelineLayoutCreateInfo() + { + SType = StructureType.PipelineLayoutCreateInfo, + PushConstantRangeCount = (uint)constants.Length, + PPushConstantRanges = constant, + SetLayoutCount = 1, + PSetLayouts = &setLayout + }; + + api.CreatePipelineLayout(device, pipelineLayoutCreateInfo, null, out _pipelineLayout) + .ThrowOnError(); + } + + + fixed (PipelineShaderStageCreateInfo* stPtr = stages) + { + var pipelineCreateInfo = new GraphicsPipelineCreateInfo() + { + SType = StructureType.GraphicsPipelineCreateInfo, + StageCount = 2, + PStages = stPtr, + PVertexInputState = &vertextInputInfo, + PInputAssemblyState = &inputAssembly, + PViewportState = &pipelineViewPortCreateInfo, + PRasterizationState = &rasterizerStateCreateInfo, + PMultisampleState = &multisampleStateCreateInfo, + PDepthStencilState = &depthStencilCreateInfo, + PColorBlendState = &colorBlendState, + PDynamicState = &dynamicStateCreateInfo, + Layout = _pipelineLayout, + RenderPass = _renderPass, + Subpass = 0, + BasePipelineHandle = _pipeline.Handle != 0 ? _pipeline : new Pipeline(), + BasePipelineIndex = _pipeline.Handle != 0 ? 0 : -1 + }; + + api.CreateGraphicsPipelines(device, new PipelineCache(), 1, &pipelineCreateInfo, null, + out _pipeline).ThrowOnError(); + } + } + } + + Marshal.FreeHGlobal(pname); + _isInit = true; + } + + private unsafe void CreateBuffers() + { + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.VertexBufferBit, out _vertexBuffer, + out _vertexBufferMemory, _points); + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.IndexBufferBit, out _indexBuffer, + out _indexBufferMemory, _indices); + } + + private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } + + + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Vertex + { + public Vector3 Position; + public Vector3 Normal; + + public static unsafe VertexInputBindingDescription VertexInputBindingDescription + { + get + { + return new VertexInputBindingDescription() + { + Binding = 0, + Stride = (uint)Marshal.SizeOf(), + InputRate = VertexInputRate.Vertex + }; + } + } + + public static unsafe VertexInputAttributeDescription[] VertexInputAttributeDescription + { + get + { + return new VertexInputAttributeDescription[] + { + new VertexInputAttributeDescription + { + Binding = 0, + Location = 0, + Format = Format.R32G32B32Sfloat, + Offset = (uint)Marshal.OffsetOf("Position") + }, + new VertexInputAttributeDescription + { + Binding = 0, + Location = 1, + Format = Format.R32G32B32Sfloat, + Offset = (uint)Marshal.OffsetOf("Normal") + } + }; + } + } + } + + private readonly Vertex[] _points; + private readonly ushort[] _indices; + private readonly float _minY; + private readonly float _maxY; + + + static Stopwatch St = Stopwatch.StartNew(); + private bool _isInit; + private VulkanImage _colorAttachment; + private DescriptorSet _descriptorSet; + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct VertextPushConstant + { + public float MaxY; + public float MinY; + public float Time; + public float Disco; + public Matrix4x4 Model; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct UniformBuffer + { + public Matrix4x4 Projection; + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanContext.cs b/samples/GpuInterop/VulkanDemo/VulkanContext.cs new file mode 100644 index 0000000000..56041d6965 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanContext.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.Vulkan; +using Silk.NET.Core; +using Silk.NET.Core.Native; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.EXT; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; +using SkiaSharp; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiDevice = SharpDX.DXGI.Device; + +namespace GpuInterop.VulkanDemo; + +public unsafe class VulkanContext : IDisposable +{ + public Vk Api { get; init; } + public Instance Instance { get; init; } + public PhysicalDevice PhysicalDevice { get; init; } + public Device Device { get; init; } + public Queue Queue { get; init; } + public uint QueueFamilyIndex { get; init; } + public VulkanCommandBufferPool Pool { get; init; } + public GRContext GrContext { get; init; } + public DescriptorPool DescriptorPool { get; init; } + public D3DDevice? D3DDevice { get; init; } + + public static (VulkanContext? result, string info) TryCreate(ICompositionGpuInterop gpuInterop) + { + using var appName = new ByteString("GpuInterop"); + using var engineName = new ByteString("Test"); + var applicationInfo = new ApplicationInfo + { + SType = StructureType.ApplicationInfo, + PApplicationName = appName, + ApiVersion = new Version32(1, 1, 0), + PEngineName = appName, + EngineVersion = new Version32(1, 0, 0), + ApplicationVersion = new Version32(1, 0, 0) + }; + + var enabledExtensions = new List() + { + "VK_KHR_get_physical_device_properties2", + "VK_KHR_external_memory_capabilities", + "VK_KHR_external_semaphore_capabilities" + }; + + var enabledLayers = new List(); + + Vk api = Vk.GetApi(); + enabledExtensions.Add("VK_EXT_debug_utils"); + if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation")) + enabledLayers.Add("VK_LAYER_KHRONOS_validation"); + + + Instance vkInstance = default; + Silk.NET.Vulkan.PhysicalDevice physicalDevice = default; + Device device = default; + DescriptorPool descriptorPool = default; + VulkanCommandBufferPool? pool = null; + GRContext? grContext = null; + try + { + using var pRequiredExtensions = new ByteStringList(enabledExtensions); + using var pEnabledLayers = new ByteStringList(enabledLayers); + api.CreateInstance(new InstanceCreateInfo + { + SType = StructureType.InstanceCreateInfo, + PApplicationInfo = &applicationInfo, + PpEnabledExtensionNames = pRequiredExtensions, + EnabledExtensionCount = pRequiredExtensions.UCount, + PpEnabledLayerNames = pEnabledLayers, + EnabledLayerCount = pEnabledLayers.UCount + }, null, out vkInstance).ThrowOnError(); + + + if (api.TryGetInstanceExtension(vkInstance, out ExtDebugUtils debugUtils)) + { + var debugCreateInfo = new DebugUtilsMessengerCreateInfoEXT + { + SType = StructureType.DebugUtilsMessengerCreateInfoExt, + MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityVerboseBitExt | + DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityWarningBitExt | + DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityErrorBitExt, + MessageType = DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeGeneralBitExt | + DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeValidationBitExt | + DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypePerformanceBitExt, + PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(LogCallback), + }; + + debugUtils.CreateDebugUtilsMessenger(vkInstance, debugCreateInfo, null, out var messenger); + } + + var requireDeviceExtensions = new List + { + "VK_KHR_external_memory", + "VK_KHR_external_semaphore" + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) + ) + return (null, "Image sharing is not supported by the current backend"); + requireDeviceExtensions.Add(KhrExternalMemoryWin32.ExtensionName); + requireDeviceExtensions.Add(KhrExternalSemaphoreWin32.ExtensionName); + requireDeviceExtensions.Add("VK_KHR_dedicated_allocation"); + requireDeviceExtensions.Add("VK_KHR_get_memory_requirements2"); + } + else + { + if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .VulkanOpaquePosixFileDescriptor) + || !gpuInterop.SupportedSemaphoreTypes.Contains(KnownPlatformGraphicsExternalSemaphoreHandleTypes + .VulkanOpaquePosixFileDescriptor) + ) + return (null, "Image sharing is not supported by the current backend"); + requireDeviceExtensions.Add(KhrExternalMemoryFd.ExtensionName); + requireDeviceExtensions.Add(KhrExternalSemaphoreFd.ExtensionName); + } + + uint count = 0; + api.EnumeratePhysicalDevices(vkInstance, ref count, null).ThrowOnError(); + var physicalDevices = stackalloc PhysicalDevice[(int)count]; + api.EnumeratePhysicalDevices(vkInstance, ref count, physicalDevices) + .ThrowOnError(); + + for (uint c = 0; c < count; c++) + { + if (requireDeviceExtensions.Any(ext => !api.IsDeviceExtensionPresent(physicalDevices[c], ext))) + continue; + + var physicalDeviceIDProperties = new PhysicalDeviceIDProperties() + { + SType = StructureType.PhysicalDeviceIDProperties + }; + var physicalDeviceProperties2 = new PhysicalDeviceProperties2() + { + SType = StructureType.PhysicalDeviceProperties2, + PNext = &physicalDeviceIDProperties + }; + api.GetPhysicalDeviceProperties2(physicalDevices[c], &physicalDeviceProperties2); + + if (gpuInterop.DeviceLuid != null && physicalDeviceIDProperties.DeviceLuidvalid) + { + if (!new Span(physicalDeviceIDProperties.DeviceLuid, 8) + .SequenceEqual(gpuInterop.DeviceLuid)) + continue; + } + else if (gpuInterop.DeviceUuid != null) + { + if (!new Span(physicalDeviceIDProperties.DeviceUuid, 16) + .SequenceEqual(gpuInterop?.DeviceUuid)) + continue; + } + + physicalDevice = physicalDevices[c]; + + var name = Marshal.PtrToStringAnsi(new IntPtr(physicalDeviceProperties2.Properties.DeviceName))!; + + + uint queueFamilyCount = 0; + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, null); + var familyProperties = stackalloc QueueFamilyProperties[(int)queueFamilyCount]; + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, familyProperties); + for (uint queueFamilyIndex = 0; queueFamilyIndex < queueFamilyCount; queueFamilyIndex++) + { + var family = familyProperties[c]; + if (!family.QueueFlags.HasAllFlags(QueueFlags.GraphicsBit)) + continue; + + + var queuePriorities = stackalloc float[(int)family.QueueCount]; + + for (var i = 0; i < family.QueueCount; i++) + queuePriorities[i] = 1f; + + var features = new PhysicalDeviceFeatures(); + + var queueCreateInfo = new DeviceQueueCreateInfo + { + SType = StructureType.DeviceQueueCreateInfo, + QueueFamilyIndex = queueFamilyIndex, + QueueCount = family.QueueCount, + PQueuePriorities = queuePriorities + }; + + using var pEnabledDeviceExtensions = new ByteStringList(requireDeviceExtensions); + var deviceCreateInfo = new DeviceCreateInfo + { + SType = StructureType.DeviceCreateInfo, + QueueCreateInfoCount = 1, + PQueueCreateInfos = &queueCreateInfo, + PpEnabledExtensionNames = pEnabledDeviceExtensions, + EnabledExtensionCount = pEnabledDeviceExtensions.UCount, + PEnabledFeatures = &features + }; + + api.CreateDevice(physicalDevice, in deviceCreateInfo, null, out device) + .ThrowOnError(); + + api.GetDeviceQueue(device, queueFamilyIndex, 0, out var queue); + + var descriptorPoolSize = new DescriptorPoolSize + { + Type = DescriptorType.UniformBuffer, DescriptorCount = 16 + }; + var descriptorPoolInfo = new DescriptorPoolCreateInfo + { + SType = StructureType.DescriptorPoolCreateInfo, + PoolSizeCount = 1, + PPoolSizes = &descriptorPoolSize, + MaxSets = 16, + Flags = DescriptorPoolCreateFlags.FreeDescriptorSetBit + }; + + api.CreateDescriptorPool(device, &descriptorPoolInfo, null, out descriptorPool) + .ThrowOnError(); + + pool = new VulkanCommandBufferPool(api, device, queue, queueFamilyIndex); + grContext = GRContext.CreateVulkan(new GRVkBackendContext + { + VkInstance = vkInstance.Handle, + VkDevice = device.Handle, + VkQueue = queue.Handle, + GraphicsQueueIndex = queueFamilyIndex, + VkPhysicalDevice = physicalDevice.Handle, + GetProcedureAddress = (proc, _, _) => + { + var rv = api.GetDeviceProcAddr(device, proc); + if (rv != IntPtr.Zero) + return rv; + rv = api.GetInstanceProcAddr(vkInstance, proc); + if (rv != IntPtr.Zero) + return rv; + return api.GetInstanceProcAddr(default, proc); + } + }); + + + D3DDevice? d3dDevice = null; + if (physicalDeviceIDProperties.DeviceLuidvalid && + RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + d3dDevice = D3DMemoryHelper.CreateDeviceByLuid( + new Span(physicalDeviceIDProperties.DeviceLuid, 8)); + + var dxgiDevice = d3dDevice?.QueryInterface(); + return (new VulkanContext + { + Api = api, + Device = device, + Instance = vkInstance, + PhysicalDevice = physicalDevice, + Queue = queue, + QueueFamilyIndex = queueFamilyIndex, + Pool = pool, + DescriptorPool = descriptorPool, + GrContext = grContext, + D3DDevice = d3dDevice + }, name); + } + return (null, "No suitable device queue found"); + } + + return (null, "Suitable device not found"); + + } + catch (Exception e) + { + return (null, e.ToString()); + } + finally + { + if (grContext == null && api != null) + { + pool?.Dispose(); + if (descriptorPool.Handle != default) + api.DestroyDescriptorPool(device, descriptorPool, null); + if (device.Handle != default) + api.DestroyDevice(device, null); + } + } + } + + private static unsafe bool IsLayerAvailable(Vk api, string layerName) + { + uint layerPropertiesCount; + + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, null).ThrowOnError(); + + var layerProperties = new LayerProperties[layerPropertiesCount]; + + fixed (LayerProperties* pLayerProperties = layerProperties) + { + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, layerProperties).ThrowOnError(); + + for (var i = 0; i < layerPropertiesCount; i++) + { + var currentLayerName = Marshal.PtrToStringAnsi((IntPtr)pLayerProperties[i].LayerName); + + if (currentLayerName == layerName) return true; + } + } + + return false; + } + + private static unsafe uint LogCallback(DebugUtilsMessageSeverityFlagsEXT messageSeverity, DebugUtilsMessageTypeFlagsEXT messageTypes, DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) + { + if (messageSeverity != DebugUtilsMessageSeverityFlagsEXT.VerboseBitExt) + { + var message = Marshal.PtrToStringAnsi((nint)pCallbackData->PMessage); + Console.WriteLine(message); + } + + return Vk.False; + } + + public void Dispose() + { + D3DDevice?.Dispose(); + GrContext.Dispose(); + Pool.Dispose(); + Api.DestroyDescriptorPool(Device, DescriptorPool, null); + Api.DestroyDevice(Device, null); + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs new file mode 100644 index 0000000000..6a1cd641b3 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Silk.NET.Core; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; + +namespace GpuInterop.VulkanDemo; + +public class VulkanDemoControl : DrawingSurfaceDemoBase +{ + private Instance _vkInstance; + private Vk _api; + + class VulkanResources : IAsyncDisposable + { + public VulkanContext Context { get; } + public VulkanSwapchain Swapchain { get; } + public VulkanContent Content { get; } + + public VulkanResources(VulkanContext context, VulkanSwapchain swapchain, VulkanContent content) + { + Context = context; + Swapchain = swapchain; + Content = content; + } + public async ValueTask DisposeAsync() + { + Context.Pool.FreeUsedCommandBuffers(); + Content.Dispose(); + await Swapchain.DisposeAsync(); + Context.Dispose(); + } + } + + protected override bool SupportsDisco => true; + + private VulkanResources? _resources; + + protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop) + { + var (context, info) = VulkanContext.TryCreate(gpuInterop); + if (context == null) + return (false, info); + try + { + var content = new VulkanContent(context); + _resources = new VulkanResources(context, + new VulkanSwapchain(context, gpuInterop, compositionDrawingSurface), content); + return (true, info); + } + catch(Exception e) + { + return (false, e.ToString()); + } + } + + protected override void FreeGraphicsResources() + { + _resources?.DisposeAsync(); + _resources = null; + } + + protected override unsafe void RenderFrame(PixelSize pixelSize) + { + if (_resources == null) + return; + using (_resources.Swapchain.BeginDraw(pixelSize, out var image)) + { + /* + var commandBuffer = _resources.Context.Pool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.None); + + var range = new ImageSubresourceRange + { + AspectMask = ImageAspectFlags.ColorBit, + LayerCount = 1, + LevelCount = 1, + BaseArrayLayer = 0, + BaseMipLevel = 0 + }; + var color = new ClearColorValue + { + Float32_0 = 1, Float32_1 = 0, Float32_2 = 0, Float32_3 = 1 + }; + _resources.Context.Api.CmdClearColorImage(commandBuffer.InternalHandle, image.InternalHandle.Value, ImageLayout.TransferDstOptimal, + &color, 1, &range); + commandBuffer.Submit();*/ + _resources.Content.Render(image, Yaw, Pitch, Roll, Disco); + } + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs b/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs new file mode 100644 index 0000000000..1a13ebd4c5 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs @@ -0,0 +1,12 @@ +using System; +using Silk.NET.Vulkan; + +namespace SilkNetDemo; + +public static class VulkanExtensions +{ + public static void ThrowOnError(this Result result) + { + if (result != Result.Success) throw new Exception($"Unexpected API error \"{result}\"."); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanImage.cs b/samples/GpuInterop/VulkanDemo/VulkanImage.cs new file mode 100644 index 0000000000..e8854bfeb2 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanImage.cs @@ -0,0 +1,276 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Vulkan; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; +using SkiaSharp; + +namespace GpuInterop.VulkanDemo; + +public unsafe class VulkanImage : IDisposable + { + private readonly VulkanContext _vk; + private readonly Instance _instance; + private readonly Device _device; + private readonly PhysicalDevice _physicalDevice; + private readonly VulkanCommandBufferPool _commandBufferPool; + private ImageLayout _currentLayout; + private AccessFlags _currentAccessFlags; + private ImageUsageFlags _imageUsageFlags { get; } + private ImageView? _imageView { get; set; } + private DeviceMemory _imageMemory { get; set; } + private SharpDX.Direct3D11.Texture2D? _d3dTexture2D; + private IntPtr _win32ShareHandle; + + internal Image? InternalHandle { get; private set; } + internal Format Format { get; } + internal ImageAspectFlags AspectFlags { get; private set; } + + public ulong Handle => InternalHandle?.Handle ?? 0; + public ulong ViewHandle => _imageView?.Handle ?? 0; + public uint UsageFlags => (uint) _imageUsageFlags; + public ulong MemoryHandle => _imageMemory.Handle; + public DeviceMemory DeviceMemory => _imageMemory; + public uint MipLevels { get; private set; } + public Vk Api { get; } + public PixelSize Size { get; } + public ulong MemorySize { get; private set; } + public uint CurrentLayout => (uint) _currentLayout; + + public VulkanImage(VulkanContext vk, uint format, PixelSize size, + bool exportable, uint mipLevels = 0) + { + _vk = vk; + _instance = vk.Instance; + _device = vk.Device; + _physicalDevice = vk.PhysicalDevice; + _commandBufferPool = vk.Pool; + Format = (Format)format; + Api = vk.Api; + Size = size; + MipLevels = 1;//mipLevels; + _imageUsageFlags = + ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit | + ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit; + + //MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2)); + + var handleType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit : + ExternalMemoryHandleTypeFlags.OpaqueFDBit; + var externalMemoryCreateInfo = new ExternalMemoryImageCreateInfo + { + SType = StructureType.ExternalMemoryImageCreateInfo, + HandleTypes = handleType + }; + + var imageCreateInfo = new ImageCreateInfo + { + PNext = exportable ? &externalMemoryCreateInfo : null, + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = Format, + Extent = + new Extent3D((uint?)Size.Width, + (uint?)Size.Height, 1), + MipLevels = MipLevels, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = Tiling, + Usage = _imageUsageFlags, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + Api + .CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError(); + InternalHandle = image; + + Api.GetImageMemoryRequirements(_device, InternalHandle.Value, + out var memoryRequirements); + + + var fdExport = new ExportMemoryAllocateInfo + { + HandleTypes = handleType, SType = StructureType.ExportMemoryAllocateInfo + }; + var dedicatedAllocation = new MemoryDedicatedAllocateInfoKHR + { + SType = StructureType.MemoryDedicatedAllocateInfoKhr, + Image = image + }; + ImportMemoryWin32HandleInfoKHR handleImport = default; + if (exportable && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _d3dTexture2D = D3DMemoryHelper.CreateMemoryHandle(vk.D3DDevice, size, Format); + using var dxgi = _d3dTexture2D.QueryInterface(); + _win32ShareHandle = dxgi.SharedHandle; + handleImport = new ImportMemoryWin32HandleInfoKHR + { + PNext = &dedicatedAllocation, + SType = StructureType.ImportMemoryWin32HandleInfoKhr, + HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit, + Handle = _win32ShareHandle, + }; + } + + var memoryAllocateInfo = new MemoryAllocateInfo + { + PNext = + exportable ? RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? &handleImport : &fdExport : null, + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex( + Api, + _physicalDevice, + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + }; + + Api.AllocateMemory(_device, memoryAllocateInfo, null, + out var imageMemory).ThrowOnError(); + + _imageMemory = imageMemory; + + + MemorySize = memoryRequirements.Size; + + Api.BindImageMemory(_device, InternalHandle.Value, _imageMemory, 0).ThrowOnError(); + var componentMapping = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity); + + AspectFlags = ImageAspectFlags.ImageAspectColorBit; + + var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1); + + var imageViewCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = InternalHandle.Value, + ViewType = ImageViewType.ImageViewType2D, + Format = Format, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + Api + .CreateImageView(_device, imageViewCreateInfo, null, out var imageView) + .ThrowOnError(); + + _imageView = imageView; + + _currentLayout = ImageLayout.Undefined; + + TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); + } + + public int ExportFd() + { + if (!Api.TryGetDeviceExtension(_instance, _device, out var ext)) + throw new InvalidOperationException(); + var info = new MemoryGetFdInfoKHR + { + Memory = _imageMemory, + SType = StructureType.MemoryGetFDInfoKhr, + HandleType = ExternalMemoryHandleTypeFlags.OpaqueFDBit + }; + ext.GetMemoryF(_device, info, out var fd).ThrowOnError(); + return fd; + } + + public IPlatformHandle Export() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + new PlatformHandle(_win32ShareHandle, + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) : + new PlatformHandle(new IntPtr(ExportFd()), + KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + + public ImageTiling Tiling => ImageTiling.Optimal; + + + + internal void TransitionLayout(CommandBuffer commandBuffer, + ImageLayout fromLayout, AccessFlags fromAccessFlags, + ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + { + VulkanMemoryHelper.TransitionLayout(Api, commandBuffer, InternalHandle.Value, + fromLayout, + fromAccessFlags, + destinationLayout, destinationAccessFlags, + MipLevels); + + _currentLayout = destinationLayout; + _currentAccessFlags = destinationAccessFlags; + } + + internal void TransitionLayout(CommandBuffer commandBuffer, + ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + => TransitionLayout(commandBuffer, _currentLayout, _currentAccessFlags, destinationLayout, + destinationAccessFlags); + + + internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + { + var commandBuffer = _commandBufferPool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + TransitionLayout(commandBuffer.InternalHandle, destinationLayout, destinationAccessFlags); + commandBuffer.EndRecording(); + commandBuffer.Submit(); + } + + public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags) + { + TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags); + } + + public unsafe void Dispose() + { + Api.DestroyImageView(_device, _imageView.Value, null); + Api.DestroyImage(_device, InternalHandle.Value, null); + Api.FreeMemory(_device, _imageMemory, null); + + _imageView = default; + InternalHandle = default; + _imageMemory = default; + } + + public void SaveTexture(string path) + { + _vk.GrContext.ResetContext(); + var _image = this; + var imageInfo = new GRVkImageInfo() + { + CurrentQueueFamily = _vk.QueueFamilyIndex, + Format = (uint)_image.Format, + Image = _image.Handle, + ImageLayout = (uint)_image.CurrentLayout, + ImageTiling = (uint)_image.Tiling, + ImageUsageFlags = (uint)_image.UsageFlags, + LevelCount = _image.MipLevels, + SampleCount = 1, + Protected = false, + Alloc = new GRVkAlloc() + { + Memory = _image.MemoryHandle, Flags = 0, Offset = 0, Size = _image.MemorySize + } + }; + + using (var backendTexture = new GRBackendRenderTarget(_image.Size.Width, _image.Size.Height, 1, + imageInfo)) + using (var surface = SKSurface.Create(_vk.GrContext, backendTexture, + GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888, SKColorSpace.CreateSrgb())) + { + using var snap = surface.Snapshot(); + using var encoded = snap.Encode(); + using (var s = File.Create(path)) + encoded.SaveTo(s); + } + } + } diff --git a/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs new file mode 100644 index 0000000000..f6778610dc --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs @@ -0,0 +1,59 @@ +using Silk.NET.Vulkan; + +namespace GpuInterop.VulkanDemo; + +internal static class VulkanMemoryHelper +{ + internal static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } + + internal static unsafe void TransitionLayout( + Vk api, + CommandBuffer commandBuffer, + Image image, + ImageLayout sourceLayout, + AccessFlags sourceAccessMask, + ImageLayout destinationLayout, + AccessFlags destinationAccessMask, + uint mipLevels) + { + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1); + + var barrier = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = sourceAccessMask, + DstAccessMask = destinationAccessMask, + OldLayout = sourceLayout, + NewLayout = destinationLayout, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image, + SubresourceRange = subresourceRange + }; + + api.CmdPipelineBarrier( + commandBuffer, + PipelineStageFlags.PipelineStageAllCommandsBit, + PipelineStageFlags.PipelineStageAllCommandsBit, + 0, + 0, + null, + 0, + null, + 1, + barrier); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs b/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs new file mode 100644 index 0000000000..279c313a27 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs @@ -0,0 +1,58 @@ +using System; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; + +namespace GpuInterop.VulkanDemo; + +class VulkanSemaphorePair : IDisposable +{ + private readonly VulkanContext _resources; + + public unsafe VulkanSemaphorePair(VulkanContext resources, bool exportable) + { + _resources = resources; + + var semaphoreExportInfo = new ExportSemaphoreCreateInfo + { + SType = StructureType.ExportSemaphoreCreateInfo, + HandleTypes = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit + }; + + var semaphoreCreateInfo = new SemaphoreCreateInfo + { + SType = StructureType.SemaphoreCreateInfo, + PNext = exportable ? &semaphoreExportInfo : null + }; + + resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out var semaphore).ThrowOnError(); + ImageAvailableSemaphore = semaphore; + + resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out semaphore).ThrowOnError(); + RenderFinishedSemaphore = semaphore; + } + + public int ExportFd(bool renderFinished) + { + if (!_resources.Api.TryGetDeviceExtension(_resources.Instance, _resources.Device, + out var ext)) + throw new InvalidOperationException(); + var info = new SemaphoreGetFdInfoKHR() + { + SType = StructureType.SemaphoreGetFDInfoKhr, + Semaphore = renderFinished ? RenderFinishedSemaphore : ImageAvailableSemaphore, + HandleType = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit + }; + ext.GetSemaphoreF(_resources.Device, info, out var fd).ThrowOnError(); + return fd; + } + + internal Semaphore ImageAvailableSemaphore { get; } + internal Semaphore RenderFinishedSemaphore { get; } + + public unsafe void Dispose() + { + _resources.Api.DestroySemaphore(_resources.Device, ImageAvailableSemaphore, null); + _resources.Api.DestroySemaphore(_resources.Device, RenderFinishedSemaphore, null); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs new file mode 100644 index 0000000000..325c815ccb --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs @@ -0,0 +1,154 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Vulkan; +using Metsys.Bson; +using Silk.NET.Vulkan; +using SkiaSharp; + +namespace GpuInterop.VulkanDemo; + +class VulkanSwapchain : SwapchainBase +{ + private readonly VulkanContext _vk; + + public VulkanSwapchain(VulkanContext vk, ICompositionGpuInterop interop, CompositionDrawingSurface target) : base(interop, target) + { + _vk = vk; + } + + protected override VulkanSwapchainImage CreateImage(PixelSize size) + { + return new VulkanSwapchainImage(_vk, size, Interop, Target); + } + + public IDisposable BeginDraw(PixelSize size, out VulkanImage image) + { + _vk.Pool.FreeUsedCommandBuffers(); + var rv = BeginDrawCore(size, out var swapchainImage); + image = swapchainImage.Image; + return rv; + } +} + +class VulkanSwapchainImage : ISwapchainImage +{ + private readonly VulkanContext _vk; + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly VulkanImage _image; + private readonly VulkanSemaphorePair _semaphorePair; + private ICompositionImportedGpuSemaphore? _availableSemaphore, _renderCompletedSemaphore; + private ICompositionImportedGpuImage? _importedImage; + private Task? _lastPresent; + public VulkanImage Image => _image; + private bool _initial = true; + + public VulkanSwapchainImage(VulkanContext vk, PixelSize size, ICompositionGpuInterop interop, CompositionDrawingSurface target) + { + _vk = vk; + _interop = interop; + _target = target; + Size = size; + _image = new VulkanImage(vk, (uint)Format.R8G8B8A8Unorm, size, true); + _semaphorePair = new VulkanSemaphorePair(vk, true); + } + + public async ValueTask DisposeAsync() + { + if (LastPresent != null) + await LastPresent; + if (_importedImage != null) + await _importedImage.DisposeAsync(); + if (_availableSemaphore != null) + await _availableSemaphore.DisposeAsync(); + if (_renderCompletedSemaphore != null) + await _renderCompletedSemaphore.DisposeAsync(); + _semaphorePair.Dispose(); + _image.Dispose(); + } + + public PixelSize Size { get; } + + public Task? LastPresent => _lastPresent; + + public void BeginDraw() + { + var buffer = _vk.Pool.CreateCommandBuffer(); + buffer.BeginRecording(); + + _image.TransitionLayout(buffer.InternalHandle, + ImageLayout.Undefined, AccessFlags.None, + ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessColorAttachmentReadBit); + + if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + buffer.Submit(null,null,null, null, new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo + { + AcquireKey = 0, + DeviceMemory = _image.DeviceMemory + }); + else if (_initial) + { + _initial = false; + buffer.Submit(); + } + else + buffer.Submit(new[] { _semaphorePair.ImageAvailableSemaphore }, + new[] + { + PipelineStageFlags.AllGraphicsBit + }); + } + + + + public void Present() + { + var buffer = _vk.Pool.CreateCommandBuffer(); + buffer.BeginRecording(); + _image.TransitionLayout(buffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferWriteBit); + + + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + buffer.Submit(null, null, null, null, + new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo + { + DeviceMemory = _image.DeviceMemory, ReleaseKey = 1 + }); + } + else + buffer.Submit(null, null, new[] { _semaphorePair.RenderFinishedSemaphore }); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _availableSemaphore ??= _interop.ImportSemaphore(new PlatformHandle( + new IntPtr(_semaphorePair.ExportFd(false)), + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)); + + _renderCompletedSemaphore ??= _interop.ImportSemaphore(new PlatformHandle( + new IntPtr(_semaphorePair.ExportFd(true)), + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)); + } + + _importedImage ??= _interop.ImportImage(_image.Export(), + new PlatformGraphicsExternalImageProperties + { + Format = PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm, + Width = Size.Width, + Height = Size.Height, + MemorySize = _image.MemorySize + }); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + _lastPresent = _target.UpdateWithKeyedMutexAsync(_importedImage, 1, 0); + else + _lastPresent = _target.UpdateWithSemaphoresAsync(_importedImage, _renderCompletedSemaphore, _availableSemaphore); + } +} diff --git a/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs new file mode 100644 index 0000000000..5b06651b5f --- /dev/null +++ b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia.Metadata; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Platform; + +[Unstable] +public interface IExternalObjectsRenderInterfaceContextFeature +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle); + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + public byte[]? DeviceUuid { get; } + public byte[]? DeviceLuid { get; } +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedObject : IDisposable +{ + +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedImage : IPlatformRenderInterfaceImportedObject +{ + IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex); + + IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, + IPlatformRenderInterfaceImportedSemaphore signalSemaphore); + + IBitmapImpl SnapshotWithAutomaticSync(); +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedSemaphore : IPlatformRenderInterfaceImportedObject +{ + +} diff --git a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs index b6464dea58..66d4f083c2 100644 --- a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs +++ b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace Avalonia.Platform; @@ -15,4 +16,12 @@ public static class OptionalFeatureProviderExtensions { public static T? TryGetFeature(this IOptionalFeatureProvider provider) where T : class => (T?)provider.TryGetFeature(typeof(T)); -} \ No newline at end of file + + public static bool TryGetFeature(this IOptionalFeatureProvider provider, [MaybeNullWhen(false)] out T rv) + where T : class + { + rv = provider.TryGetFeature(); + return rv != null; + } + +} diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 1828f24aff..89bf047401 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -210,5 +210,10 @@ namespace Avalonia.Platform /// /// An . IRenderTarget CreateRenderTarget(IEnumerable surfaces); + + /// + /// Indicates that the context is no longer usable. This method should be thread-safe + /// + bool IsLost { get; } } } diff --git a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs new file mode 100644 index 0000000000..cad4ab2051 --- /dev/null +++ b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.Platform; + +public struct PlatformGraphicsExternalImageProperties +{ + public int Width { get; set; } + public int Height { get; set; } + public PlatformGraphicsExternalImageFormat Format { get; set; } + public ulong MemorySize { get; set; } + public ulong MemoryOffset { get; set; } + public bool TopLeftOrigin { get; set; } +} + +public enum PlatformGraphicsExternalImageFormat +{ + R8G8B8A8UNorm, + B8G8R8A8UNorm +} + +/// +/// Describes various GPU memory handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalImageHandleTypes +{ + /// + /// An DXGI global shared handle returned by IDXGIResource::GetSharedHandle D3D11_RESOURCE_MISC_SHARED or D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX flag. + /// The handle does not own the reference to the underlying video memory, so the provider should make sure that the resource is valid until + /// the handle has been successfully imported + /// + public const string D3D11TextureGlobalSharedHandle = nameof(D3D11TextureGlobalSharedHandle); + /// + /// A DXGI NT handle returned by IDXGIResource1::CreateSharedHandle for a texture created with D3D11_RESOURCE_MISC_SHARED_NTHANDLE or flag + /// + public const string D3D11TextureNtHandle = nameof(D3D11TextureNtHandle); + /// + /// A POSIX file descriptor that's exported by Vulkan using VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); +} + +/// +/// Describes various GPU semaphore handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalSemaphoreHandleTypes +{ + /// + /// A POSIX file descriptor that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); + + /// + /// A NT handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT or in a compatible way + /// + public const string VulkanOpaqueNtHandle = nameof(VulkanOpaqueNtHandle); + + // A global shared handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT or in a compatible way + public const string VulkanOpaqueKmtHandle = nameof(VulkanOpaqueKmtHandle); + + /// A DXGI NT handle returned by ID3D12Device::CreateSharedHandle or ID3D11Fence::CreateSharedHandle + public const string Direct3D12FenceNtHandle = nameof(Direct3D12FenceNtHandle); +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index b2080aeb87..a2209db09f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -28,7 +28,7 @@ public class CompositingRenderer : IRendererWithCompositor private HashSet _dirty = new(); private HashSet _recalculateChildren = new(); private bool _queuedUpdate; - private Action _update; + private Action _update; private bool _updating; internal CompositionTarget CompositionTarget; @@ -70,7 +70,7 @@ public class CompositingRenderer : IRendererWithCompositor if(_queuedUpdate) return; _queuedUpdate = true; - _compositor.InvokeBeforeNextCommit(_update); + _compositor.RequestCompositionUpdate(_update); } /// @@ -265,7 +265,7 @@ public class CompositingRenderer : IRendererWithCompositor SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); } - private void Update(Task batchCompletion) + private void Update() { if(_updating) return; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs new file mode 100644 index 0000000000..2f7e261250 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public class CompositionDrawingSurface : CompositionSurface +{ + internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server; + internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) + { + } + + /// + /// Updates the surface contents using an imported memory image using a keyed mutex as the means of synchronization + /// + /// GPU image with new surface contents + /// The mutex key to wait for before accessing the image + /// The mutex key to release for after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithKeyedMutexAsync(ICompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithKeyedMutex(img, acquireIndex, releaseIndex)); + } + + /// + /// Updates the surface contents using an imported memory image using a semaphore pair as the means of synchronization + /// + /// GPU image with new surface contents + /// The semaphore to wait for before accessing the image + /// The semaphore to signal after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithSemaphoresAsync(ICompositionImportedGpuImage image, + ICompositionImportedGpuSemaphore waitForSemaphore, + ICompositionImportedGpuSemaphore signalSemaphore) + { + var img = (CompositionImportedGpuImage)image; + var wait = (CompositionImportedGpuSemaphore)waitForSemaphore; + var signal = (CompositionImportedGpuSemaphore)signalSemaphore; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithSemaphores(img, wait, signal)); + } + + /// + /// Updates the surface contents using an unspecified automatic means of synchronization + /// provided by the underlying platform + /// + /// GPU image with new surface contents + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateAsync(ICompositionImportedGpuImage image) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithAutomaticSync(img)); + } + + ~CompositionDrawingSurface() + { + Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs new file mode 100644 index 0000000000..ce728f86a2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; +public interface ICompositionGpuInterop +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + /// + /// Returns the supported ways to synchronize access to the imported GPU image + /// + /// + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// + ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + /// An image that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// + /// A semaphore that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image); + + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + public bool IsLost { get; } + + /// + /// The LUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceLuid { get; set; } + + /// + /// The UUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceUuid { get; set; } +} + +[Flags] +public enum CompositionGpuImportedImageSynchronizationCapabilities +{ + /// + /// Pre-render and after-render semaphores must be provided alongside with the image + /// + Semaphores = 1, + /// + /// Image must be created with D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX or in other compatible way + /// + KeyedMutex = 2, + /// + /// Synchronization and ordering is somehow handled by the underlying platform + /// + Automatic = 4 +} + +/// +/// An imported GPU object that's usable by composition APIs +/// +public interface ICompositionGpuImportedObject : IAsyncDisposable +{ + /// + /// Tracks the import status of the object. Once the task is completed, + /// the user code is allowed to free the resource owner in case when a non-owning + /// sharing handle was used + /// + Task ImportCompeted { get; } + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + bool IsLost { get; } +} + +/// +/// An imported GPU image object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuImage : ICompositionGpuImportedObject +{ + +} + +/// +/// An imported GPU semaphore object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuSemaphore : ICompositionGpuImportedObject +{ + +} + +/// +/// An GPU object descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextObject : IDisposable +{ +} + +/// +/// An GPU image descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextImage : IDisposable +{ +} + +/// +/// An GPU semaphore descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextSemaphore : IDisposable +{ +} + diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs new file mode 100644 index 0000000000..1643ec6e8d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionInterop : ICompositionGpuInterop +{ + private readonly Compositor _compositor; + private readonly IPlatformRenderInterfaceContext _context; + private readonly IExternalObjectsRenderInterfaceContextFeature _externalObjects; + + + public CompositionInterop( + Compositor compositor, + IExternalObjectsRenderInterfaceContextFeature externalObjects) + { + _compositor = compositor; + _context = compositor.Server.RenderInterface.Value; + DeviceLuid = externalObjects.DeviceLuid; + DeviceUuid = externalObjects.DeviceUuid; + _externalObjects = externalObjects; + } + + public IReadOnlyList SupportedImageHandleTypes => _externalObjects.SupportedImageHandleTypes; + public IReadOnlyList SupportedSemaphoreTypes => _externalObjects.SupportedSemaphoreTypes; + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + => _externalObjects.GetSynchronizationCapabilities(imageHandleType); + + public ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties) + => new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(handle, properties)); + + public ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image) + { + return new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(image)); + } + + public ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle) + => new CompositionImportedGpuSemaphore(handle, _compositor, _context, _externalObjects); + + public ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image) + { + throw new System.NotSupportedException(); + } + + public bool IsLost { get; } + public byte[]? DeviceLuid { get; set; } + public byte[]? DeviceUuid { get; set; } +} + +abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject +{ + protected Compositor Compositor { get; } + public IPlatformRenderInterfaceContext Context { get; } + public IExternalObjectsRenderInterfaceContextFeature Feature { get; } + + public CompositionGpuImportedObjectBase(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) + { + Compositor = compositor; + Context = context; + Feature = feature; + + ImportCompeted = Compositor.InvokeServerJobAsync(Import); + } + + protected abstract void Import(); + public abstract void Dispose(); + + public Task ImportCompeted { get; } + public bool IsLost => Context.IsLost; + + public ValueTask DisposeAsync() => new(Compositor.InvokeServerJobAsync(() => + { + if (ImportCompeted.Status == TaskStatus.RanToCompletion) + Dispose(); + })); +} + +class CompositionImportedGpuImage : CompositionGpuImportedObjectBase, ICompositionImportedGpuImage +{ + private readonly Func _importer; + private IPlatformRenderInterfaceImportedImage? _image; + + public CompositionImportedGpuImage(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature, + Func importer): base(compositor, context, feature) + { + _importer = importer; + } + + protected override void Import() + { + using (Compositor.Server.RenderInterface.EnsureCurrent()) + { + // The original context was lost and the new one might have different capabilities + if (Context != Compositor.Server.RenderInterface.Value) + throw new PlatformGraphicsContextLostException(); + _image = _importer(); + } + } + + public IPlatformRenderInterfaceImportedImage Image => + _image ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuImage)); + + public bool IsUsable => _image != null && Compositor.Server.RenderInterface.Value == Context; + + public override void Dispose() + { + _image?.Dispose(); + _image = null!; + } +} + +class CompositionImportedGpuSemaphore : CompositionGpuImportedObjectBase, ICompositionImportedGpuSemaphore +{ + private readonly IPlatformHandle _handle; + private IPlatformRenderInterfaceImportedSemaphore? _semaphore; + + public CompositionImportedGpuSemaphore(IPlatformHandle handle, + Compositor compositor, IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) : base(compositor, context, feature) + { + _handle = handle; + } + + public IPlatformRenderInterfaceImportedSemaphore Semaphore => + _semaphore ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuSemaphore)); + + + public bool IsUsable => _semaphore != null && Compositor.Server.RenderInterface.Value == Context; + + protected override void Import() + { + _semaphore = Feature.ImportSemaphore(_handle); + } + + public override void Dispose() + { + _semaphore?.Dispose(); + _semaphore = null; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs new file mode 100644 index 0000000000..0edd8ac732 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs @@ -0,0 +1,10 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public class CompositionSurface : CompositionObject +{ + internal CompositionSurface(Compositor compositor, ServerObject server) : base(compositor, server) + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 2b1b3f461f..6dba18704f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -35,4 +35,8 @@ public partial class Compositor new(this, new ServerCompositionSolidColorVisual(Server)); public CompositionCustomVisual CreateCustomVisual(CompositionCustomVisualHandler handler) => new(this, handler); -} \ No newline at end of file + + public CompositionSurfaceVisual CreateSurfaceVisual() => new(this, new ServerCompositionSurfaceVisual(_server)); + + public CompositionDrawingSurface CreateDrawingSurface() => new(this); +} diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 7fc5487171..bdcbe65403 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.Composition private BatchStreamObjectPool _batchObjectPool = new(); private BatchStreamMemoryPool _batchMemoryPool = new(); private List _objectsForSerialization = new(); - private Queue> _invokeBeforeCommit = new(); + private Queue _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new(); internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); @@ -77,16 +77,30 @@ namespace Avalonia.Rendering.Composition return _nextCommit.Task; } - + internal Task Commit() + { + try + { + return CommitCore(); + } + finally + { + if (_invokeBeforeCommitWrite.Count > 0) + RequestCommitAsync(); + } + } + + Task CommitCore() { Dispatcher.UIThread.VerifyAccess(); using var noPump = NonPumpingLockHelper.Use(); _nextCommit ??= new TaskCompletionSource(); - while (_invokeBeforeCommit.Count > 0) - _invokeBeforeCommit.Dequeue()(_nextCommit.Task); + (_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead); + while (_invokeBeforeCommitRead.Count > 0) + _invokeBeforeCommitRead.Dequeue()(); var batch = new Batch(_nextCommit); @@ -109,6 +123,7 @@ namespace Avalonia.Rendering.Composition writer.WriteObject(job); writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); } + _pendingServerCompositorJobs.Clear(); } batch.CommittedAt = Server.Clock.Elapsed; @@ -138,34 +153,73 @@ namespace Avalonia.Rendering.Composition RequestCommitAsync(); } - internal void InvokeBeforeNextCommit(Action action) + /// + /// Enqueues a callback to be called before the next scheduled commit. + /// If there is no scheduled commit it automatically schedules one + /// This is useful for updating your composition tree objects after binding + /// and layout passes have completed + /// + public void RequestCompositionUpdate(Action action) { Dispatcher.UIThread.VerifyAccess(); - _invokeBeforeCommit.Enqueue(action); + _invokeBeforeCommitWrite.Enqueue(action); RequestCommitAsync(); } - /// - /// Attempts to query for a feature from the platform render interface - /// - public ValueTask TryGetRenderInterfaceFeature(Type featureType) + internal void PostServerJob(Action job) { - var tcs = new TaskCompletionSource(); - _pendingServerCompositorJobs.Add(() => + Dispatcher.UIThread.VerifyAccess(); + _pendingServerCompositorJobs.Add(job); + RequestCommitAsync(); + } + + internal Task InvokeServerJobAsync(Action job) => + InvokeServerJobAsync(() => + { + job(); + return null; + }); + + internal Task InvokeServerJobAsync(Func job) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + PostServerJob(() => { try { - using (Server.RenderInterface.EnsureCurrent()) - { - tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType)); - } + tcs.SetResult(job()); } catch (Exception e) { tcs.TrySetException(e); } }); - return new ValueTask(tcs.Task); + return tcs.Task; } + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + return Server.RenderInterface.Value.TryGetFeature(featureType); + } + })); + + public ValueTask TryGetCompositionGpuInterop() => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + var feature = Server.RenderInterface.Value + .TryGetFeature(); + if (feature == null) + return null; + return new CompositionInterop(this, feature); + } + })); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs new file mode 100644 index 0000000000..9c6c78c1ad --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.ExceptionServices; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisposable +{ + private IRef? _bitmap; + private IPlatformRenderInterfaceContext? _createdWithContext; + public override IRef? Bitmap + { + get + { + // Failsafe to avoid consuming an image imported with a different context + if (Compositor.RenderInterface.Value != _createdWithContext) + return null; + return _bitmap; + } + } + + public ServerCompositionDrawingSurface(ServerCompositor compositor) : base(compositor) + { + } + + void PerformSanityChecks(CompositionImportedGpuImage image) + { + // Failsafe to avoid consuming an image imported with a different context + if (!image.IsUsable) + throw new PlatformGraphicsContextLostException(); + + // This should never happen, but check for it anyway to avoid a deadlock + if (!image.ImportCompeted.IsCompleted) + throw new InvalidOperationException("The import operation is not completed yet"); + + // Rethrow the import here exception + if (image.ImportCompeted.IsFaulted) + image.ImportCompeted.GetAwaiter().GetResult(); + } + + void Update(IBitmapImpl newImage, IPlatformRenderInterfaceContext context) + { + _bitmap?.Dispose(); + _bitmap = RefCountable.Create(newImage); + _createdWithContext = context; + Changed?.Invoke(); + } + + public void UpdateWithAutomaticSync(CompositionImportedGpuImage image) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithAutomaticSync(), image.Context); + } + + public void UpdateWithKeyedMutex(CompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithKeyedMutex(acquireIndex, releaseIndex), image.Context); + } + + public void UpdateWithSemaphores(CompositionImportedGpuImage image, CompositionImportedGpuSemaphore wait, CompositionImportedGpuSemaphore signal) + { + PerformSanityChecks(image); + if (!wait.IsUsable || !signal.IsUsable) + throw new PlatformGraphicsContextLostException(); + Update(image.Image.SnapshotWithSemaphores(wait.Semaphore, signal.Semaphore), image.Context); + } + + public void Dispose() + { + _bitmap?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs index 32a99fa187..88f10ba642 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -1,11 +1,18 @@ // Special license applies License.md +using System; +using Avalonia.Platform; +using Avalonia.Utilities; + namespace Avalonia.Rendering.Composition.Server { - internal abstract class ServerCompositionSurface : ServerObject + internal abstract partial class ServerCompositionSurface : ServerObject { protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) { } + + public abstract IRef? Bitmap { get; } + public Action? Changed { get; set; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs new file mode 100644 index 0000000000..c75ae8e631 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -0,0 +1,48 @@ +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionSurfaceVisual +{ + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + if (Surface == null) + return; + if (Surface.Bitmap == null) + return; + var bmp = Surface.Bitmap.Item; + + //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI + canvas.DrawBitmap(Surface.Bitmap, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( + new Size(Size.X, Size.Y))); + } + + + private void OnSurfaceInvalidated() => ValuesInvalidated(); + + protected override void OnAttachedToRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + base.OnAttachedToRoot(target); + } + + protected override void OnDetachedFromRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + base.OnDetachedFromRoot(target); + } + + partial void OnSurfaceChanged() + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + } + + partial void OnSurfaceChanging() + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e7405995f5..0492997200 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -22,7 +22,8 @@ namespace Avalonia.Rendering.Composition.Server { private readonly IRenderLoop _renderLoop; - private readonly Queue _batches = new Queue(); + private readonly Queue _batches = new Queue(); + private readonly Queue _receivedJobQueue = new(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } @@ -75,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server var readObject = stream.ReadObject(); if (readObject == RenderThreadJobsStartMarker) { - ReadAndExecuteJobs(stream); + ReadServerJobs(stream); continue; } @@ -97,21 +98,24 @@ namespace Avalonia.Rendering.Composition.Server } } - void ReadAndExecuteJobs(BatchStreamReader reader) + void ReadServerJobs(BatchStreamReader reader) { object? readObject; while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) - { - var job = (Action)readObject!; + _receivedJobQueue.Enqueue((Action)readObject!); + } + + void ExecuteServerJobs() + { + while(_receivedJobQueue.Count > 0) try { - job(); + _receivedJobQueue.Dequeue()(); } catch { // Ignore } - } } void CompletePendingBatches() @@ -160,6 +164,7 @@ namespace Avalonia.Rendering.Composition.Server try { RenderInterface.EnsureValidBackendContext(); + ExecuteServerJobs(); foreach (var t in _activeTargets) t.Render(); } diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 198b36564a..51a4ca1bf3 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -50,6 +50,8 @@ public class PlatformRenderInterfaceContextManager } } + internal IPlatformGraphicsContext? GpuContext => _gpuContext?.Value; + public IDisposable EnsureCurrent() { EnsureValidBackendContext(); diff --git a/src/Avalonia.Base/Rendering/SwapchainBase.cs b/src/Avalonia.Base/Rendering/SwapchainBase.cs new file mode 100644 index 0000000000..ccfb704f00 --- /dev/null +++ b/src/Avalonia.Base/Rendering/SwapchainBase.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Rendering; + +/// +/// A helper class for composition-backed swapchains, should not be a public API yet +/// +abstract class SwapchainBase : IAsyncDisposable where TImage : class, ISwapchainImage +{ + protected ICompositionGpuInterop Interop { get; } + protected CompositionDrawingSurface Target { get; } + private List _pendingImages = new(); + + public SwapchainBase(ICompositionGpuInterop interop, CompositionDrawingSurface target) + { + Interop = interop; + Target = target; + } + + static bool IsBroken(TImage image) => image.LastPresent?.IsFaulted == true; + static bool IsReady(TImage image) => image.LastPresent == null || image.LastPresent.Status == TaskStatus.RanToCompletion; + + TImage? CleanupAndFindNextImage(PixelSize size) + { + TImage? firstFound = null; + var foundMultiple = false; + + for (var c = _pendingImages.Count - 1; c > -1; c--) + { + var image = _pendingImages[c]; + var ready = IsReady(image); + var matches = image.Size == size; + if (IsBroken(image) || (!matches && ready)) + { + image.DisposeAsync(); + _pendingImages.RemoveAt(c); + } + + if (matches && ready) + { + if (firstFound == null) + firstFound = image; + else + foundMultiple = true; + } + + } + + // We are making sure that there was at least one image of the same size in flight + // Otherwise we might encounter UI thread lockups + return foundMultiple ? firstFound : null; + } + + protected abstract TImage CreateImage(PixelSize size); + + protected IDisposable BeginDrawCore(PixelSize size, out TImage image) + { + var img = CleanupAndFindNextImage(size) ?? CreateImage(size); + + img.BeginDraw(); + _pendingImages.Remove(img); + image = img; + return Disposable.Create(() => + { + img.Present(); + _pendingImages.Add(img); + }); + } + + public async ValueTask DisposeAsync() + { + foreach (var img in _pendingImages) + await img.DisposeAsync(); + } +} + + +interface ISwapchainImage : IAsyncDisposable +{ + PixelSize Size { get; } + Task? LastPresent { get; } + void BeginDraw(); + void Present(); +} diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 6dfcb2e74d..a0dbf238dc 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -7,6 +7,8 @@ + + @@ -30,6 +32,9 @@ + + + @@ -46,4 +51,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9bb4b34976..a12d876e64 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -49,6 +49,7 @@ namespace Avalonia.Headless public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + public bool IsLost => false; public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 9b38c150fa..73493c69c0 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -15,7 +15,7 @@ - - + + diff --git a/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs new file mode 100644 index 0000000000..3db0aafd7b --- /dev/null +++ b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL.Controls; + +internal class CompositionOpenGlSwapchain : SwapchainBase +{ + private readonly IGlContext _context; + private readonly IGlContextExternalObjectsFeature? _externalObjectsFeature; + private readonly IOpenGlTextureSharingRenderInterfaceContextFeature? _sharingFeature; + + public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, + IOpenGlTextureSharingRenderInterfaceContextFeature sharingFeature + ) : base(interop, target) + { + _context = context; + _sharingFeature = sharingFeature; + } + + public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, + IGlContextExternalObjectsFeature externalObjectsFeature) : base(interop, target) + { + _context = context; + _externalObjectsFeature = externalObjectsFeature; + } + + + + protected override IGlSwapchainImage CreateImage(PixelSize size) + { + if (_sharingFeature != null) + return new CompositionOpenGlSwapChainImage(_context, _sharingFeature, size, Interop, Target); + return new DxgiMutexOpenGlSwapChainImage(Interop, Target, _externalObjectsFeature!, size); + } + + public IDisposable BeginDraw(PixelSize size, out IGlTexture texture) + { + var rv = BeginDrawCore(size, out var tex); + texture = tex; + return rv; + } +} + +internal interface IGlTexture +{ + int TextureId { get; } + int InternalFormat { get; } + PixelSize Size { get; } +} + + +interface IGlSwapchainImage : ISwapchainImage, IGlTexture +{ + +} +internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage +{ + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _surface; + private readonly IGlExportableExternalImageTexture _texture; + private Task? _lastPresent; + private ICompositionImportedGpuImage? _imported; + + public DxgiMutexOpenGlSwapChainImage(ICompositionGpuInterop interop, CompositionDrawingSurface surface, + IGlContextExternalObjectsFeature externalObjects, PixelSize size) + { + _interop = interop; + _surface = surface; + _texture = externalObjects.CreateImage(KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle, + size, PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm); + } + public async ValueTask DisposeAsync() + { + // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture + // before destroying it + if (_imported != null) + { + // No need to wait for import / LastPresent since calls are serialized on the compositor side anyway + try + { + await _imported.DisposeAsync(); + } + catch + { + // Ignore + } + } + _texture.Dispose(); + } + + public int TextureId => _texture.TextureId; + public int InternalFormat => _texture.InternalFormat; + public PixelSize Size => new(_texture.Properties.Width, _texture.Properties.Height); + public Task LastPresent => _lastPresent; + public void BeginDraw() => _texture.AcquireKeyedMutex(0); + + public void Present() + { + _texture.ReleaseKeyedMutex(1); + _imported ??= _interop.ImportImage(_texture.GetHandle(), _texture.Properties); + _lastPresent = _surface.UpdateWithKeyedMutexAsync(_imported, 1, 0); + } +} + +internal class CompositionOpenGlSwapChainImage : IGlSwapchainImage +{ + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly ICompositionImportableOpenGlSharedTexture _texture; + private ICompositionImportedGpuImage? _imported; + + public CompositionOpenGlSwapChainImage( + IGlContext context, + IOpenGlTextureSharingRenderInterfaceContextFeature sharingFeature, + PixelSize size, + ICompositionGpuInterop interop, + CompositionDrawingSurface target) + { + _interop = interop; + _target = target; + _texture = sharingFeature.CreateSharedTextureForComposition(context, size); + } + + + public async ValueTask DisposeAsync() + { + // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture + // before destroying it + if (_imported != null) + { + // No need to wait for import / LastPresent since calls are serialized on the compositor side anyway + try + { + await _imported.DisposeAsync(); + } + catch + { + // Ignore + } + } + + _texture.Dispose(); + } + + public int TextureId => _texture.TextureId; + public int InternalFormat => _texture.InternalFormat; + public PixelSize Size => _texture.Size; + public Task? LastPresent { get; private set; } + public void BeginDraw() + { + // No-op for texture sharing + } + + public void Present() + { + _imported ??= _interop.ImportImage(_texture); + LastPresent = _target.UpdateAsync(_imported); + } +} diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index c62a3d8d2f..4d0663a12b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -1,120 +1,53 @@ using System; +using System.Numerics; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.OpenGL.Imaging; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.VisualTree; -using static Avalonia.OpenGL.GlConsts; - +using Avalonia.Platform; namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { - private IGlContext _context; - private int _fb, _depthBuffer; - private OpenGlBitmap _bitmap; - private IOpenGlBitmapAttachment _attachment; - private PixelSize _depthBufferSize; - + private CompositionSurfaceVisual _visual; + private Action _update; + private bool _updateQueued; private Task _initialization; - private IOpenGlTextureSharingRenderInterfaceContextFeature _feature; - - protected GlVersion GlVersion { get; private set; } - public sealed override void Render(DrawingContext context) - { - if(!EnsureInitialized()) - return; - - using (_context.MakeCurrent()) - { - _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); - EnsureTextureAttachment(); - EnsureDepthBufferAttachment(_context.GlInterface); - if(!CheckFramebufferStatus(_context.GlInterface)) - return; - - OnOpenGlRender(_context.GlInterface, _fb); - _attachment.Present(); - } - - context.DrawImage(_bitmap, new Rect(_bitmap.Size), new Rect(Bounds.Size)); - base.Render(context); - } + private OpenGlControlBaseResources? _resources; + private Compositor? _compositor; + protected GlVersion GlVersion => _resources?.Context.Version ?? default; - void EnsureTextureAttachment() + public OpenGlControlBase() { - _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); - if (_bitmap == null || _attachment == null || _bitmap.PixelSize != GetPixelSize()) - { - _attachment?.Dispose(); - _attachment = null; - _bitmap?.Dispose(); - _bitmap = null; - _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); - _attachment = _bitmap.CreateFramebufferAttachment(_context); - } - } - - void EnsureDepthBufferAttachment(GlInterface gl) - { - var size = GetPixelSize(); - if (size == _depthBufferSize && _depthBuffer != 0) - return; - - gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); - if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); - - _depthBuffer = gl.GenRenderbuffer(); - gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); - gl.RenderbufferStorage(GL_RENDERBUFFER, - GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, - size.Width, size.Height); - gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); - gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + _update = Update; } void DoCleanup() { - if (_context != null) + if (_initialization is { Status: TaskStatus.RanToCompletion } && _resources != null) { - using (_context.MakeCurrent()) + try { - var gl = _context.GlInterface; - gl.ActiveTexture(GL_TEXTURE0); - gl.BindTexture(GL_TEXTURE_2D, 0); - gl.BindFramebuffer(GL_FRAMEBUFFER, 0); - if (_fb != 0) - gl.DeleteFramebuffer(_fb); - _fb = 0; - if (_depthBuffer != 0) - gl.DeleteRenderbuffer(_depthBuffer); - _depthBuffer = 0; - _attachment?.Dispose(); - _attachment = null; - _bitmap?.Dispose(); - _bitmap = null; - - try - { - if (_initialization is { Status: TaskStatus.RanToCompletion, Result: true }) - { - OnOpenGlDeinit(_context.GlInterface, _fb); - _initialization = null; - } - } - finally + using (_resources.Context.EnsureCurrent()) { - _context.Dispose(); - _context = null; + OnOpenGlDeinit(_resources.Context.GlInterface); } } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to free user OpenGL resources: {exception}", e); + } } - _fb = _depthBuffer = 0; - _attachment = null; - _bitmap = null; - _feature = null; + ElementComposition.SetElementChildVisual(this, null); + _visual = null; + + _resources?.DisposeAsync(); + _resources = null; + _initialization = null; } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -123,95 +56,75 @@ namespace Avalonia.OpenGL.Controls base.OnDetachedFromVisualTree(e); } - private bool EnsureInitializedCore(IOpenGlTextureSharingRenderInterfaceContextFeature feature) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - try - { - _context = feature.CreateSharedContext(); - _feature = feature; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); - return false; - } + base.OnAttachedToVisualTree(e); + _compositor = (this.GetVisualRoot()?.Renderer as IRendererWithCompositor)?.Compositor; + RequestNextFrameRendering(); + } - if (_context == null) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context."); - return false; - } + private bool EnsureInitializedCore( + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature contextSharingFeature) + { + var surface = _compositor.CreateDrawingSurface(); - GlVersion = _context.Version; + IGlContext ctx = null; + var contextFactory = AvaloniaLocator.Current.GetService(); try { - _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); - if (!_bitmap.SupportsContext(_context)) + if (contextSharingFeature?.CanCreateSharedContext == true) + _resources = OpenGlControlBaseResources.TryCreate(surface, interop, contextSharingFeature); + + if(_resources == null) + { + ctx = contextFactory.CreateContext(null); + if (ctx.TryGetFeature(out var externalObjects)) + _resources = OpenGlControlBaseResources.TryCreate(ctx, surface, interop, externalObjects); + else + ctx.Dispose(); + } + + if(_resources == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlBitmap: OpenGL context is not compatible"); + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing and shared memory"); return false; } } catch (Exception e) { - _context.Dispose(); - _context = null; Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlBitmap: {exception}", e); + "Unable to initialize OpenGL: {exception}", e); + ctx?.Dispose(); return false; } + + _visual = _compositor.CreateSurfaceVisual(); + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + _visual.Surface = _resources.Surface; + ElementComposition.SetElementChildVisual(this, _visual); + using (_resources.Context.MakeCurrent()) + OnOpenGlInit(_resources.Context.GlInterface); + return true; - using (_context.MakeCurrent()) - { - try - { - _depthBufferSize = GetPixelSize(); - var gl = _context.GlInterface; - _fb = gl.GenFramebuffer(); - gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - - EnsureDepthBufferAttachment(gl); - EnsureTextureAttachment(); - - return CheckFramebufferStatus(gl); - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {exception}", e); - return false; - } - } } - - private static bool CheckFramebufferStatus(GlInterface gl) + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) + if (_visual != null && change.Property == BoundsProperty) { - int code; - while ((code = gl.GetError()) != 0) - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {code}", code); - return false; + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + RequestNextFrameRendering(); } - return true; + base.OnPropertyChanged(change); } void ContextLost() { - _context = null; - _feature = null; _initialization = null; - _attachment = null; - _bitmap = null; - _fb = 0; - _depthBuffer = 0; - _depthBufferSize = default; + _resources?.DisposeAsync(); OnOpenGlLost(); } @@ -228,59 +141,106 @@ namespace Avalonia.OpenGL.Controls if (_initialization is { IsCompleted: false }) return false; - if (_context.IsLost) + if (_resources!.Context.IsLost) ContextLost(); else return true; } _initialization = InitializeAsync(); + + async void ContinueOnInitialization() + { + try + { + await _initialization; + RequestNextFrameRendering(); + } + catch + { + // + } + } + ContinueOnInitialization(); return false; } + + private void Update() + { + _updateQueued = false; + if (VisualRoot == null) + return; + if(!EnsureInitialized()) + return; + using (_resources.BeginDraw(GetPixelSize())) + OnOpenGlRender(_resources.Context.GlInterface, _resources.Fbo); + } + private async Task InitializeAsync() { + if (_compositor == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to obtain Compositor instance"); + return false; + } + + var gpuInteropTask = _compositor.TryGetCompositionGpuInterop(); + var contextSharingFeature = (IOpenGlTextureSharingRenderInterfaceContextFeature) - await this.GetVisualRoot()!.Renderer.TryGetRenderInterfaceFeature( + await _compositor.TryGetRenderInterfaceFeature( typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); + var interop = await gpuInteropTask; - if (contextSharingFeature == null || !contextSharingFeature.CanCreateSharedContext) + if (interop == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); + "Compositor backend doesn't support GPU interop"); return false; } - if (!EnsureInitializedCore(contextSharingFeature)) + if (!EnsureInitializedCore(interop, contextSharingFeature)) { DoCleanup(); return false; } - using (_context.MakeCurrent()) - OnOpenGlInit(_context.GlInterface, _fb); - - InvalidateVisual(); + using (_resources!.Context.MakeCurrent()) + OnOpenGlInit(_resources.Context.GlInterface); return true; } + + [Obsolete("Use RequestNextFrameRendering()")] + // ReSharper disable once MemberCanBeProtected.Global + public new void InvalidateVisual() => RequestNextFrameRendering(); + public void RequestNextFrameRendering() + { + if ((_initialization == null || _initialization is { Status: TaskStatus.RanToCompletion }) && + !_updateQueued) + { + _updateQueued = true; + _compositor?.RequestCompositionUpdate(_update); + } + } + private PixelSize GetPixelSize() { - var scaling = VisualRoot.RenderScaling; + var scaling = VisualRoot!.RenderScaling; return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), Math.Max(1, (int)(Bounds.Height * scaling))); } - - - protected virtual void OnOpenGlInit(GlInterface gl, int fb) + + protected virtual void OnOpenGlInit(GlInterface gl) { } - protected virtual void OnOpenGlDeinit(GlInterface gl, int fb) + protected virtual void OnOpenGlDeinit(GlInterface gl) { } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs new file mode 100644 index 0000000000..81270bb642 --- /dev/null +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs @@ -0,0 +1,170 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using static Avalonia.OpenGL.GlConsts; +namespace Avalonia.OpenGL.Controls; + +internal class OpenGlControlBaseResources : IAsyncDisposable +{ + private int _depthBuffer; + public int Fbo { get; private set; } + private PixelSize _depthBufferSize; + public CompositionDrawingSurface Surface { get; } + public CompositionOpenGlSwapchain _swapchain; + public IGlContext Context { get; private set; } + + public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface, + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature feature) + { + IGlContext context; + try + { + context = feature.CreateSharedContext(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); + return null; + } + + if (context == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context."); + return null; + } + + return new OpenGlControlBaseResources(context, surface, interop, feature, null); + } + + public static OpenGlControlBaseResources? TryCreate(IGlContext context, CompositionDrawingSurface surface, + ICompositionGpuInterop interop, IGlContextExternalObjectsFeature externalObjects) + { + if (!interop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) + || !externalObjects.SupportedExportableExternalImageTypes.Contains( + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle)) + return null; + return new OpenGlControlBaseResources(context, surface, interop, null, externalObjects); + } + + public OpenGlControlBaseResources(IGlContext context, + CompositionDrawingSurface surface, + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature? feature, + IGlContextExternalObjectsFeature? externalObjects + ) + { + Context = context; + Surface = surface; + using (context.MakeCurrent()) + Fbo = context.GlInterface.GenFramebuffer(); + _swapchain = + feature != null ? + new CompositionOpenGlSwapchain(context, interop, Surface, feature) : + new CompositionOpenGlSwapchain(context, interop, Surface, externalObjects); + } + + void UpdateDepthRenderbuffer(PixelSize size) + { + if (size == _depthBufferSize && _depthBuffer != 0) + return; + + var gl = Context.GlInterface; + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); + if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); + + _depthBuffer = gl.GenRenderbuffer(); + gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); + gl.RenderbufferStorage(GL_RENDERBUFFER, + Context.Version.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, + size.Width, size.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + _depthBufferSize = size; + } + + public IDisposable BeginDraw(PixelSize size) + { + var restoreContext = Context.EnsureCurrent(); + IDisposable? imagePresent = null; + var success = false; + try + { + var gl = Context.GlInterface; + Context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, Fbo); + UpdateDepthRenderbuffer(size); + + imagePresent = _swapchain.BeginDraw(size, out var texture); + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.TextureId, 0); + + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) + { + int code = gl.GetError(); + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to configure OpenGL FBO: {code}", code); + throw OpenGlException.GetFormattedException("Unable to configure OpenGL FBO", code); + } + + success = true; + return Disposable.Create(() => + { + try + { + Context.GlInterface.Flush(); + imagePresent.Dispose(); + } + finally + { + restoreContext.Dispose(); + } + }); + } + finally + { + if (!success) + { + imagePresent?.Dispose(); + restoreContext.Dispose(); + } + } + } + + public async ValueTask DisposeAsync() + { + if (Context is { IsLost: false }) + { + try + { + using (Context.MakeCurrent()) + { + var gl = Context.GlInterface; + if (Fbo != 0) + gl.DeleteFramebuffer(Fbo); + Fbo = 0; + if (_depthBuffer != 0) + gl.DeleteRenderbuffer(_depthBuffer); + _depthBuffer = 0; + } + + } + catch + { + // + } + + Surface.Dispose(); + await _swapchain.DisposeAsync(); + + Context = null!; + } + } +} diff --git a/src/Avalonia.OpenGL/Egl/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs index 428d11857f..aff56ecfe0 100644 --- a/src/Avalonia.OpenGL/Egl/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -64,7 +64,7 @@ namespace Avalonia.OpenGL.Egl public const int EGL_WIDTH = 0x3057; public const int EGL_WINDOW_BIT = 0x0004; -// public const int EGL_BACK_BUFFER = 0x3084; + public const int EGL_BACK_BUFFER = 0x3084; // public const int EGL_BIND_TO_TEXTURE_RGB = 0x3039; // public const int EGL_BIND_TO_TEXTURE_RGBA = 0x303A; public const int EGL_CONTEXT_LOST = 0x300E; @@ -73,11 +73,11 @@ namespace Avalonia.OpenGL.Egl // public const int EGL_MIPMAP_TEXTURE = 0x3082; // public const int EGL_MIPMAP_LEVEL = 0x3083; // public const int EGL_NO_TEXTURE = 0x305C; -// public const int EGL_TEXTURE_2D = 0x305F; -// public const int EGL_TEXTURE_FORMAT = 0x3080; + public const int EGL_TEXTURE_2D = 0x305F; + public const int EGL_TEXTURE_FORMAT = 0x3080; // public const int EGL_TEXTURE_RGB = 0x305D; -// public const int EGL_TEXTURE_RGBA = 0x305E; -// public const int EGL_TEXTURE_TARGET = 0x3081; + public const int EGL_TEXTURE_RGBA = 0x305E; + public const int EGL_TEXTURE_TARGET = 0x3081; // public const int EGL_ALPHA_FORMAT = 0x3088; // public const int EGL_ALPHA_FORMAT_NONPRE = 0x308B; @@ -216,5 +216,7 @@ namespace Avalonia.OpenGL.Egl public const int EGL_TEXTURE_OFFSET_Y_ANGLE = 0x3491; public const int EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE = 0x33A6; + + public const int EGL_TEXTURE_INTERNAL_FORMAT_ANGLE = 0x345D; } } diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index 4d75a776c3..425206e3c4 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Disposables; using System.Threading; using Avalonia.Platform; @@ -19,21 +20,24 @@ namespace Avalonia.OpenGL.Egl private readonly object _lock; internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, - GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, Dictionary features) + GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, + Dictionary> features) { _disp = display; _egl = egl; _sharedWith = sharedWith; _context = ctx; _disposeCallback = disposeCallback; - _features = features; OffscreenSurface = offscreenSurface; Version = version; SampleCount = sampleCount; StencilSize = stencilSize; _lock = display.ContextSharedSyncRoot ?? new object(); using (MakeCurrent()) + { GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress); + _features = features.ToDictionary(x => x.Key, x => x.Value(this)); + } } public IntPtr Context => @@ -155,6 +159,14 @@ namespace Avalonia.OpenGL.Egl { if(_context == IntPtr.Zero) return; + + foreach(var f in _features.ToList()) + if (f.Value is IDisposable d) + { + d.Dispose(); + _features.Remove(f.Key); + } + _egl.DestroyContext(_disp.Handle, Context); OffscreenSurface?.Dispose(); _context = IntPtr.Zero; diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index eea2587587..e4f0b2826d 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -93,7 +93,7 @@ namespace Avalonia.OpenGL.Egl var rv = new EglContext(this, _egl, share, ctx, offscreenSurface, _config.Version, _config.SampleCount, _config.StencilSize, - options.DisposeCallback, options.ExtraFeatures); + options.DisposeCallback, options.ExtraFeatures ?? new()); _contexts.Add(rv); return rv; } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs index 5648645c54..906a533e2d 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -19,7 +19,7 @@ public class EglContextOptions public EglContext ShareWith { get; set; } public EglSurface OffscreenSurface { get; set; } public Action DisposeCallback { get; set; } - public Dictionary ExtraFeatures { get; set; } + public Dictionary> ExtraFeatures { get; set; } } public class EglDisplayCreationOptions : EglDisplayOptions diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index a913c05996..67fd172e8e 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -98,6 +98,9 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglCreateWindowSurface")] public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); + [GetProcAddress("eglBindTexImage")] + public partial int BindTexImage(IntPtr display, IntPtr surface, int buffer); + [GetProcAddress("eglGetConfigAttrib")] public partial bool GetConfigAttrib(IntPtr display, IntPtr config, int attr, out int rv); diff --git a/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs new file mode 100644 index 0000000000..02d152ff61 --- /dev/null +++ b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.SourceGenerator; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.OpenGL.Features; + +unsafe partial class ExternalObjectsInterface +{ + public ExternalObjectsInterface(Func getProcAddress) + { + Initialize(getProcAddress); + } + + [GetProcAddress("glImportMemoryFdEXT", true)] + public partial void ImportMemoryFdEXT(uint memory, ulong size, int handleType, int fd); + + [GetProcAddress("glImportSemaphoreFdEXT", true)] + public partial void ImportSemaphoreFdEXT(uint semaphore, + int handleType, + int fd); + + [GetProcAddress("glCreateMemoryObjectsEXT")] + public partial void CreateMemoryObjectsEXT(int n, out uint memoryObjects); + + [GetProcAddress("glDeleteMemoryObjectsEXT")] + public partial void DeleteMemoryObjectsEXT(int n, ref uint objects); + + [GetProcAddress("glTexStorageMem2DEXT")] + public partial void TexStorageMem2DEXT(int target, int levels, int internalFormat, int width, int height, + uint memory, ulong offset); + + [GetProcAddress("glGenSemaphoresEXT")] + public partial void GenSemaphoresEXT(int n, out uint semaphores); + + [GetProcAddress("glDeleteSemaphoresEXT")] + public partial void DeleteSemaphoresEXT(int n, ref uint semaphores); + + [GetProcAddress("glWaitSemaphoreEXT")] + public partial void WaitSemaphoreEXT(uint semaphore, + uint numBufferBarriers, uint* buffers, + uint numTextureBarriers, int* textures, + int* srcLayouts); + + [GetProcAddress("glSignalSemaphoreEXT")] + public partial void SignalSemaphoreEXT(uint semaphore, + uint numBufferBarriers, uint* buffers, + uint numTextureBarriers, int* textures, + int* dstLayouts); + + + [GetProcAddress("glGetUnsignedBytei_vEXT", true)] + public partial void GetUnsignedBytei_vEXT(int target, uint index, byte* data); + + [GetProcAddress("glGetUnsignedBytevEXT", true)] + public partial void GetUnsignedBytevEXT(int target, byte* data); +} + +public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFeature +{ + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private List _imageTypes = new(); + private List _semaphoreTypes = new(); + + public static ExternalObjectsOpenGlExtensionFeature TryCreate(IGlContext context) + { + var extensions = context.GlInterface.GetExtensions(); + if (extensions.Contains("GL_EXT_memory_object") && extensions.Contains("GL_EXT_semaphore")) + { + try + { + return new ExternalObjectsOpenGlExtensionFeature(context, extensions); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(nameof(ExternalObjectsOpenGlExtensionFeature), + "Unable to initialize EXT_external_objects extension: " + e); + } + } + + return null; + } + + private unsafe ExternalObjectsOpenGlExtensionFeature(IGlContext context, List extensions) + { + _context = context; + _ext = new ExternalObjectsInterface(_context.GlInterface.GetProcAddress); + + if (_ext.IsGetUnsignedBytei_vEXTAvailable) + { + _context.GlInterface.GetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, out var numUiids); + if (numUiids > 0) + { + DeviceUuid = new byte[16]; + fixed (byte* pUuid = DeviceUuid) + _ext.GetUnsignedBytei_vEXT(GL_DEVICE_UUID_EXT, 0, pUuid); + } + } + + if (_ext.IsGetUnsignedBytevEXTAvailable) + { + if (extensions.Contains("GL_EXT_memory_object_win32") || extensions.Contains("GL_EXT_semaphore_win32")) + { + DeviceLuid = new byte[8]; + fixed (byte* pLuid = DeviceLuid) + _ext.GetUnsignedBytevEXT(GL_DEVICE_LUID_EXT, pLuid); + } + } + + if (extensions.Contains("GL_EXT_memory_object_fd") + && extensions.Contains("GL_EXT_semaphore_fd")) + { + _imageTypes.Add(KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + _semaphoreTypes.Add(KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor); + } + + + + } + + public IReadOnlyList SupportedImportableExternalImageTypes => _imageTypes; + public IReadOnlyList SupportedExportableExternalImageTypes { get; } = Array.Empty(); + public IReadOnlyList SupportedImportableExternalSemaphoreTypes => _semaphoreTypes; + public IReadOnlyList SupportedExportableExternalSemaphoreTypes { get; } = Array.Empty(); + public IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type) + { + return new[] + { + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm + }; + } + + public IGlExportableExternalImageTexture CreateImage(string type, PixelSize size, + PlatformGraphicsExternalImageFormat format) => + throw new NotSupportedException(); + + public IGlExportableExternalImageTexture CreateSemaphore(string type) => throw new NotSupportedException(); + + public IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + { + if(!_imageTypes.Contains(handle.HandleDescriptor)) + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + + if (handle.HandleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) + { + while (_context.GlInterface.GetError() != 0) + { + //Skip existing errors + } + _ext.CreateMemoryObjectsEXT(1, out var memoryObject); + _ext.ImportMemoryFdEXT(memoryObject, properties.MemorySize, GL_HANDLE_TYPE_OPAQUE_FD_EXT, + handle.Handle.ToInt32()); + + var err = _context.GlInterface.GetError(); + if (err != 0) + throw OpenGlException.GetFormattedException("glImportMemoryFdEXT", err); + + _context.GlInterface.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + + var texture = _context.GlInterface.GenTexture(); + _context.GlInterface.BindTexture(GL_TEXTURE_2D, texture); + _ext.TexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_RGBA8, properties.Width, properties.Height, + memoryObject, properties.MemoryOffset); + err = _context.GlInterface.GetError(); + + _context.GlInterface.BindTexture(GL_TEXTURE_2D, oldTexture); + if (err != 0) + throw OpenGlException.GetFormattedException("glTexStorageMem2DEXT", err); + + return new ExternalImageTexture(_context, properties, _ext, memoryObject, texture); + } + + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + } + + public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) + { + if(!_semaphoreTypes.Contains(handle.HandleDescriptor)) + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + + if (handle.HandleDescriptor == + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor) + { + _ext.GenSemaphoresEXT(1, out var semaphore); + _ext.ImportSemaphoreFdEXT(semaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, handle.Handle.ToInt32()); + return new ExternalSemaphore(_context, _ext, semaphore); + } + + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + } + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + { + if (imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) + return CompositionGpuImportedImageSynchronizationCapabilities.Semaphores; + return default; + } + + public byte[] DeviceLuid { get; } + public byte[] DeviceUuid { get; } + + unsafe class ExternalSemaphore : IGlExternalSemaphore + { + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private uint _semaphore; + + public ExternalSemaphore(IGlContext context, ExternalObjectsInterface ext, uint semaphore) + { + _context = context; + _ext = ext; + _semaphore = semaphore; + } + + public void Dispose() + { + if(_context.IsLost) + return; + using (_context.EnsureCurrent()) + _ext.DeleteSemaphoresEXT(1, ref _semaphore); + _semaphore = 0; + } + + public void WaitSemaphore(IGlExternalImageTexture texture) + { + var tex = (ExternalImageTexture)texture; + var texId = tex.TextureId; + var srcLayout = GL_LAYOUT_TRANSFER_SRC_EXT; + _ext.WaitSemaphoreEXT(_semaphore, 0, null, 1, &texId, &srcLayout); + } + + public void SignalSemaphore(IGlExternalImageTexture texture) + { + var tex = (ExternalImageTexture)texture; + var texId = tex.TextureId; + var dstLayout = 0; + _ext.SignalSemaphoreEXT(_semaphore, 0, null, 1, &texId, &dstLayout); + } + } + + class ExternalImageTexture : IGlExternalImageTexture + { + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private uint _objectId; + + public ExternalImageTexture(IGlContext context, + PlatformGraphicsExternalImageProperties properties, + ExternalObjectsInterface ext, uint objectId, int textureId) + { + Properties = properties; + TextureId = textureId; + _context = context; + _ext = ext; + _objectId = objectId; + } + + public void Dispose() + { + if(_context.IsLost) + return; + using (_context.EnsureCurrent()) + { + _context.GlInterface.DeleteTexture(TextureId); + _ext.DeleteMemoryObjectsEXT(1, ref _objectId); + _objectId = 0; + } + } + + public void AcquireKeyedMutex(uint key) => throw new NotSupportedException(); + + public void ReleaseKeyedMutex(uint key) => throw new NotSupportedException(); + + public int TextureId { get; } + public int InternalFormat => GL_RGBA8; + public PlatformGraphicsExternalImageProperties Properties { get; } + } +} diff --git a/src/Avalonia.OpenGL/GlConsts.cs b/src/Avalonia.OpenGL/GlConsts.cs index 3fdc147701..4fbe738ca9 100644 --- a/src/Avalonia.OpenGL/GlConsts.cs +++ b/src/Avalonia.OpenGL/GlConsts.cs @@ -534,7 +534,7 @@ namespace Avalonia.OpenGL // public const int GL_MAX_ELEMENTS_VERTICES = 0x80E8; // public const int GL_MAX_ELEMENTS_INDICES = 0x80E9; // public const int GL_BGR = 0x80E0; -// public const int GL_BGRA = 0x80E1; + public const int GL_BGRA = 0x80E1; // public const int GL_UNSIGNED_BYTE_3_3_2 = 0x8032; // public const int GL_UNSIGNED_BYTE_2_3_3_REV = 0x8362; // public const int GL_UNSIGNED_SHORT_5_6_5 = 0x8363; @@ -3372,16 +3372,16 @@ namespace Avalonia.OpenGL // public const int GL_TILING_TYPES_EXT = 0x9583; // public const int GL_OPTIMAL_TILING_EXT = 0x9584; // public const int GL_LINEAR_TILING_EXT = 0x9585; -// public const int GL_NUM_DEVICE_UUIDS_EXT = 0x9596; -// public const int GL_DEVICE_UUID_EXT = 0x9597; + internal const int GL_NUM_DEVICE_UUIDS_EXT = 0x9596; + internal const int GL_DEVICE_UUID_EXT = 0x9597; // public const int GL_DRIVER_UUID_EXT = 0x9598; // public const int GL_UUID_SIZE_EXT = 16; // public const int GL_EXT_memory_object_fd = 1; -// public const int GL_HANDLE_TYPE_OPAQUE_FD_EXT = 0x9586; + internal const int GL_HANDLE_TYPE_OPAQUE_FD_EXT = 0x9586; // public const int GL_EXT_memory_object_win32 = 1; // public const int GL_HANDLE_TYPE_OPAQUE_WIN32_EXT = 0x9587; // public const int GL_HANDLE_TYPE_OPAQUE_WIN32_KMT_EXT = 0x9588; -// public const int GL_DEVICE_LUID_EXT = 0x9599; + public const int GL_DEVICE_LUID_EXT = 0x9599; // public const int GL_DEVICE_NODE_MASK_EXT = 0x959A; // public const int GL_LUID_SIZE_EXT = 8; // public const int GL_HANDLE_TYPE_D3D12_TILEPOOL_EXT = 0x9589; @@ -3483,11 +3483,11 @@ namespace Avalonia.OpenGL // public const int GL_SECONDARY_COLOR_ARRAY_EXT = 0x845E; // public const int GL_EXT_semaphore = 1; // public const int GL_LAYOUT_GENERAL_EXT = 0x958D; -// public const int GL_LAYOUT_COLOR_ATTACHMENT_EXT = 0x958E; + internal const int GL_LAYOUT_COLOR_ATTACHMENT_EXT = 0x958E; // public const int GL_LAYOUT_DEPTH_STENCIL_ATTACHMENT_EXT = 0x958F; // public const int GL_LAYOUT_DEPTH_STENCIL_READ_ONLY_EXT = 0x9590; // public const int GL_LAYOUT_SHADER_READ_ONLY_EXT = 0x9591; -// public const int GL_LAYOUT_TRANSFER_SRC_EXT = 0x9592; + internal const int GL_LAYOUT_TRANSFER_SRC_EXT = 0x9592; // public const int GL_LAYOUT_TRANSFER_DST_EXT = 0x9593; // public const int GL_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_EXT = 0x9530; // public const int GL_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_EXT = 0x9531; diff --git a/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs b/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs new file mode 100644 index 0000000000..676f8eaf86 --- /dev/null +++ b/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL; + +public interface IGlContextExternalObjectsFeature +{ + IReadOnlyList SupportedImportableExternalImageTypes { get; } + IReadOnlyList SupportedExportableExternalImageTypes { get; } + IReadOnlyList SupportedImportableExternalSemaphoreTypes { get; } + IReadOnlyList SupportedExportableExternalSemaphoreTypes { get; } + IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type); + + IGlExportableExternalImageTexture CreateImage(string type,PixelSize size, PlatformGraphicsExternalImageFormat format); + + IGlExportableExternalImageTexture CreateSemaphore(string type); + IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties); + IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle); + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + public byte[]? DeviceLuid { get; } + public byte[]? DeviceUuid { get; } +} + +public interface IGlExternalSemaphore : IDisposable +{ + void WaitSemaphore(IGlExternalImageTexture texture); + void SignalSemaphore(IGlExternalImageTexture texture); +} + +public interface IGlExportableExternalSemaphore : IGlExternalSemaphore +{ + IPlatformHandle GetHandle(); +} + +public interface IGlExternalImageTexture : IDisposable +{ + void AcquireKeyedMutex(uint key); + void ReleaseKeyedMutex(uint key); + int TextureId { get; } + int InternalFormat { get; } + + PlatformGraphicsExternalImageProperties Properties { get; } +} + +public interface IGlExportableExternalImageTexture : IGlExternalImageTexture +{ + IPlatformHandle GetHandle(); +} diff --git a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs index 9c22d446ef..2043e944d1 100644 --- a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs +++ b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs @@ -1,6 +1,6 @@ +using System; using System.Collections.Generic; -using Avalonia.OpenGL.Imaging; -using Avalonia.Platform; +using Avalonia.Rendering.Composition; namespace Avalonia.OpenGL { @@ -8,6 +8,13 @@ namespace Avalonia.OpenGL { bool CanCreateSharedContext { get; } IGlContext CreateSharedContext(IEnumerable preferredVersions = null); - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); + ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size); + } + + public interface ICompositionImportableOpenGlSharedTexture : ICompositionImportableSharedGpuContextImage + { + int TextureId { get; } + int InternalFormat { get; } + PixelSize Size { get; } } } diff --git a/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs b/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs new file mode 100644 index 0000000000..e6c83415e6 --- /dev/null +++ b/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Avalonia.OpenGL; + +public interface IPlatformGraphicsOpenGlContextFactory +{ + IGlContext CreateContext(IEnumerable? versions); +} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs deleted file mode 100644 index 22f0cebf57..0000000000 --- a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.OpenGL.Imaging -{ - [Unstable] - public interface IOpenGlBitmapImpl : IBitmapImpl - { - IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback); - bool SupportsContext(IGlContext context); - } - - [Unstable] - public interface IOpenGlBitmapAttachment : IDisposable - { - void Present(); - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs deleted file mode 100644 index 53013ae5a3..0000000000 --- a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Avalonia.OpenGL.Imaging -{ - public class OpenGlBitmap : Bitmap, IAffectsRender - { - private IOpenGlBitmapImpl _impl; - - public OpenGlBitmap(IOpenGlTextureSharingRenderInterfaceContextFeature feature, - PixelSize size, Vector dpi) - : base(CreateOrThrow(feature, size, dpi)) - { - _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; - } - - static IOpenGlBitmapImpl CreateOrThrow(IOpenGlTextureSharingRenderInterfaceContextFeature feature, - PixelSize size, Vector dpi) => feature.CreateOpenGlBitmap(size, dpi); - - public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => - _impl.CreateFramebufferAttachment(context, SetIsDirty); - - public bool SupportsContext(IGlContext context) => _impl.SupportsContext(context); - - void SetIsDirty() - { - if (Dispatcher.UIThread.CheckAccess()) - CallInvalidated(); - else - Dispatcher.UIThread.Post(CallInvalidated); - } - - private void CallInvalidated() => Invalidated?.Invoke(this, EventArgs.Empty); - - public event EventHandler Invalidated; - } -} diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index efe305dba5..c498ed7833 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -27,6 +27,9 @@ namespace Avalonia.OpenGL return GetFormattedException(funcName, (GlErrors)err, err); } + public static OpenGlException GetFormattedException(string funcName, int errorCode) => + GetFormattedException(funcName, (GlErrors)errorCode, errorCode); + public static OpenGlException GetFormattedEglException(string funcName, int errorCode) => GetFormattedException(funcName, (EglErrors)errorCode,errorCode); diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index def5228e94..dd10beb32e 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; +using Avalonia.OpenGL.Features; + namespace Avalonia.X11.Glx { class GlxContext : IGlContext @@ -14,6 +16,7 @@ namespace Avalonia.X11.Glx private readonly IntPtr _defaultXid; private readonly bool _ownsPBuffer; private readonly object _lock = new object(); + private ExternalObjectsOpenGlExtensionFeature? _externalObjects; public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, GlxContext sharedWith, @@ -32,7 +35,10 @@ namespace Avalonia.X11.Glx SampleCount = sampleCount; StencilSize = stencilSize; using (MakeCurrent()) + { GlInterface = new GlInterface(version, GlxInterface.SafeGetProcAddress); + _externalObjects = ExternalObjectsOpenGlExtensionFeature.TryCreate(this); + } } public GlxDisplay Display { get; } @@ -123,6 +129,11 @@ namespace Avalonia.X11.Glx Glx.DestroyPbuffer(_x11.Display, _defaultXid); } - public object TryGetFeature(Type featureType) => null; + public object TryGetFeature(Type featureType) + { + if (featureType == typeof(IGlContextExternalObjectsFeature)) + return _externalObjects; + return null; + } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index b064445a0b..a5782037f3 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using SkiaSharp; @@ -33,9 +31,4 @@ namespace Avalonia.Skia bool CanBlit { get; } void Blit(SKCanvas canvas); } - - public interface IOpenGlAwareSkiaGpu : ISkiaGpu - { - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); - } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs new file mode 100644 index 0000000000..4bf43634ef --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Avalonia.OpenGL; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal class GlSkiaExternalObjectsFeature : IExternalObjectsRenderInterfaceContextFeature +{ + private readonly GlSkiaGpu _gpu; + private readonly IGlContextExternalObjectsFeature? _feature; + + public GlSkiaExternalObjectsFeature(GlSkiaGpu gpu, IGlContextExternalObjectsFeature? feature) + { + _gpu = gpu; + _feature = feature; + } + + public IReadOnlyList SupportedImageHandleTypes => _feature?.SupportedImportableExternalImageTypes + ?? Array.Empty(); + public IReadOnlyList SupportedSemaphoreTypes => _feature?.SupportedImportableExternalSemaphoreTypes + ?? Array.Empty(); + + public IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties) + { + if (_feature == null) + throw new NotSupportedException("Importing this platform handle is not supported"); + using (_gpu.EnsureCurrent()) + { + var image = _feature.ImportImage(handle, properties); + return new GlSkiaImportedImage(_gpu, image); + } + } + + public IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image) + { + var img = (GlSkiaSharedTextureForComposition)image; + if (!img.Context.IsSharedWith(_gpu.GlContext)) + throw new InvalidOperationException("Contexts do not belong to the same share group"); + + return new GlSkiaImportedImage(_gpu, img); + } + + public IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle) + { + if (_feature == null) + throw new NotSupportedException("Importing this platform handle is not supported"); + using (_gpu.EnsureCurrent()) + { + var semaphore = _feature.ImportSemaphore(handle); + return new GlSkiaImportedSemaphore(_gpu, semaphore); + } + } + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + => _feature?.GetSynchronizationCapabilities(imageHandleType) ?? default; + + public byte[]? DeviceUuid => _feature?.DeviceUuid; + public byte[]? DeviceLuid => _feature?.DeviceLuid; +} + +internal class GlSkiaImportedSemaphore : IPlatformRenderInterfaceImportedSemaphore +{ + private readonly GlSkiaGpu _gpu; + public IGlExternalSemaphore Semaphore { get; } + + public GlSkiaImportedSemaphore(GlSkiaGpu gpu, IGlExternalSemaphore semaphore) + { + _gpu = gpu; + Semaphore = semaphore; + } + + public void Dispose() => Semaphore.Dispose(); +} + +internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage +{ + private readonly GlSkiaSharedTextureForComposition? _sharedTexture; + private readonly GlSkiaGpu _gpu; + private readonly IGlExternalImageTexture? _image; + + public GlSkiaImportedImage(GlSkiaGpu gpu, IGlExternalImageTexture image) + { + _gpu = gpu; + _image = image; + } + + public GlSkiaImportedImage(GlSkiaGpu gpu, GlSkiaSharedTextureForComposition sharedTexture) + { + _gpu = gpu; + _sharedTexture = sharedTexture; + } + + public void Dispose() + { + _image?.Dispose(); + _sharedTexture?.Dispose(_gpu.GlContext); + } + + SKColorType ConvertColorType(PlatformGraphicsExternalImageFormat format) => + format switch + { + PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm => SKColorType.Bgra8888, + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm => SKColorType.Rgba8888, + _ => SKColorType.Rgba8888 + }; + + SKSurface? TryCreateSurface(int textureId, int format, int width, int height, bool topLeft) + { + var origin = topLeft ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft; + using var texture = new GRBackendTexture(width, height, false, + new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId, (uint)format)); + var surf = SKSurface.Create(_gpu.GrContext, texture, origin, SKColorType.Rgba8888); + if (surf != null) + return surf; + + using var unformatted = new GRBackendTexture(width, height, false, + new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId)); + + return SKSurface.Create(_gpu.GrContext, unformatted, origin, SKColorType.Rgba8888); + } + + IBitmapImpl TakeSnapshot() + { + var width = _image?.Properties.Width ?? _sharedTexture!.Size.Width; + var height = _image?.Properties.Height ?? _sharedTexture!.Size.Height; + var internalFormat = _image?.InternalFormat ?? _sharedTexture!.InternalFormat; + var textureId = _image?.TextureId ?? _sharedTexture!.TextureId; + var topLeft = _image?.Properties.TopLeftOrigin ?? false; + + using var texture = new GRBackendTexture(width, height, false, + new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId, (uint)internalFormat)); + + IBitmapImpl rv; + using (var surf = TryCreateSurface(textureId, internalFormat, width, height, topLeft)) + { + if (surf == null) + throw new OpenGlException("Unable to consume provided texture"); + rv = new ImmutableBitmap(surf.Snapshot()); + } + + _gpu.GrContext.Flush(); + _gpu.GlContext.GlInterface.Flush(); + return rv; + } + + public IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex) + { + using (_gpu.EnsureCurrent()) + { + _image.AcquireKeyedMutex(acquireIndex); + try + { + return TakeSnapshot(); + } + finally + { + _image.ReleaseKeyedMutex(releaseIndex); + } + } + } + + public IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, + IPlatformRenderInterfaceImportedSemaphore signalSemaphore) + { + var wait = (GlSkiaImportedSemaphore)waitForSemaphore; + var signal = (GlSkiaImportedSemaphore)signalSemaphore; + using (_gpu.EnsureCurrent()) + { + wait.Semaphore.WaitSemaphore(_image); + try + { + return TakeSnapshot(); + } + finally + { + signal.Semaphore.SignalSemaphore(_image); + } + } + } + + public IBitmapImpl SnapshotWithAutomaticSync() + { + using (_gpu.EnsureCurrent()) + return TakeSnapshot(); + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index cdba3b9ea2..bf3e950e81 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -3,19 +3,22 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Logging; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using SkiaSharp; +using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - class GlSkiaGpu : IOpenGlAwareSkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature + class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { private GRContext _grContext; private IGlContext _glContext; + public GRContext GrContext => _grContext; + public IGlContext GlContext => _glContext; private List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; + private IExternalObjectsRenderInterfaceContextFeature? _externalObjectsFeature; public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { @@ -32,6 +35,9 @@ namespace Avalonia.Skia _grContext.SetResourceCacheLimit(maxResourceBytes.Value); } } + + context.TryGetFeature(out var externalObjects); + _externalObjectsFeature = new GlSkiaExternalObjectsFeature(this, externalObjects); } } @@ -103,8 +109,34 @@ namespace Avalonia.Skia public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => _glContext.CreateSharedContext(preferredVersions); - public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); - + public ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size) + { + if (!context.IsSharedWith(_glContext)) + throw new InvalidOperationException("Contexts do not belong to the same share group"); + + using (context.EnsureCurrent()) + { + var gl = context.GlInterface; + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out int oldTexture); + var tex = gl.GenTexture(); + + var format = context.Version.Type == GlProfileType.OpenGLES && context.Version.Major == 2 + ? GL_RGBA + : GL_RGBA8; + + gl.BindTexture(GL_TEXTURE_2D, tex); + gl.TexImage2D(GL_TEXTURE_2D, 0, + format, size.Width, size.Height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + + return new GlSkiaSharedTextureForComposition(context, tex, format, size); + } + } + public void Dispose() { if (_glContext.IsLost) @@ -125,6 +157,8 @@ namespace Avalonia.Skia { if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) return this; + if (featureType == typeof(IExternalObjectsRenderInterfaceContextFeature)) + return _externalObjectsFeature; return null; } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs new file mode 100644 index 0000000000..2d53254500 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs @@ -0,0 +1,44 @@ +using Avalonia.OpenGL; + +namespace Avalonia.Skia; + +internal class GlSkiaSharedTextureForComposition : ICompositionImportableOpenGlSharedTexture +{ + public IGlContext Context { get; } + private readonly object _lock = new(); + + public GlSkiaSharedTextureForComposition(IGlContext context, int textureId, int internalFormat, PixelSize size) + { + Context = context; + TextureId = textureId; + InternalFormat = internalFormat; + Size = size; + } + public void Dispose(IGlContext context) + { + lock (_lock) + { + if(TextureId == 0) + return; + try + { + using (context.EnsureCurrent()) + context.GlInterface.DeleteTexture(TextureId); + } + catch + { + // Ignore + } + + TextureId = 0; + } + } + + public int TextureId { get; private set; } + public int InternalFormat { get; } + public PixelSize Size { get; } + public void Dispose() + { + Dispose(Context); + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs deleted file mode 100644 index d8bff7cfc8..0000000000 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; -using Avalonia.Utilities; -using SkiaSharp; -using static Avalonia.OpenGL.GlConsts; - -namespace Avalonia.Skia -{ - class GlOpenGlBitmapImpl : IOpenGlBitmapImpl, IDrawableBitmapImpl - { - private readonly IGlContext _context; - private readonly object _lock = new object(); - private IGlPresentableOpenGlSurface _surface; - - public GlOpenGlBitmapImpl(IGlContext context, PixelSize pixelSize, Vector dpi) - { - _context = context; - PixelSize = pixelSize; - Dpi = dpi; - } - - public Vector Dpi { get; } - public PixelSize PixelSize { get; } - public int Version { get; private set; } - public void Save(string fileName, int? quality = null) => throw new NotSupportedException(); - - public void Save(Stream stream, int? quality = null) => throw new NotSupportedException(); - - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) - { - lock (_lock) - { - if (_surface == null) - return; - using (_surface.Lock()) - { - using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, - new GRGlTextureInfo( - GlConsts.GL_TEXTURE_2D, (uint)_surface.GetTextureId(), - (uint)_surface.InternalFormat))) - using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, - SKColorType.Rgba8888, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal))) - { - // Again, silently ignore, if something went wrong it's not our fault - if (surface == null) - return; - - using (var snapshot = surface.Snapshot()) - context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); - } - - } - } - } - - public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback) - { - if (!SupportsContext(context)) - throw new OpenGlException("Context is not supported for texture sharing"); - return new SharedOpenGlBitmapAttachment(this, context, presentCallback); - } - - public bool SupportsContext(IGlContext context) - { - // TODO: negotiated platform surface sharing - return _context.IsSharedWith(context); - } - - public void Dispose() - { - - } - - internal void Present(IGlPresentableOpenGlSurface surface) - { - lock (_lock) - { - _surface = surface; - } - } - } - - interface IGlPresentableOpenGlSurface : IDisposable - { - int GetTextureId(); - int InternalFormat { get; } - IDisposable Lock(); - } - - class SharedOpenGlBitmapAttachment : IOpenGlBitmapAttachment, IGlPresentableOpenGlSurface - { - private readonly GlOpenGlBitmapImpl _bitmap; - private readonly IGlContext _context; - private readonly Action _presentCallback; - private readonly int _fbo; - private readonly int _texture; - private readonly int _frontBuffer; - private bool _disposed; - private readonly DisposableLock _lock = new DisposableLock(); - - public unsafe SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) - { - _bitmap = bitmap; - _context = context; - _presentCallback = presentCallback; - using (_context.EnsureCurrent()) - { - var glVersion = _context.Version; - InternalFormat = glVersion.Type == GlProfileType.OpenGLES && glVersion.Major == 2 - ? GL_RGBA - : GL_RGBA8; - - _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); - if (_fbo == 0) - throw new OpenGlException("Current FBO is 0"); - - { - var gl = _context.GlInterface; - - Span textures = stackalloc int[2]; - fixed (int* ptex = textures) - gl.GenTextures(2, ptex); - _texture = textures[0]; - _frontBuffer = textures[1]; - - gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); - foreach (var t in textures) - { - gl.BindTexture(GL_TEXTURE_2D, t); - gl.TexImage2D(GL_TEXTURE_2D, 0, - InternalFormat, - _bitmap.PixelSize.Width, _bitmap.PixelSize.Height, - 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); - - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - } - - gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - } - } - } - - public void Present() - { - using (_context.EnsureCurrent()) - { - if (_disposed) - throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); - - var gl = _context.GlInterface; - - gl.Finish(); - using (Lock()) - { - gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var oldFbo); - gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); - gl.GetIntegerv(GL_ACTIVE_TEXTURE, out var oldActive); - - gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); - gl.ActiveTexture(GL_TEXTURE0); - gl.BindTexture(GL_TEXTURE_2D, _frontBuffer); - - gl.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, _bitmap.PixelSize.Width, - _bitmap.PixelSize.Height); - - gl.BindFramebuffer(GL_FRAMEBUFFER, oldFbo); - gl.ActiveTexture(oldActive); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - - gl.Finish(); - } - } - - _bitmap.Present(this); - _presentCallback(); - } - - public unsafe void Dispose() - { - var gl = _context.GlInterface; - _bitmap.Present(null); - - if(_disposed) - return; - using (_context.MakeCurrent()) - using (Lock()) - { - if(_disposed) - return; - _disposed = true; - var ptex = stackalloc[] { _texture, _frontBuffer }; - gl.DeleteTextures(2, ptex); - } - } - - int IGlPresentableOpenGlSurface.GetTextureId() - { - return _frontBuffer; - } - - public int InternalFormat { get; } - - public IDisposable Lock() => _lock.Lock(); - } -} diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index e24d805050..802736119f 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -37,6 +37,13 @@ namespace Avalonia.Skia } } + public ImmutableBitmap(SKImage image) + { + _image = image; + PixelSize = new PixelSize(image.Width, image.Height); + Dpi = new Vector(96, 96); + } + public ImmutableBitmap(ImmutableBitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode) { SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b202b60cdf..23b7edcc8f 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,13 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading; - -using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using Avalonia.Media.Imaging; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 4949f4a50d..0cf66767cb 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -43,5 +43,7 @@ internal class SkiaContext : IPlatformRenderInterfaceContext "Don't know how to create a Skia render target from any of provided surfaces"); } + public bool IsLost => _gpu.IsLost; + public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5887ba2172..25c351c908 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -238,6 +238,7 @@ namespace Avalonia.Direct2D1 } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); + public bool IsLost => false; } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs index a749ed1b45..1357e8aa63 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs @@ -52,6 +52,44 @@ namespace Avalonia.Win32.DirectX D3D11_USAGE_STAGING = 3, } + + [Flags] + internal enum D3D11_RESOURCE_MISC_FLAG + { + D3D11_RESOURCE_MISC_GENERATE_MIPS = 0x00000001, + D3D11_RESOURCE_MISC_SHARED = 0x00000002, + D3D11_RESOURCE_MISC_TEXTURECUBE = 0x00000004, + D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS = 0x00000010, + D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS = 0x00000020, + D3D11_RESOURCE_MISC_BUFFER_STRUCTURED = 0x00000040, + D3D11_RESOURCE_MISC_RESOURCE_CLAMP = 0x00000080, + D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX = 0x00000100, + D3D11_RESOURCE_MISC_GDI_COMPATIBLE = 0x00000200, + D3D11_RESOURCE_MISC_SHARED_NTHANDLE = 0x00000800, + D3D11_RESOURCE_MISC_RESTRICTED_CONTENT = 0x00001000, + D3D11_RESOURCE_MISC_RESTRICT_SHARED_RESOURCE = 0x00002000, + D3D11_RESOURCE_MISC_RESTRICT_SHARED_RESOURCE_DRIVER = 0x00004000, + D3D11_RESOURCE_MISC_GUARDED = 0x00008000, + D3D11_RESOURCE_MISC_TILE_POOL = 0x00020000, + D3D11_RESOURCE_MISC_TILED = 0x00040000, + D3D11_RESOURCE_MISC_HW_PROTECTED = 0x00080000, + } + + [Flags] + internal enum D3D11_BIND_FLAG + { + D3D11_BIND_VERTEX_BUFFER = 0x00000001, + D3D11_BIND_INDEX_BUFFER = 0x00000002, + D3D11_BIND_CONSTANT_BUFFER = 0x00000004, + D3D11_BIND_SHADER_RESOURCE = 0x00000008, + D3D11_BIND_STREAM_OUTPUT = 0x00000010, + D3D11_BIND_RENDER_TARGET = 0x00000020, + D3D11_BIND_DEPTH_STENCIL = 0x00000040, + D3D11_BIND_UNORDERED_ACCESS = 0x00000080, + D3D11_BIND_DECODER = 0x00000200, + D3D11_BIND_VIDEO_ENCODER = 0x00000400, + } + internal enum DXGI_SWAP_EFFECT { DXGI_SWAP_EFFECT_DISCARD = 0, diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs index 47451831a6..c11a6026e7 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs @@ -282,11 +282,11 @@ namespace Avalonia.Win32.DirectX public D3D11_USAGE Usage; - public uint BindFlags; + public D3D11_BIND_FLAG BindFlags; public uint CPUAccessFlags; - public uint MiscFlags; + public D3D11_RESOURCE_MISC_FLAG MiscFlags; } #nullable restore } diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index a4552eedea..1d66c898db 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -10,6 +10,7 @@ @clr-map SIZE Avalonia.Win32.Interop.UnmanagedMethods.SIZE @clr-map POINT Avalonia.Win32.Interop.UnmanagedMethods.POINT @clr-map HWND IntPtr +@clr-map HANDLE IntPtr @clr-map BOOL int @clr-map DWORD int @clr-map SIZE_T IntPtr @@ -259,10 +260,28 @@ interface IDXGISurface : IDXGIDeviceSubObject HRESULT Unmap(); } +[uuid( 035f3ab4-482e-4e50-b41f-8a7f8bd8960b)] +interface IDXGIResource : IDXGIDeviceSubObject +{ + HRESULT GetSharedHandle( [out, annotation("_Out_")] HANDLE * pSharedHandle ); + HRESULT GetUsage( [out] DXGI_USAGE * pUsage ); + HRESULT SetEvictionPriority( [in] UINT EvictionPriority ); + HRESULT GetEvictionPriority( [out, retval, annotation("_Out_")] UINT* pEvictionPriority ); +}; + + +[ uuid( 9d8e1289-d7b3-465f-8126-250e349af85d)] +interface IDXGIKeyedMutex : + IDXGIDeviceSubObject +{ + HRESULT AcquireSync( [in] UINT64 Key, [in] uint dwMilliseconds); + HRESULT ReleaseSync( [in] UINT64 Key); +}; + [uuid(770aae78-f26f-4dba-a829-253c83d1b387)] interface IDXGIFactory1 : IDXGIFactory { - HRESULT EnumAdapters1([in] UINT Adapter, [out, annotation("_COM_Outptr_")] IDXGIAdapter1** ppAdapter); + int EnumAdapters1([in] UINT Adapter, [out] void** ppAdapter); BOOL IsCurrent(); } @@ -339,9 +358,9 @@ interface ID3D11Device : IUnknown IntPtr pInitialData, [out, retval] IUnknown** ppTexture1D ); HRESULT CreateTexture2D( - IntPtr pDesc, + D3D11_TEXTURE2D_DESC* pDesc, IntPtr pInitialData, - [out, retval] IUnknown** ppTexture2D ); + [out, retval] ID3D11Texture2D** ppTexture2D ); HRESULT CreateTexture3D( IntPtr pDesc, IntPtr pInitialData, @@ -481,3 +500,11 @@ interface ID3D11Device : IUnknown HRESULT SetExceptionMode( UINT RaiseFlags ); UINT GetExceptionMode(); } + + +[uuid( 6f15aaf2-d208-4e89-9ab4-489535d34f9c)] +interface ID3D11Texture2D : IUnknown +{ + // Just a marker interface for now +}; + diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs new file mode 100644 index 0000000000..b0fa76fda2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs @@ -0,0 +1,107 @@ +#nullable enable +using System; +using System.Threading; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; +using static Avalonia.OpenGL.Egl.EglConsts; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture +{ + private readonly EglContext _context; + private ID3D11Texture2D _texture2D; + private EglSurface _eglSurface; + private IDXGIKeyedMutex _mutex; + + public unsafe AngleExternalMemoryD3D11Texture2D(EglContext context, ID3D11Texture2D texture2D, PlatformGraphicsExternalImageProperties props) + { + _context = context; + _texture2D = texture2D.CloneReference(); + _mutex = _texture2D.QueryInterface(); + Properties = props; + + InternalFormat = GL_RGBA8; + + _eglSurface = _context.Display.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, texture2D.GetNativeIntPtr(), + new[] + { + EGL_WIDTH, props.Width, EGL_HEIGHT, props.Height, EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA, + EGL_TEXTURE_TARGET, EGL_TEXTURE_2D, EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_RGBA, EGL_NONE, EGL_NONE, + EGL_NONE + }); + + var gl = _context.GlInterface; + int temp = 0; + gl.GenTextures(1, &temp); + TextureId = temp; + gl.BindTexture(GlConsts.GL_TEXTURE_2D, TextureId); + + if (_context.Display.EglInterface.BindTexImage(_context.Display.Handle, _eglSurface.DangerousGetHandle(), + EGL_BACK_BUFFER) == 0) + + throw OpenGlException.GetFormattedException("eglBindTexImage", _context.Display.EglInterface); + } + + public void Dispose() + { + + if (!_context.IsLost && TextureId != 0) + using (_context.EnsureCurrent()) + _context.GlInterface.DeleteTexture(TextureId); + TextureId = 0; + _eglSurface?.Dispose(); + _eglSurface = null!; + _texture2D?.Dispose(); + _texture2D = null!; + _mutex?.Dispose(); + _mutex = null!; + } + + + public void AcquireKeyedMutex(uint key) => _mutex.AcquireSync(key, int.MaxValue); + + public void ReleaseKeyedMutex(uint key) => _mutex.ReleaseSync(key); + + public int TextureId { get; private set; } + public int InternalFormat { get; } + public PlatformGraphicsExternalImageProperties Properties { get; } +} + +internal class AngleExternalMemoryD3D11ExportedTexture2D : AngleExternalMemoryD3D11Texture2D, IGlExportableExternalImageTexture +{ + static IPlatformHandle GetHandle(ID3D11Texture2D texture2D) + { + using var resource = texture2D.QueryInterface(); + return new PlatformHandle(resource.SharedHandle, + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle); + } + + public AngleExternalMemoryD3D11ExportedTexture2D(EglContext context, ID3D11Texture2D texture2D, + D3D11_TEXTURE2D_DESC desc, + PlatformGraphicsExternalImageFormat format) + : this(context, texture2D, GetHandle(texture2D), + new PlatformGraphicsExternalImageProperties + { + Width = (int)desc.Width, Height = (int)desc.Height, Format = format + }) + { + + } + + private AngleExternalMemoryD3D11ExportedTexture2D(EglContext context, ID3D11Texture2D texture2D, + IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + : base(context, texture2D, properties) + { + Handle = handle; + } + + public IPlatformHandle Handle { get; } + public IPlatformHandle GetHandle() => Handle; + +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs new file mode 100644 index 0000000000..a7b52e953e --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Documents; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleExternalObjectsFeature : IGlContextExternalObjectsFeature, IDisposable +{ + private readonly EglContext _context; + private readonly ID3D11Device _device; + + public AngleExternalObjectsFeature(EglContext context) + { + _context = context; + var angle = (AngleWin32EglDisplay)context.Display; + _device = MicroComRuntime.CreateProxyFor(angle.GetDirect3DDevice(), false).CloneReference(); + using var dxgiDevice = _device.QueryInterface(); + using var adapter = dxgiDevice.Adapter; + DeviceLuid = BitConverter.GetBytes(adapter.Desc.AdapterLuid); + } + + public IReadOnlyList SupportedImportableExternalImageTypes { get; } = new[] + { + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle + }; + + public IReadOnlyList SupportedExportableExternalImageTypes => SupportedImportableExternalImageTypes; + public IReadOnlyList SupportedImportableExternalSemaphoreTypes => Array.Empty(); + public IReadOnlyList SupportedExportableExternalSemaphoreTypes => Array.Empty(); + + public IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type) => + new[] + { + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm + }; + + public unsafe IGlExportableExternalImageTexture CreateImage(string type, PixelSize size, + PlatformGraphicsExternalImageFormat format) + { + if (format != PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm) + throw new NotSupportedException("Unsupported external memory format"); + using (_context.EnsureCurrent()) + { + var fmt = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; + + var desc = new D3D11_TEXTURE2D_DESC + { + Format = fmt, + Width = (uint)size.Width, + Height = (uint)size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDesc = new DXGI_SAMPLE_DESC { Count = 1, Quality = 0 }, + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + CPUAccessFlags = 0, + MiscFlags = D3D11_RESOURCE_MISC_FLAG.D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX, + BindFlags = D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET | D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, + }; + using var texture = _device.CreateTexture2D(&desc, IntPtr.Zero); + return new AngleExternalMemoryD3D11ExportedTexture2D(_context, texture, desc, format); + } + } + + public IGlExportableExternalImageTexture CreateSemaphore(string type) => throw new NotSupportedException(); + + public unsafe IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + { + if (handle.HandleDescriptor != KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) + throw new NotSupportedException("Unsupported external memory type"); + + using (_context.EnsureCurrent()) + { + var guid = MicroComRuntime.GetGuidFor(typeof(ID3D11Texture2D)); + using var opened = _device.OpenSharedResource(handle.Handle, &guid); + using var texture = opened.QueryInterface(); + return new AngleExternalMemoryD3D11Texture2D(_context, texture, properties); + } + } + + public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) => throw new NotSupportedException(); + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + { + if (imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle + || imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureNtHandle) + return CompositionGpuImportedImageSynchronizationCapabilities.KeyedMutex; + return default; + } + + public byte[] DeviceLuid { get; } + public byte[] DeviceUuid { get; } + + public void Dispose() + { + _device.Dispose(); + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs index b2d7b2014b..53bf2fb8b1 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs @@ -1,5 +1,8 @@ +#nullable enable annotations using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; @@ -7,6 +10,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.Win32.DirectX; using MicroCom.Runtime; using static Avalonia.OpenGL.Egl.EglConsts; +// ReSharper disable SimplifyLinqExpressionUseMinByAndMaxBy namespace Avalonia.Win32.OpenGl.Angle { @@ -40,51 +44,90 @@ namespace Avalonia.Win32.OpenGl.Angle }, AngleOptions.PlatformApi.DirectX11); } - public static AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl) + public static unsafe AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl, + bool preferDiscreteAdapter = false) { - unsafe + var featureLevels = new[] { - var featureLevels = new[] - { - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 - }; - - DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, - IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, - 7, out var pD3dDevice, out var featureLevel, null); - if (pD3dDevice == IntPtr.Zero) - throw new Win32Exception("Unable to create D3D11 Device"); + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 + }; - var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); - var angleDevice = IntPtr.Zero; - var display = IntPtr.Zero; + var dxgiFactoryGuid = MicroComRuntime.GetGuidFor(typeof(IDXGIFactory1)); + DirectXUnmanagedMethods.CreateDXGIFactory1(ref dxgiFactoryGuid, out var pDxgiFactory); + IDXGIAdapter1? chosenAdapter = null; + if (pDxgiFactory != null) + { + using var factory = MicroComRuntime.CreateProxyFor(pDxgiFactory, true); - void Cleanup() + void* pAdapter = null; + if (preferDiscreteAdapter) { - if (angleDevice != IntPtr.Zero) - egl.ReleaseDeviceANGLE(angleDevice); - d3dDevice.Dispose(); + ushort adapterIndex = 0; + var adapters = new List<(IDXGIAdapter1 adapter, string name)>(); + while (factory.EnumAdapters1(adapterIndex, &pAdapter) == 0) + { + var adapter = MicroComRuntime.CreateProxyFor(pAdapter, true); + var desc = adapter.Desc1; + var name = Marshal.PtrToStringUni(new IntPtr(desc.Description))!.ToLowerInvariant(); + adapters.Add((adapter, name)); + adapterIndex++; + } + + if (adapters.Count == 0) + throw new OpenGlException("No adapters found"); + chosenAdapter = adapters + .OrderByDescending(x => x.name.Contains("nvidia") ? 2 : x.name.Contains("amd") ? 1 : 0) + .First().adapter.CloneReference(); + foreach (var a in adapters) + a.adapter.Dispose(); } - - bool success = false; - try + else { - angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); - if (angleDevice == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + if (factory.EnumAdapters1(0, &pAdapter) != 0) + throw new OpenGlException("No adapters found"); + chosenAdapter = MicroComRuntime.CreateProxyFor(pAdapter, true); + } + } + + IntPtr pD3dDevice; + using (chosenAdapter) + DirectXUnmanagedMethods.D3D11CreateDevice(chosenAdapter?.GetNativeIntPtr() ?? IntPtr.Zero, + D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, + IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, + 7, out pD3dDevice, out var featureLevel, null); + - display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); - if (display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + if (pD3dDevice == IntPtr.Zero) + throw new Win32Exception("Unable to create D3D11 Device"); + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + var angleDevice = IntPtr.Zero; + var display = IntPtr.Zero; - var rv = new AngleWin32EglDisplay(display, new EglDisplayOptions + void Cleanup() + { + if (angleDevice != IntPtr.Zero) + egl.ReleaseDeviceANGLE(angleDevice); + d3dDevice.Dispose(); + } + + bool success = false; + try + { + angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); + if (angleDevice == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + + display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + + + var rv = new AngleWin32EglDisplay(display, + new EglDisplayOptions { DisposeCallback = Cleanup, Egl = egl, @@ -92,17 +135,16 @@ namespace Avalonia.Win32.OpenGl.Angle DeviceLostCheckCallback = () => d3dDevice.DeviceRemovedReason != 0, GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles }, AngleOptions.PlatformApi.DirectX11); - success = true; - return rv; - } - finally + success = true; + return rv; + } + finally + { + if (!success) { - if (!success) - { - if (display != IntPtr.Zero) - egl.Terminate(display); - Cleanup(); - } + if (display != IntPtr.Zero) + egl.Terminate(display); + Cleanup(); } } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs index 9a829aff92..a4d1ea457a 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -9,7 +9,7 @@ using Avalonia.Platform; namespace Avalonia.Win32.OpenGl.Angle; -internal class AngleWin32PlatformGraphics : IPlatformGraphics +internal class AngleWin32PlatformGraphics : IPlatformGraphics, IPlatformGraphicsOpenGlContextFactory { private readonly Win32AngleEglInterface _egl; private AngleWin32EglDisplay _sharedDisplay; @@ -29,9 +29,10 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics var rv = display.CreateContext(new EglContextOptions { DisposeCallback = display.Dispose, - ExtraFeatures = new Dictionary + ExtraFeatures = new Dictionary> { - [typeof(IGlPlatformSurfaceRenderTargetFactory)] = new AngleD3DTextureFeature() + [typeof(IGlPlatformSurfaceRenderTargetFactory)] = _ => new AngleD3DTextureFeature(), + [typeof(IGlContextExternalObjectsFeature)] = context => new AngleExternalObjectsFeature(context) } }); success = true; @@ -73,8 +74,6 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics public static AngleWin32PlatformGraphics TryCreate(AngleOptions options) { - - Win32AngleEglInterface egl; try { @@ -128,4 +127,13 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics } return null; } + + public IGlContext CreateContext(IEnumerable? versions) + { + if (UsesSharedContext) + throw new InvalidOperationException(); + if (versions != null && versions.All(v => v.Type != GlProfileType.OpenGLES || v.Major != 3)) + throw new OpenGlException("Unable to create context with requested version"); + return (IGlContext)CreateContext(); + } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 25ea060576..ab6de1a027 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -37,6 +37,9 @@ namespace Avalonia.Win32 if (egl != null && egl.PlatformApi == AngleOptions.PlatformApi.DirectX11) { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(egl); + if (opts.UseWindowsUIComposition) { WinUiCompositorConnection.TryCreateAndRegister(); diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 42e33729ac..6e0d7f657a 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -15,6 +15,8 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a272d89b8a..5b28453c17 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -46,6 +46,8 @@ namespace Avalonia.Benchmarks throw new NotImplementedException(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index f9e1e45098..143937307e 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -57,6 +57,8 @@ namespace Avalonia.UnitTests return new MockRenderTarget(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) From 96038db08fd908a3a25ca223ecdaf539fb21b5c1 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 19 Jan 2023 16:12:00 +0100 Subject: [PATCH 051/326] fix: ItemTemplate is not applied on Menu/MenuItem --- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 1 + src/Avalonia.Themes.Simple/Controls/MenuItem.xaml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index e87f4b7d93..72a7797bc3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -92,6 +92,7 @@ + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding ItemTemplate}"> From 63e428851c2b783896218d5a0275f9f38d774884 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 19 Jan 2023 16:33:20 +0100 Subject: [PATCH 052/326] feat(MenuFlyout): Add ItemContainerTheme --- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 19 ++++++++++++++++++- .../Flyouts/MenuFlyoutPresenter.cs | 10 ++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index 97fda68051..b028a8f007 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -3,6 +3,7 @@ using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -27,6 +28,12 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(ItemTemplate), x => x.ItemTemplate, (x, v) => x.ItemTemplate = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty ItemContainerThemeProperty = + ItemsControl.ItemContainerThemeProperty.AddOwner(); + public Classes FlyoutPresenterClasses => _classes ??= new Classes(); /// @@ -48,6 +55,15 @@ namespace Avalonia.Controls set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value); } + /// + /// Gets or sets the that is applied to the container element generated for each item. + /// + public ControlTheme? ItemContainerTheme + { + get { return GetValue(ItemContainerThemeProperty); } + set { SetValue(ItemContainerThemeProperty, value); } + } + private Classes? _classes; private IEnumerable? _items; private IDataTemplate? _itemTemplate; @@ -57,7 +73,8 @@ namespace Avalonia.Controls return new MenuFlyoutPresenter { [!ItemsControl.ItemsProperty] = this[!ItemsProperty], - [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty] + [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty], + [!ItemsControl.ItemContainerThemeProperty] = this[!ItemContainerThemeProperty], }; } diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs index 7aca21b42e..12fa014fc6 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -47,5 +47,15 @@ namespace Avalonia.Controls } } } + + protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index) + { + base.PrepareContainerForItemOverride(element, item, index); + + // Child menu items should not inherit the menu's ItemContainerTheme as that is specific + // for top-level menu items. + if ((element as MenuItem)?.ItemContainerTheme == ItemContainerTheme) + element.ClearValue(ItemContainerThemeProperty); + } } } From 5cc0edb8aed1839ff0b9103848f7ccf8ab984195 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 19 Jan 2023 17:03:55 +0100 Subject: [PATCH 053/326] fix: ItemContainerGenerator obsolete member after #9677 --- .../Automation/Peers/ListItemAutomationPeer.cs | 4 ++-- .../Peers/SelectingItemsControlAutomationPeer.cs | 2 +- src/Avalonia.Controls/ComboBox.cs | 4 ++-- src/Avalonia.Controls/MenuBase.cs | 4 ++-- src/Avalonia.Controls/MenuItem.cs | 7 +++---- src/Avalonia.Controls/TabControl.cs | 2 +- src/Avalonia.Controls/TreeView.cs | 2 +- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index ac23873e6a..85f139a6a3 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -36,7 +36,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is SelectingItemsControl parent) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) parent.SelectedIndex = index; @@ -50,7 +50,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is ItemsControl parent && parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) selectionModel.Select(index); diff --git a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs index 7ae0ba7244..11480fcb34 100644 --- a/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs @@ -35,7 +35,7 @@ namespace Avalonia.Automation.Peers foreach (var i in selection.SelectedIndexes) { - var container = owner.ItemContainerGenerator.ContainerFromIndex(i); + var container = owner.ContainerFromIndex(i); if (container is Control c && c.IsAttachedToVisualTree) { diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index aeaf94d728..b7a298bb16 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -399,12 +399,12 @@ namespace Avalonia.Controls var selectedIndex = SelectedIndex; if (IsDropDownOpen && selectedIndex != -1) { - var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + var container = ContainerFromIndex(selectedIndex); if (container == null && SelectedIndex != -1) { ScrollIntoView(Selection.SelectedIndex); - container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); + container = ContainerFromIndex(selectedIndex); } if (container != null && CanFocus(container)) diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index 1918964bc8..da7a36fa73 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -84,13 +84,13 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : + (IMenuItem?)ContainerFromIndex(index) : null; } set { SelectedIndex = value is Control c ? - ItemContainerGenerator.IndexFromContainer(c) : -1; + IndexFromContainer(c) : -1; } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index aeee6f8410..5588bde7c0 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -13,7 +13,6 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -319,12 +318,12 @@ namespace Avalonia.Controls { var index = SelectedIndex; return (index != -1) ? - (IMenuItem?)ItemContainerGenerator.ContainerFromIndex(index) : + (IMenuItem?)ContainerFromIndex(index) : null; } set { - SelectedIndex = value is Control c ? ItemContainerGenerator.IndexFromContainer(c) : -1; + SelectedIndex = value is Control c ? IndexFromContainer(c) : -1; } } @@ -691,7 +690,7 @@ namespace Avalonia.Controls if (selected != -1) { - var container = ItemContainerGenerator?.ContainerFromIndex(selected); + var container = ContainerFromIndex(selected); container?.Focus(); } } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 33089344e8..f12a66a4e6 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -195,7 +195,7 @@ namespace Avalonia.Controls else { var container = SelectedItem as IContentControl ?? - ItemContainerGenerator.ContainerFromIndex(SelectedIndex) as IContentControl; + ContainerFromIndex(SelectedIndex) as IContentControl; SelectedContentTemplate = container?.ContentTemplate; SelectedContent = container?.Content; } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 2fc901438d..67e0d85436 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -575,7 +575,7 @@ namespace Avalonia.Controls { var previous = (TreeViewItem)parentItemsControl.ContainerFromIndex(index - 1)!; result = previous.IsExpanded && previous.ItemCount > 0 ? - (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1)! : + (TreeViewItem)previous.ContainerFromIndex(previous.ItemCount - 1)! : previous; } else From f2908c6c79075bdbeb6a3b19a72a789f4245289b Mon Sep 17 00:00:00 2001 From: cristinathoughtpennies Date: Thu, 19 Jan 2023 10:56:24 -0600 Subject: [PATCH 054/326] HeaderDoubleTapped in TreeViewItem is now protected virtual instead of private --- src/Avalonia.Controls/TreeViewItem.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index fb20a7ee14..532d0e32db 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -264,6 +264,15 @@ namespace Avalonia.Controls Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll } } + + protected virtual void HeaderDoubleTapped(object? sender, TappedEventArgs e) + { + if (ItemCount > 0) + { + IsExpanded = !IsExpanded; + e.Handled = true; + } + } private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class { @@ -277,14 +286,5 @@ namespace Avalonia.Controls return logical != null ? result : @default; } - - private void HeaderDoubleTapped(object? sender, TappedEventArgs e) - { - if (ItemCount > 0) - { - IsExpanded = !IsExpanded; - e.Handled = true; - } - } } } From 89a78f557b13f61b06c6eb56915b7f1a077808e7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 18:39:25 +0100 Subject: [PATCH 055/326] Don't keep the text layout buffers around if they're too large --- .../Media/TextFormatting/BidiReorderer.cs | 4 +- .../TextFormatting/FormattingBufferHelper.cs | 62 +++++++++++++++++++ .../TextFormatting/FormattingObjectPool.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 3 + .../TextFormatting/Unicode/BiDiAlgorithm.cs | 53 ++++++++++++++-- .../Media/TextFormatting/Unicode/BiDiData.cs | 22 +++++-- src/Avalonia.Base/Utilities/BidiDictionary.cs | 29 ++++----- .../Text/HugeTextLayout.cs | 8 +-- 8 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 2c6db4b753..4db55fae6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -117,8 +117,8 @@ namespace Avalonia.Media.TextFormatting } finally { - _runs.Clear(); - _ranges.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs new file mode 100644 index 0000000000..0341842cb6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal static class FormattingBufferHelper + { + // 1MB, arbitrary, that's 512K characters or 128K object references on x64 + private const long MaxKeptBufferSizeInBytes = 1024 * 1024; + + public static void ClearThenResetIfTooLarge(ref ArrayBuilder arrayBuilder) + { + arrayBuilder.Clear(); + + if (IsBufferTooLarge(arrayBuilder.Capacity)) + { + arrayBuilder = default; + } + } + + public static void ClearThenResetIfTooLarge(List list) + { + list.Clear(); + + if (IsBufferTooLarge(list.Capacity)) + { + list.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(Stack stack) + { + stack.Clear(); + + if (IsBufferTooLarge(stack.Count)) + { + stack.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) + where TKey : notnull + { + dictionary.Clear(); + + // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here + if (IsBufferTooLarge>(dictionary.Count)) + { +#if NET6_0_OR_GREATER + dictionary.TrimExcess(); +#else + dictionary = new Dictionary(); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsBufferTooLarge(int length) + => (long)Unsafe.SizeOf() * length > MaxKeptBufferSizeInBytes; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs index 0468d8f413..cb8168e693 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting } --_pendingReturnCount; - rentedList.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList); if (_size < MaxSize) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index c25d530472..bf9f6f77f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -225,6 +225,9 @@ namespace Avalonia.Media.TextFormatting CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); + bidiData.Reset(); + bidiAlgorithm.Reset(); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); for (var index = 0; index < processedRuns.Count; index++) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index e960a510a9..3a81784152 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -28,6 +28,11 @@ namespace Avalonia.Media.TextFormatting.Unicode /// internal sealed class BidiAlgorithm { + /// + /// Whether the state is clean and can be reused without a reset. + /// + private bool _hasCleanState = true; + /// /// The original BiDiClass classes as provided by the caller /// @@ -226,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs.Clear(); - _workingClassesBuffer.Clear(); - _levelRuns.Clear(); - _resolvedLevelsBuffer.Clear(); + Reset(); if (types.IsEmpty) { return; } + _hasCleanState = false; + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); @@ -1639,6 +1643,47 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + /// + /// Resets the bidi algorithm to a clean state. + /// + public void Reset() + { + if (_hasCleanState) + { + return; + } + + _originalClasses = default; + _pairedBracketTypes = default; + _pairedBracketValues = default; + _hasBrackets = default; + _hasEmbeddings = default; + _hasIsolates = default; + _isolatePairs.ClearThenResetIfTooLarge(); + _workingClasses = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer); + _resolvedLevels = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer); + _paragraphEmbeddingLevel = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map); + FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings); + _runLevel = default; + _runDirection = default; + _runLength = default; + _runResolvedClasses = default; + _runOriginalClasses = default; + _runLevels = default; + _runBiDiPairedBracketTypes = default; + _runPairedBracketValues = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets); + + _hasCleanState = true; + } + /// /// Hold the start and end index of a pair of brackets /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 226e5ad6bd..8bd2171a41 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -14,6 +14,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// To avoid allocations, this class is designed to be reused. internal sealed class BidiData { + private bool _hasCleanState = true; private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; private ArrayBuilder _pairedBracketValues; @@ -62,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text to process. public void Append(ReadOnlySpan text) { + _hasCleanState = false; + _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); _pairedBracketValues.Add(text.Length); @@ -183,12 +186,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void Reset() { - _classes.Clear(); - _pairedBracketTypes.Clear(); - _pairedBracketValues.Clear(); - _savedClasses.Clear(); - _savedPairedBracketTypes.Clear(); - _tempLevelBuffer.Clear(); + if (_hasCleanState) + { + return; + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer); ParagraphEmbeddingLevel = 0; HasBrackets = false; @@ -199,6 +207,8 @@ namespace Avalonia.Media.TextFormatting.Unicode Classes = default; PairedBracketTypes = default; PairedBracketValues = default; + + _hasCleanState = true; } } } diff --git a/src/Avalonia.Base/Utilities/BidiDictionary.cs b/src/Avalonia.Base/Utilities/BidiDictionary.cs index 654fbc9807..01af53ad89 100644 --- a/src/Avalonia.Base/Utilities/BidiDictionary.cs +++ b/src/Avalonia.Base/Utilities/BidiDictionary.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media.TextFormatting; namespace Avalonia.Utilities { @@ -9,32 +11,27 @@ namespace Avalonia.Utilities /// Value type internal sealed class BidiDictionary where T1 : notnull where T2 : notnull { - public Dictionary Forward { get; } = new Dictionary(); + private Dictionary _forward = new(); + private Dictionary _reverse = new(); - public Dictionary Reverse { get; } = new Dictionary(); - - public void Clear() + public void ClearThenResetIfTooLarge() { - Forward.Clear(); - Reverse.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse); } public void Add(T1 key, T2 value) { - Forward.Add(key, value); - Reverse.Add(value, key); + _forward.Add(key, value); + _reverse.Add(value, key); } -#pragma warning disable CS8601 - public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value); -#pragma warning restore CS8601 + public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value); -#pragma warning disable CS8601 - public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key); -#pragma warning restore CS8601 + public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key); - public bool ContainsKey(T1 key) => Forward.ContainsKey(key); + public bool ContainsKey(T1 key) => _forward.ContainsKey(key); - public bool ContainsValue(T2 value) => Reverse.ContainsKey(value); + public bool ContainsValue(T2 value) => _reverse.ContainsKey(value); } } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 8b23855fde..c96edbef5c 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -48,10 +48,10 @@ It may reveal how the matters of peculiar interest slowly the goals and objectiv In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; [Params(false, true)] - public bool UseWrapping { get; set; } + public bool Wrap { get; set; } [Params(false, true)] - public bool UseTrimming { get; set; } + public bool Trim { get; set; } [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -101,8 +101,8 @@ In respect that the structure of the sufficient amount poses problems and challe private TextLayout MakeLayout(string str) { - var wrapping = UseWrapping ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; - var trimming = UseTrimming ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var wrapping = Wrap ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = Trim ? TextTrimming.CharacterEllipsis : TextTrimming.None; var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, textTrimming: trimming, textWrapping: wrapping); layout.Dispose(); From 86b61050a0ec49fdefe225c79e01ea67af4deb0a Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Thu, 19 Jan 2023 19:47:35 +0100 Subject: [PATCH 056/326] Use DBusVariantItem to manually encode/decode variants Note: Tray-Icon submenus don't work, needs investigation --- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 13 +-- src/Avalonia.FreeDesktop/DBusInterfaces.cs | 3 +- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 72 +++++++------- .../DBusPlatformSettings.cs | 85 ++++++++-------- src/Avalonia.FreeDesktop/DBusSettings.cs | 16 --- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 74 +++++++------- .../org.freedesktop.portal.Settings.xml | 99 +++++++++++++++++++ src/Linux/Tmds.DBus | 2 +- src/tools/Tmds.DBus.SourceGenerator | 2 +- 9 files changed, 226 insertions(+), 140 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/DBusSettings.cs create mode 100644 src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index 57bb10a885..59e9ecd1cf 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -4,6 +4,8 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; + namespace Avalonia.FreeDesktop.DBusIme.IBus { @@ -49,18 +51,13 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus }); } - private void OnCommitText(Exception? e, object wtf) + private void OnCommitText(Exception? e, DBusVariantItem variantItem) { if (e is not null) return; - // Hello darkness, my old friend - if (wtf.GetType().GetField("Item3") is { } prop) - { - var text = prop.GetValue(wtf) as string; - if (!string.IsNullOrEmpty(text)) - FireCommit(text!); - } + if (variantItem.Value is DBusStructItem { Count: >= 3 } structItem && structItem[2] is DBusStringItem stringItem) + FireCommit(stringItem.Value); } protected override Task DisconnectAsync() => _service?.DestroyAsync() ?? Task.CompletedTask; diff --git a/src/Avalonia.FreeDesktop/DBusInterfaces.cs b/src/Avalonia.FreeDesktop/DBusInterfaces.cs index 3bece27ab1..fb2f0ca2f3 100644 --- a/src/Avalonia.FreeDesktop/DBusInterfaces.cs +++ b/src/Avalonia.FreeDesktop/DBusInterfaces.cs @@ -9,9 +9,10 @@ namespace Avalonia.FreeDesktop [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod.xml")] [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputContext1.xml")] [DBusInterface("./DBusXml/org.fcitx.Fcitx.InputMethod1.xml")] + [DBusInterface("./DBusXml/org.freedesktop.IBus.Portal.xml")] [DBusInterface("./DBusXml/org.freedesktop.portal.FileChooser.xml")] [DBusInterface("./DBusXml/org.freedesktop.portal.Request.xml")] - [DBusInterface("./DBusXml/org.freedesktop.IBus.Portal.xml")] + [DBusInterface("./DBusXml/org.freedesktop.portal.Settings.xml")] [DBusHandler("./DBusXml/DBusMenu.xml")] [DBusHandler("./DBusXml/StatusNotifierItem.xml")] internal class DBusInterfaces { } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index cb6def056f..1978dfba81 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -9,8 +9,7 @@ using Avalonia.Input; using Avalonia.Platform; using Avalonia.Threading; using Tmds.DBus.Protocol; - -#pragma warning disable 1998 +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { @@ -60,7 +59,7 @@ namespace Avalonia.FreeDesktop public override string Path { get; } - protected override (uint revision, (int, Dictionary, object[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) + protected override (uint revision, (int, Dictionary, DBusVariantItem[]) layout) OnGetLayout(int parentId, int recursionDepth, string[] propertyNames) { var menu = GetMenu(parentId); var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); @@ -73,15 +72,15 @@ namespace Avalonia.FreeDesktop return (_revision, layout); } - protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => + protected override (int, Dictionary)[] OnGetGroupProperties(int[] ids, string[] propertyNames) => ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray(); - protected override object OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? 0; + protected override DBusVariantItem OnGetProperty(int id, string name) => GetProperty(GetMenu(id), name) ?? new DBusVariantItem("i", new DBusInt32Item(0)); - protected override void OnEvent(int id, string eventId, object data, uint timestamp) => + protected override void OnEvent(int id, string eventId, DBusVariantItem data, uint timestamp) => Dispatcher.UIThread.Post(() => HandleEvent(id, eventId)); - protected override int[] OnEventGroup((int, string, object, uint)[] events) + protected override int[] OnEventGroup((int, string, DBusVariantItem, uint)[] events) { foreach (var e in events) Dispatcher.UIThread.Post(() => HandleEvent(e.Item1, e.Item2)); @@ -115,14 +114,14 @@ namespace Avalonia.FreeDesktop if (_disposed) return; _disposed = true; - // Fire and forget - _registrar?.UnregisterWindowAsync(_xid); + _ = _registrar?.UnregisterWindowAsync(_xid); } public bool IsNativeMenuExported { get; private set; } + public event EventHandler? OnIsNativeMenuExportedChanged; public void SetNativeMenu(NativeMenu? menu) @@ -203,31 +202,31 @@ namespace Avalonia.FreeDesktop QueueReset(); } - private static readonly string[] AllProperties = { + private static readonly string[] s_allProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - private object? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + private DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; if (it is NativeMenuItemSeparator) { if (name == "type") - return "separator"; + return new DBusVariantItem("s", new DBusStringItem("separator")); } else if (it is NativeMenuItem item) { if (name == "type") return null; if (name == "label") - return item.Header ?? ""; + return new DBusVariantItem("s", new DBusStringItem(item.Header ?? "")); if (name == "enabled") { if (item.Menu is not null && item.Menu.Items.Count == 0) - return false; + return new DBusVariantItem("b", new DBusBoolItem(false)); if (!item.IsEnabled) - return false; + return new DBusVariantItem("b", new DBusBoolItem(false)); return null; } if (name == "shortcut") @@ -236,30 +235,30 @@ namespace Avalonia.FreeDesktop return null; if (item.Gesture.KeyModifiers == 0) return null; - var lst = new List(); + var lst = new List(); var mod = item.Gesture; if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control)) - lst.Add("Control"); + lst.Add(new DBusStringItem("Control")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Alt)) - lst.Add("Alt"); + lst.Add(new DBusStringItem("Alt")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Shift)) - lst.Add("Shift"); + lst.Add(new DBusStringItem("Shift")); if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) - lst.Add("Super"); - lst.Add(item.Gesture.Key.ToString()); - return new[] { lst.ToArray() }; + lst.Add(new DBusStringItem("Super")); + lst.Add(new DBusStringItem(item.Gesture.Key.ToString())); + return new DBusVariantItem("aas", new DBusArrayItem(DBusType.Array, new[] { new DBusArrayItem(DBusType.String, lst) })); } if (name == "toggle-type") { if (item.ToggleType == NativeMenuItemToggleType.CheckBox) - return "checkmark"; + return new DBusVariantItem("s", new DBusStringItem("checkmark")); if (item.ToggleType == NativeMenuItemToggleType.Radio) - return "radio"; + return new DBusVariantItem("s", new DBusStringItem("radio")); } if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) - return item.IsChecked ? 1 : 0; + return new DBusVariantItem("i", new DBusInt32Item(item.IsChecked ? 1 : 0)); if (name == "icon-data") { @@ -273,23 +272,24 @@ namespace Avalonia.FreeDesktop using var ms = new MemoryStream(); icon.Save(ms); - return ms.ToArray(); + return new DBusVariantItem("ay", + new DBusArrayItem(DBusType.Byte, ms.ToArray().Select(static x => new DBusByteItem(x)))); } } } if (name == "children-display") - return menu is not null ? "submenu" : null; + return menu is not null ? new DBusVariantItem("s", new DBusStringItem("submenu")) : null; } return null; } - private Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names.Length == 0) - names = AllProperties; - var properties = new Dictionary(); + names = s_allProperties; + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); @@ -300,17 +300,23 @@ namespace Avalonia.FreeDesktop return properties; } - private (int, Dictionary, object[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) + private (int, Dictionary, DBusVariantItem[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { var id = item is null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); - var children = depth == 0 || menu is null ? Array.Empty() : new object[menu.Items.Count]; + var children = depth == 0 || menu is null ? Array.Empty() : new DBusVariantItem[menu.Items.Count]; if (menu is not null) { for (var c = 0; c < children.Length; c++) { var ch = menu.Items[c]; - children[c] = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); + children[c] = new DBusVariantItem("(ia{sv}av)", new DBusStructItem(new DBusItem[] + { + new DBusInt32Item(layout.Item1), + new DBusArrayItem(DBusType.DictEntry, layout.Item2.Select(static x => new DBusDictEntryItem(new DBusStringItem(x.Key), x.Value))), + new DBusArrayItem(DBusType.Variant, layout.Item3) + })); } } diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 039fc7c088..a25bb68458 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -2,44 +2,35 @@ using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform; +using Tmds.DBus.SourceGenerator; -namespace Avalonia.FreeDesktop; - -internal class DBusPlatformSettings : DefaultPlatformSettings +namespace Avalonia.FreeDesktop { - private readonly IDBusSettings? _settings; - private PlatformColorValues? _lastColorValues; - - public DBusPlatformSettings() + internal class DBusPlatformSettings : DefaultPlatformSettings { - _settings = DBusHelper.TryInitialize()? - .CreateProxy("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; - if (_settings is not null) + public DBusPlatformSettings() { - _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + if (DBusHelper.Connection is null) + return; - _ = TryGetInitialValue(); + _settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); + _ = TryGetInitialValueAsync(); } - } - - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } - private async Task TryGetInitialValue() - { - var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - if (colorSchemeTask.Status == TaskStatus.RanToCompletion) + public override PlatformColorValues GetColorValues() { - _lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result); + return _lastColorValues ?? base.GetColorValues(); } - else + + private async Task TryGetInitialValueAsync() { try { - var value = await colorSchemeTask; + var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); _lastColorValues = GetColorValuesFromSetting(value); OnColorValuesChanged(_lastColorValues); } @@ -49,29 +40,31 @@ internal class DBusPlatformSettings : DefaultPlatformSettings Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); } } - } - - private void SettingsChangedHandler((string @namespace, string key, object value) tuple) - { - if (tuple.@namespace == "org.freedesktop.appearance" - && tuple.key == "color-scheme") + + private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(tuple.value); - OnColorValuesChanged(_lastColorValues); + if (exception is not null) + return; + + if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _lastColorValues = GetColorValuesFromSetting(value); + OnColorValuesChanged(_lastColorValues); + } } - } - - private static PlatformColorValues GetColorValuesFromSetting(object value) - { - var isDark = value?.ToString() == "1"; - return new PlatformColorValues + + private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; + return new PlatformColorValues + { + ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light + }; + } } } diff --git a/src/Avalonia.FreeDesktop/DBusSettings.cs b/src/Avalonia.FreeDesktop/DBusSettings.cs deleted file mode 100644 index 05911981c7..0000000000 --- a/src/Avalonia.FreeDesktop/DBusSettings.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Tmds.DBus; - -namespace Avalonia.FreeDesktop; - -[DBusInterface("org.freedesktop.portal.Settings")] -internal interface IDBusSettings : IDBusObject -{ - Task<(string @namespace, IDictionary)> ReadAllAsync(string[] namespaces); - - Task ReadAsync(string @namespace, string key); - - Task WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action? onError = null); -} diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 81233f3b2f..763480a33d 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; using Tmds.DBus.Protocol; +using Tmds.DBus.SourceGenerator; namespace Avalonia.FreeDesktop { @@ -44,12 +44,12 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeFilter); - if (filters.Any()) + if (filters is not null) chooserOptions.Add("filters", filters); - chooserOptions.Add("multiple", options.AllowMultiple); + chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple))); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); @@ -58,9 +58,8 @@ namespace Avalonia.FreeDesktop using var disposable = await request.WatchResponseAsync((e, x) => { if (e is not null) - tsc.TrySetException(e); - else - tsc.TrySetResult(x.results["uris"] as string[]); + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); }); var uris = await tsc.Task ?? Array.Empty(); @@ -71,15 +70,15 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); var filters = ParseFilters(options.FileTypeChoices); - if (filters.Any()) + if (filters is not null) chooserOptions.Add("filters", filters); if (options.SuggestedFileName is { } currentName) - chooserOptions.Add("current_name", currentName); + chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName))); if (options.SuggestedStartLocation?.TryGetUri(out var currentFolder) == true) - chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(currentFolder.ToString())); + chooserOptions.Add("current_folder", new DBusVariantItem("s", new DBusStringItem(currentFolder.ToString()))); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); @@ -87,9 +86,8 @@ namespace Avalonia.FreeDesktop using var disposable = await request.WatchResponseAsync((e, x) => { if (e is not null) - tsc.TrySetException(e); - else - tsc.TrySetResult(x.results["uris"] as string[]); + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); }); var uris = await tsc.Task; @@ -106,10 +104,10 @@ namespace Avalonia.FreeDesktop public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { var parentWindow = $"x11:{_handle.Handle:X}"; - var chooserOptions = new Dictionary + var chooserOptions = new Dictionary { - { "directory", true }, - { "multiple", options.AllowMultiple } + { "directory", new DBusVariantItem("b", new DBusBoolItem(true)) }, + { "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) } }; var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); @@ -118,9 +116,8 @@ namespace Avalonia.FreeDesktop using var disposable = await request.WatchResponseAsync((e, x) => { if (e is not null) - tsc.TrySetException(e); - else - tsc.TrySetResult(x.results["uris"] as string[]); + return; + tsc.TrySetResult((x.results["uris"].Value as DBusArrayItem)?.Select(static y => (y as DBusStringItem)!.Value).ToArray()); }); var uris = await tsc.Task ?? Array.Empty(); @@ -131,29 +128,38 @@ namespace Avalonia.FreeDesktop .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); } - private static (string name, (uint style, string extension)[])[] ParseFilters(IReadOnlyList? fileTypes) + private static DBusVariantItem? ParseFilters(IReadOnlyList? fileTypes) { + const uint GlobStyle = 0u; + const uint MimeStyle = 1u; + // Example: [('Images', [(0, '*.ico'), (1, 'image/png')]), ('Text', [(0, '*.txt')])] if (fileTypes is null) - return Array.Empty<(string name, (uint style, string extension)[])>(); + return null; + + var any = false; + var filters = new DBusArrayItem(DBusType.Struct, new List()); - var filters = new List<(string name, (uint style, string extension)[])>(); foreach (var fileType in fileTypes) { - const uint GlobStyle = 0u; - const uint MimeStyle = 1u; - - var extensions = Enumerable.Empty<(uint, string)>(); + var extensions = new List(); + if (fileType.Patterns?.Count > 0) + extensions.AddRange( + fileType.Patterns.Select(static pattern => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(GlobStyle), new DBusStringItem(pattern) }))); + else if (fileType.MimeTypes?.Count > 0) + extensions.AddRange( + fileType.MimeTypes.Select(static mimeType => + new DBusStructItem(new DBusItem[] { new DBusUInt32Item(MimeStyle), new DBusStringItem(mimeType) }))); + else + continue; - if (fileType.Patterns is not null) - extensions = extensions.Concat(fileType.Patterns.Select(static x => (globStyle: GlobStyle, x))); - else if (fileType.MimeTypes is not null) - extensions = extensions.Concat(fileType.MimeTypes.Select(static x => (mimeStyle: MimeStyle, x))); - if (extensions.Any()) - filters.Add((fileType.Name, extensions.ToArray())); + any = true; + filters.Add(new DBusStructItem( + new DBusItem[] { new DBusStringItem(fileType.Name), new DBusArrayItem(DBusType.Struct, extensions) })); } - return filters.ToArray(); + return any ? new DBusVariantItem("a(sa(us))", filters) : null; } } } diff --git a/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml new file mode 100644 index 0000000000..669997a3df --- /dev/null +++ b/src/Avalonia.FreeDesktop/DBusXml/org.freedesktop.portal.Settings.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Linux/Tmds.DBus b/src/Linux/Tmds.DBus index bfca94ab05..e86bcf1bc2 160000 --- a/src/Linux/Tmds.DBus +++ b/src/Linux/Tmds.DBus @@ -1 +1 @@ -Subproject commit bfca94ab052683c7ccef51e4a036098f539cc676 +Subproject commit e86bcf1bc2d86338ab61663a4ae48dbc0bd7e02d diff --git a/src/tools/Tmds.DBus.SourceGenerator b/src/tools/Tmds.DBus.SourceGenerator index 6007f27d04..86e0ded07f 160000 --- a/src/tools/Tmds.DBus.SourceGenerator +++ b/src/tools/Tmds.DBus.SourceGenerator @@ -1 +1 @@ -Subproject commit 6007f27d04691f7c4c35722df3dc65bf4b558f18 +Subproject commit 86e0ded07fc8622e216f93f8fc01e8c1b3ef29b1 From 7a2ca3e9998a3d0f61c57f6044246d0861cc844e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 19 Jan 2023 23:00:51 +0600 Subject: [PATCH 057/326] Build --- samples/GpuInterop/D3DDemo/D3D11Swapchain.cs | 9 --------- samples/GpuInterop/GpuInterop.csproj | 1 + samples/GpuInterop/Program.cs | 4 ++-- src/Avalonia.Base/Rendering/SwapchainBase.cs | 3 +-- src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs | 3 +-- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs index 30a4c19d35..d2cf43ac74 100644 --- a/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs +++ b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs @@ -1,23 +1,14 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using SharpDX.Direct2D1; using SharpDX.Direct3D11; using SharpDX.DXGI; -using SharpDX.Mathematics.Interop; -using Buffer = SharpDX.Direct3D11.Buffer; -using DeviceContext = SharpDX.Direct2D1.DeviceContext; using DxgiFactory1 = SharpDX.DXGI.Factory1; -using Matrix = SharpDX.Matrix; using D3DDevice = SharpDX.Direct3D11.Device; using DxgiResource = SharpDX.DXGI.Resource; -using FeatureLevel = SharpDX.Direct3D.FeatureLevel; namespace GpuInterop.D3DDemo; diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj index c201d9bf85..88e6d3d283 100644 --- a/samples/GpuInterop/GpuInterop.csproj +++ b/samples/GpuInterop/GpuInterop.csproj @@ -28,6 +28,7 @@ + diff --git a/samples/GpuInterop/Program.cs b/samples/GpuInterop/Program.cs index 86fd239b4c..8d7ccf4866 100644 --- a/samples/GpuInterop/Program.cs +++ b/samples/GpuInterop/Program.cs @@ -1,5 +1,5 @@ -using Avalonia; - +global using System.Reactive.Disposables; +using Avalonia; namespace GpuInterop { public class Program diff --git a/src/Avalonia.Base/Rendering/SwapchainBase.cs b/src/Avalonia.Base/Rendering/SwapchainBase.cs index ccfb704f00..5d0bba2341 100644 --- a/src/Avalonia.Base/Rendering/SwapchainBase.cs +++ b/src/Avalonia.Base/Rendering/SwapchainBase.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading.Tasks; -using Avalonia; +using Avalonia.Reactive; using Avalonia.Rendering.Composition; namespace Avalonia.Rendering; diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs index 81270bb642..bda2bd1568 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs @@ -1,10 +1,9 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Rendering.Composition; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.OpenGL.Controls; From a4665747e3f6aa3f31784d6adddf358e1b84f8a5 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Thu, 19 Jan 2023 23:31:41 +0100 Subject: [PATCH 058/326] Don't dispose DBus connection in tray icon --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 7 ++++--- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 1978dfba81..6f1810d1c1 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -43,7 +44,7 @@ namespace Avalonia.FreeDesktop _xid = (uint)xid.ToInt32(); Path = GenerateDBusMenuObjPath; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } public DBusMenuExporterImpl(Connection connection, string path) @@ -52,7 +53,7 @@ namespace Avalonia.FreeDesktop _appMenu = false; Path = path; SetNativeMenu(new NativeMenu()); - Init(); + _ = InitializeAsync(); } protected override Connection Connection { get; } @@ -92,7 +93,7 @@ namespace Avalonia.FreeDesktop protected override (int[] updatesNeeded, int[] idErrors) OnAboutToShowGroup(int[] ids) => (Array.Empty(), Array.Empty()); - private async void Init() + private async Task InitializeAsync() { Connection.AddMethodHandler(this); if (!_appMenu) diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index a34a9a0a84..f732cfef07 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -134,7 +134,6 @@ namespace Avalonia.FreeDesktop IsActive = false; _isDisposed = true; DestroyTrayIcon(); - _connection?.Dispose(); _serviceWatchDisposable?.Dispose(); } From 32cfbe6578f2d4952eed49ddd3d6074d3f34895f Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 20 Jan 2023 15:30:27 +0800 Subject: [PATCH 059/326] fix: fix screen display in control catalog. --- samples/ControlCatalog/Pages/ScreenPage.cs | 82 +++++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b5b80fb147..94fc4da7ef 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using System.Linq; +using System.Net.Http.Headers; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -12,6 +14,11 @@ namespace ControlCatalog.Pages public class ScreenPage : UserControl { private double _leftMost; + private double _topMost; + private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7"); + private IBrush _defaultBrush = Brushes.LightGray; + private IPen _activePen = new Pen(Brushes.Black); + private IPen _defaultPen = new Pen(Brushes.DarkGray); protected override bool BypassFlowDirectionPolicies => true; @@ -37,51 +44,88 @@ namespace ControlCatalog.Pages var drawBrush = Brushes.Black; Pen p = new Pen(drawBrush); - foreach (Screen screen in screens) + var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))); + double maxBottom = 0; + + for (int i = 0; i Date: Fri, 20 Jan 2023 15:47:57 +0800 Subject: [PATCH 060/326] fix: remove unused pen. --- samples/ControlCatalog/Pages/ScreenPage.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index 94fc4da7ef..2cdd031693 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -41,9 +41,6 @@ namespace ControlCatalog.Pages var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; - var drawBrush = Brushes.Black; - Pen p = new Pen(drawBrush); - var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))); double maxBottom = 0; @@ -114,7 +111,7 @@ namespace ControlCatalog.Pages } - context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f+Math.Abs(_topMost), w.Bounds.Width / 10, w.Bounds.Height / 10)); + context.DrawRectangle(_activePen, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f+Math.Abs(_topMost), w.Bounds.Width / 10, w.Bounds.Height / 10)); } private static FormattedText CreateFormattedText(string textToFormat, double size = 12) From 1cc29c7e8d24c0d326c741f0488c92b95ac6b718 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 20 Jan 2023 09:35:37 +0000 Subject: [PATCH 061/326] set pull and pich gestures to handled when handled by handler --- samples/ControlCatalog/Pages/GesturePage.cs | 35 ++++++++++++------- .../PinchGestureRecognizer.cs | 5 ++- .../PullGestureRecognizer.cs | 6 +++- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs index ee10f21317..0bb8f38219 100644 --- a/samples/ControlCatalog/Pages/GesturePage.cs +++ b/samples/ControlCatalog/Pages/GesturePage.cs @@ -6,6 +6,7 @@ using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.Rendering.Composition; +using Avalonia.Utilities; namespace ControlCatalog.Pages { @@ -53,6 +54,7 @@ namespace ControlCatalog.Pages { _currentScale = 1; compositionVisual.Scale = new Vector3(1,1,1); + compositionVisual.Offset = default; image.InvalidateMeasure(); } }; @@ -100,13 +102,19 @@ namespace ControlCatalog.Pages { InitComposition(control!); - isZooming = true; - if(compositionVisual != null) { var scale = _currentScale * (float)e.Scale; + if (scale <= 1) + { + scale = 1; + compositionVisual.Offset = default; + } + compositionVisual.Scale = new(scale, scale, 1); + + e.Handled = true; } }); @@ -114,8 +122,6 @@ namespace ControlCatalog.Pages { InitComposition(control!); - isZooming = false; - if (compositionVisual != null) { _currentScale = compositionVisual.Scale.X; @@ -126,11 +132,19 @@ namespace ControlCatalog.Pages { InitComposition(control!); - if (compositionVisual != null && !isZooming) + if (compositionVisual != null && _currentScale != 1) { - currentOffset -= new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0); + currentOffset += new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0); + + var currentSize = control.Bounds.Size * _currentScale; + + currentOffset = new Vector3((float)MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width), + (float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height), + 0); - compositionVisual.Offset = currentOffset; + compositionVisual.Offset = currentOffset * -1; + + e.Handled = true; } }); } @@ -173,6 +187,8 @@ namespace ControlCatalog.Pages if (ballCompositionVisual != null) { ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1); + + e.Handled = true; } }); @@ -187,11 +203,6 @@ namespace ControlCatalog.Pages void InitComposition(Control control) { - if (ballCompositionVisual != null) - { - return; - } - ballCompositionVisual = ElementComposition.GetElementVisual(ball); if (ballCompositionVisual != null) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs index eea7c3b7d1..3b83d0cb87 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs @@ -57,7 +57,10 @@ namespace Avalonia.Input var scale = distance / _initialDistance; - _target?.RaiseEvent(new PinchEventArgs(scale, _origin)); + var pinchEventArgs = new PinchEventArgs(scale, _origin); + _target?.RaiseEvent(pinchEventArgs); + + e.Handled = pinchEventArgs.Handled; } } } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs index 23bab13fc8..991694cc60 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Avalonia.Input.GestureRecognizers; namespace Avalonia.Input @@ -88,7 +89,10 @@ namespace Avalonia.Input } _pullInProgress = true; - _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection)); + var pullEventArgs = new PullGestureEventArgs(_gestureId, delta, PullDirection); + _target?.RaiseEvent(pullEventArgs); + + e.Handled = pullEventArgs.Handled; } } From 7ae394d247906b93b1347616d55bee4ac5df5890 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 13:00:39 +0100 Subject: [PATCH 062/326] Fixed text layout buffer reset for Stack and Dictionary (+ tests) --- .../TextFormatting/FormattingBufferHelper.cs | 34 +++- .../Media/TextFormatting/Unicode/BiDiData.cs | 4 + .../FormattingBufferHelperTests.cs | 151 ++++++++++++++++++ 3 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs index 0341842cb6..c27903cd55 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Numerics; using System.Runtime.CompilerServices; using Avalonia.Utilities; @@ -13,7 +14,7 @@ namespace Avalonia.Media.TextFormatting { arrayBuilder.Clear(); - if (IsBufferTooLarge(arrayBuilder.Capacity)) + if (IsBufferTooLarge((uint) arrayBuilder.Capacity)) { arrayBuilder = default; } @@ -23,7 +24,7 @@ namespace Avalonia.Media.TextFormatting { list.Clear(); - if (IsBufferTooLarge(list.Capacity)) + if (IsBufferTooLarge((uint) list.Capacity)) { list.TrimExcess(); } @@ -31,9 +32,11 @@ namespace Avalonia.Media.TextFormatting public static void ClearThenResetIfTooLarge(Stack stack) { + var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count); + stack.Clear(); - if (IsBufferTooLarge(stack.Count)) + if (IsBufferTooLarge(approximateCapacity)) { stack.TrimExcess(); } @@ -42,10 +45,12 @@ namespace Avalonia.Media.TextFormatting public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) where TKey : notnull { + var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count); + dictionary.Clear(); // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here - if (IsBufferTooLarge>(dictionary.Count)) + if (IsBufferTooLarge>(approximateCapacity)) { #if NET6_0_OR_GREATER dictionary.TrimExcess(); @@ -56,7 +61,24 @@ namespace Avalonia.Media.TextFormatting } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsBufferTooLarge(int length) - => (long)Unsafe.SizeOf() * length > MaxKeptBufferSizeInBytes; + private static bool IsBufferTooLarge(uint capacity) + => (long) (uint) Unsafe.SizeOf() * capacity > MaxKeptBufferSizeInBytes; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RoundUpToPowerOf2(uint value) + { +#if NET6_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 8bd2171a41..5cc222b813 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -150,6 +150,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void SaveTypes() { + _hasCleanState = false; + // Capture the types data _savedClasses.Clear(); _savedClasses.Add(_classes.AsSlice()); @@ -162,6 +164,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void RestoreTypes() { + _hasCleanState = false; + _classes.Clear(); _classes.Add(_savedClasses.AsSlice()); _pairedBracketTypes.Clear(); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs new file mode 100644 index 0000000000..192f34eea7 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.TextFormatting +{ + public class FormattingBufferHelperTests + { + public static TheoryData SmallSizes => new() { 1, 500, 10_000, 125_000 }; + public static TheoryData LargeSizes => new() { 500_000, 1_000_000 }; + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearList(int itemCount) + { + var list = new List(); + + for (var i = 0; i < itemCount; ++i) + { + list.Add(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(list); + + return list.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearArrayBuilder(int itemCount) + { + var arrayBuilder = new ArrayBuilder(); + + for (var i = 0; i < itemCount; ++i) + { + arrayBuilder.AddItem(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref arrayBuilder); + + return arrayBuilder.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearStack(int itemCount) + { + var stack = new Stack(); + + for (var i = 0; i < itemCount; ++i) + { + stack.Push(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(stack); + + var array = (Array) stack.GetType() + .GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(stack)!; + + return array.Length; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity <= 3); // dictionary trims to the nearest prime starting with 3 + } + + private static int FillAndClearDictionary(int itemCount) + { + var dictionary = new Dictionary(); + + for (var i = 0; i < itemCount; ++i) + { + dictionary.Add(i, i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref dictionary); + + var array = (Array) dictionary.GetType() + .GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(dictionary)!; + + return array.Length; + } + } +} From b1b63cba2abcd10776f6141921a7cf29c8cb7c24 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 20 Jan 2023 12:54:22 +0000 Subject: [PATCH 063/326] wip --- src/Avalonia.Controls/ItemsControl.cs | 60 ++++++++++++++++- .../Presenters/ItemsPresenter.cs | 67 ++++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index d19a04eb21..1948fda928 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -12,6 +12,8 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Styling; @@ -23,7 +25,7 @@ namespace Avalonia.Controls /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IChildIndexProvider + public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo { /// /// The default value for the property. @@ -91,6 +93,7 @@ namespace Avalonia.Controls private IDataTemplate? _displayMemberItemTemplate; private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; + private ItemsPresenter? _itemsPresenter; /// /// Initializes a new instance of the class. @@ -203,6 +206,45 @@ namespace Avalonia.Controls remove => _childIndexChanged -= value; } + + public event EventHandler HorizontalSnapPointsChanged + { + add + { + if (_itemsPresenter != null) + { + _itemsPresenter.HorizontalSnapPointsChanged += value; + } + } + + remove + { + if (_itemsPresenter != null) + { + _itemsPresenter.HorizontalSnapPointsChanged -= value; + } + } + } + + public event EventHandler VerticalSnapPointsChanged + { + add + { + if (_itemsPresenter != null) + { + _itemsPresenter.VerticalSnapPointsChanged += value; + } + } + + remove + { + if (_itemsPresenter != null) + { + _itemsPresenter.VerticalSnapPointsChanged -= value; + } + } + } + /// /// Returns the container for the item at the specified index. /// @@ -254,7 +296,8 @@ namespace Avalonia.Controls /// Gets the currently realized containers. /// public IEnumerable GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty(); - + public bool AreHorizontalSnapPointsRegular => _itemsPresenter?.AreHorizontalSnapPointsRegular ?? false; + public bool AreVerticalSnapPointsRegular => _itemsPresenter?.AreVerticalSnapPointsRegular ?? false; /// /// Creates or a container that can be used to display an item. @@ -355,6 +398,7 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); + _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } /// @@ -671,5 +715,17 @@ namespace Avalonia.Controls count = ItemsView.Count; return true; } + + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List(); + } + + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + offset = 0; + + return _itemsPresenter?.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset) ?? 0; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e0252feed5..08de6bf5a8 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -3,13 +3,15 @@ using System.Collections.Generic; using System.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { /// /// Presents items inside an . /// - public class ItemsPresenter : Control, ILogicalScrollable + public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo { /// /// Defines the property. @@ -21,6 +23,44 @@ namespace Avalonia.Controls.Presenters private ILogicalScrollable? _logicalScrollable; private EventHandler? _scrollInvalidated; + public event EventHandler HorizontalSnapPointsChanged + { + add + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.HorizontalSnapPointsChanged += value; + } + } + + remove + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.HorizontalSnapPointsChanged -= value; + } + } + } + + public event EventHandler VerticalSnapPointsChanged + { + add + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged += value; + } + } + + remove + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged -= value; + } + } + } + static ItemsPresenter() { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( @@ -89,6 +129,9 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; + public bool AreHorizontalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreHorizontalSnapPointsRegular : false; + public bool AreVerticalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreVerticalSnapPointsRegular : false; + public override sealed void ApplyTemplate() { if (Panel is null && ItemsControl is not null) @@ -204,5 +247,27 @@ namespace Avalonia.Controls.Presenters } private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e); + + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + return scrollSnapPointsInfo.GetIrregularSnapPoints(orientation, snapPointsAlignment); + } + + return new List(); + } + + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + return scrollSnapPointsInfo.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset); + } + + offset = 0; + + return 0; + } } } From b0ebd5e1d1d35ded49d2a12c7f89d39175590cf9 Mon Sep 17 00:00:00 2001 From: cristinathoughtpennies Date: Fri, 20 Jan 2023 09:41:10 -0600 Subject: [PATCH 064/326] Update HeaderDoubleTapped functions to match conventions --- src/Avalonia.Controls/TreeViewItem.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 532d0e32db..022e1a74b1 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -265,7 +265,10 @@ namespace Avalonia.Controls } } - protected virtual void HeaderDoubleTapped(object? sender, TappedEventArgs e) + /// + /// Invoked when the event occurs in the header. + /// + protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) { if (ItemCount > 0) { @@ -286,5 +289,10 @@ namespace Avalonia.Controls return logical != null ? result : @default; } + + private void HeaderDoubleTapped(object? sender, TappedEventArgs e) + { + OnHeaderDoubleTapped(e); + } } } From e6c60ddfef1a23b5225056bab610758afdd98ff0 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 20 Jan 2023 17:52:55 +0000 Subject: [PATCH 065/326] add regular snap points to virutalizing panel --- src/Avalonia.Controls/ListBox.cs | 5 +- .../Presenters/ItemsPresenter.cs | 83 ++++---- src/Avalonia.Controls/ScrollViewer.cs | 8 +- src/Avalonia.Controls/StackPanel.cs | 4 +- .../VirtualizingStackPanel.cs | 190 +++++++++++++++++- .../Controls/ListBox.xaml | 3 + 6 files changed, 250 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 8b1a307182..775e0ae0cb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -21,7 +21,10 @@ namespace Avalonia.Controls /// The default value for the property. /// private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new VirtualizingStackPanel()); + new FuncTemplate(() => new VirtualizingStackPanel() + { + AreVerticalSnapPointsRegular= true, + }); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 08de6bf5a8..e3332ef3a2 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -23,43 +23,21 @@ namespace Avalonia.Controls.Presenters private ILogicalScrollable? _logicalScrollable; private EventHandler? _scrollInvalidated; - public event EventHandler HorizontalSnapPointsChanged - { - add - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.HorizontalSnapPointsChanged += value; - } - } - - remove - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.HorizontalSnapPointsChanged -= value; - } - } - } - - public event EventHandler VerticalSnapPointsChanged - { - add - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.VerticalSnapPointsChanged += value; - } - } + /// + /// Defines the event. + /// + public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(HorizontalSnapPointsChanged), + RoutingStrategies.Bubble); - remove - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.VerticalSnapPointsChanged -= value; - } - } - } + /// + /// Defines the event. + /// + public static readonly RoutedEvent VerticalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(VerticalSnapPointsChanged), + RoutingStrategies.Bubble); static ItemsPresenter() { @@ -123,6 +101,24 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Occurs when the measurements for horizontal snap points change. + /// + public event EventHandler? HorizontalSnapPointsChanged + { + add => AddHandler(HorizontalSnapPointsChangedEvent, value); + remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); + } + + /// + /// Occurs when the measurements for vertical snap points change. + /// + public event EventHandler? VerticalSnapPointsChanged + { + add => AddHandler(VerticalSnapPointsChangedEvent, value); + remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); + } + bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false; Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default; Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default; @@ -151,6 +147,21 @@ namespace Avalonia.Controls.Presenters else CreateSimplePanelGenerator(); + if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged += (s, e) => + { + e.RoutedEvent = VerticalSnapPointsChangedEvent; + RaiseEvent(e); + }; + + scrollSnapPointsInfo.HorizontalSnapPointsChanged += (s, e) => + { + e.RoutedEvent = HorizontalSnapPointsChangedEvent; + RaiseEvent(e); + }; + } + _logicalScrollable = Panel as ILogicalScrollable; if (_logicalScrollable is not null) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index fb40006b4c..1340de3af9 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -168,15 +168,15 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty HorizontalSnapPointsAlignmentProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty HorizontalSnapPointsAlignmentProperty = + AvaloniaProperty.RegisterAttached( nameof(HorizontalSnapPointsAlignment)); /// /// Defines the property. /// - public static readonly StyledProperty VerticalSnapPointsAlignmentProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty VerticalSnapPointsAlignmentProperty = + AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsAlignment)); /// diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index b9524a515f..aa63ac975e 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -356,6 +356,7 @@ namespace Avalonia.Controls child.Arrange(rect); } + /// public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) { var snapPoints = new List(); @@ -419,6 +420,7 @@ namespace Avalonia.Controls return snapPoints; } + /// public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) { offset = 0f; @@ -470,7 +472,7 @@ namespace Avalonia.Controls break; } - return snapPoint; + return snapPoint + Spacing; } } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 3f539ce198..f60c67b577 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; +using System.Reflection; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -14,7 +17,7 @@ namespace Avalonia.Controls /// /// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically. /// - public class VirtualizingStackPanel : VirtualizingPanel + public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo { /// /// Defines the property. @@ -22,6 +25,34 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = StackLayout.OrientationProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(HorizontalSnapPointsChanged), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent VerticalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(VerticalSnapPointsChanged), + RoutingStrategies.Bubble); + private static readonly AttachedProperty ItemIsOwnContainerProperty = AvaloniaProperty.RegisterAttached("ItemIsOwnContainer"); @@ -62,6 +93,42 @@ namespace Avalonia.Controls set => SetValue(OrientationProperty, value); } + /// + /// Occurs when the measurements for horizontal snap points change. + /// + public event EventHandler? HorizontalSnapPointsChanged + { + add => AddHandler(HorizontalSnapPointsChangedEvent, value); + remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); + } + + /// + /// Occurs when the measurements for vertical snap points change. + /// + public event EventHandler? VerticalSnapPointsChanged + { + add => AddHandler(VerticalSnapPointsChangedEvent, value); + remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); + } + + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } + protected override Size MeasureOverride(Size availableSize) { if (!IsEffectivelyVisible) @@ -145,6 +212,8 @@ namespace Avalonia.Controls finally { _isInLayout = false; + + RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent)); } } @@ -622,6 +691,125 @@ namespace Avalonia.Controls Invalidate(c); } + /// + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + var snapPoints = new List(); + + switch (orientation) + { + case Orientation.Horizontal: + if (AreHorizontalSnapPointsRegular) + throw new InvalidOperationException(); + if (Orientation == Orientation.Horizontal) + { + foreach (var child in VisualChildren) + { + double snapPoint = 0; + + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = child.Bounds.Left; + break; + case SnapPointsAlignment.Center: + snapPoint = child.Bounds.Center.X; + break; + case SnapPointsAlignment.Far: + snapPoint = child.Bounds.Right; + break; + } + + snapPoints.Add(snapPoint); + } + } + break; + case Orientation.Vertical: + if (AreVerticalSnapPointsRegular) + throw new InvalidOperationException(); + if (Orientation == Orientation.Vertical) + { + foreach (var child in VisualChildren) + { + double snapPoint = 0; + + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = child.Bounds.Top; + break; + case SnapPointsAlignment.Center: + snapPoint = child.Bounds.Center.Y; + break; + case SnapPointsAlignment.Far: + snapPoint = child.Bounds.Bottom; + break; + } + + snapPoints.Add(snapPoint); + } + } + break; + } + + return snapPoints; + } + + /// + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + offset = 0f; + var firstRealizedChild = _realizedElements?.Elements.FirstOrDefault(); + + if (firstRealizedChild == null) + { + return 0; + } + + double snapPoint = 0; + + switch (Orientation) + { + case Orientation.Horizontal: + if (!AreHorizontalSnapPointsRegular) + throw new InvalidOperationException(); + + snapPoint = firstRealizedChild.Bounds.Width; + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + offset = 0; + break; + case SnapPointsAlignment.Center: + offset = (firstRealizedChild.Bounds.Right - firstRealizedChild.Bounds.Left) / 2; + break; + case SnapPointsAlignment.Far: + offset = firstRealizedChild.Bounds.Width; + break; + } + break; + case Orientation.Vertical: + if (!AreVerticalSnapPointsRegular) + throw new InvalidOperationException(); + snapPoint = firstRealizedChild.Bounds.Height; + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + offset = 0; + break; + case SnapPointsAlignment.Center: + offset = (firstRealizedChild.Bounds.Bottom - firstRealizedChild.Bounds.Top) / 2; + break; + case SnapPointsAlignment.Far: + offset = firstRealizedChild.Bounds.Height; + break; + } + break; + } + + return snapPoint; + } + /// /// Stores the realized element state for a . /// diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index ec0b876c71..dc18d65797 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -19,6 +19,7 @@ + @@ -29,6 +30,8 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> Date: Fri, 20 Jan 2023 20:28:54 +0100 Subject: [PATCH 066/326] Use IGlyphRunImpl in the IDrawingContextImpl --- src/Avalonia.Base/Media/DrawingContext.cs | 2 +- src/Avalonia.Base/Media/DrawingGroup.cs | 5 +- src/Avalonia.Base/Media/GlyphRun.cs | 55 ++++++++----------- src/Avalonia.Base/Media/GlyphRunMetrics.cs | 15 ++--- .../Media/ImmediateDrawingContext.cs | 2 +- src/Avalonia.Base/Media/TextDecoration.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 2 +- .../Platform/IDrawingContextImpl.cs | 2 +- src/Avalonia.Base/Platform/IGlyphRunImpl.cs | 19 ++++++- .../Drawing/CompositionDrawingContext.cs | 2 +- .../Composition/Server/DrawingContextProxy.cs | 2 +- .../Composition/Server/FpsCounter.cs | 2 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 2 +- .../Rendering/SceneGraph/GlyphRunNode.cs | 18 ++++-- .../HeadlessPlatformRenderInterface.cs | 8 ++- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 10 ++-- src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 10 +++- .../Avalonia.Skia/PlatformRenderInterface.cs | 14 +++-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 14 ++++- .../Media/DrawingContextImpl.cs | 8 +-- .../Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 12 +++- .../Media/GlyphRunTests.cs | 4 +- .../NullDrawingContextImpl.cs | 2 +- tests/Avalonia.Benchmarks/NullGlyphRun.cs | 4 ++ .../NullRenderingPlatform.cs | 2 +- .../DatePickerTests.cs | 3 +- .../MaskedTextBoxTests.cs | 8 +-- .../Primitives/SelectingItemsControlTests.cs | 4 +- .../TimePickerTests.cs | 3 +- .../Media/GlyphRunTests.cs | 4 +- tests/Avalonia.UnitTests/MockGlyphRun.cs | 11 ++++ .../MockPlatformRenderInterface.cs | 2 +- 32 files changed, 154 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 077816c645..d295111d72 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -246,7 +246,7 @@ namespace Avalonia.Media if (foreground != null) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun); + PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); } } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 7d3b4c056e..481329c20c 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -167,18 +167,17 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { if (foreground == null || glyphRun == null) { return; } - // Add a GlyphRunDrawing to the Drawing graph GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = glyphRun, + GlyphRun = new GlyphRun(glyphRun) }; // Add Drawing to the Drawing graph diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index b637c94d88..65575617d0 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -13,10 +13,9 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { - private IGlyphRunImpl? _glyphRunImpl; + private IRef? _platformImpl; private double _fontRenderingEmSize; private int _biDiLevel; - private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; private ReadOnlyMemory _characters; private IReadOnlyList _glyphInfos; @@ -68,6 +67,13 @@ namespace Avalonia.Media _biDiLevel = biDiLevel; } + internal GlyphRun(IRef platformImpl) + { + _glyphInfos = Array.Empty(); + GlyphTypeface = Typeface.Default.GlyphTypeface; + _platformImpl = platformImpl; + } + private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, double fontRenderingEmSize, IGlyphTypeface glyphTypeface) { @@ -132,7 +138,7 @@ namespace Avalonia.Media /// /// Gets or sets the conservative bounding box of the . /// - public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height); + public Size Size => PlatformImpl.Item.Size; /// /// @@ -141,13 +147,9 @@ namespace Avalonia.Media => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// - /// Gets or sets the baseline origin of the. + /// Gets the baseline origin of the. /// - public Point BaselineOrigin - { - get => _baselineOrigin ??= CalculateBaselineOrigin(); - set => Set(ref _baselineOrigin, value); - } + public Point BaselineOrigin => PlatformImpl.Item.BaselineOrigin; /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . @@ -193,8 +195,8 @@ namespace Avalonia.Media /// /// The platform implementation of the . /// - public IGlyphRunImpl GlyphRunImpl - => _glyphRunImpl ??= CreateGlyphRunImpl(); + public IRef PlatformImpl + => _platformImpl ??= CreateGlyphRunImpl(); /// /// Obtains geometry for the glyph run. @@ -233,7 +235,7 @@ namespace Avalonia.Media if (characterIndex > Metrics.LastCluster) { - return Metrics.WidthIncludingTrailingWhitespace; + return Size.Width; } var glyphIndex = FindGlyphIndex(characterIndex); @@ -607,15 +609,6 @@ namespace Avalonia.Media return new CharacterHit(cluster, clusterLength); } - /// - /// Calculates the default baseline origin of the . - /// - /// The baseline origin. - private Point CalculateBaselineOrigin() - { - return new Point(0, -GlyphTypeface.Metrics.Ascent * Scale); - } - private GlyphRunMetrics CreateGlyphRunMetrics() { int firstCluster, lastCluster; @@ -668,8 +661,6 @@ namespace Avalonia.Media return new GlyphRunMetrics( width, - widthIncludingTrailingWhitespace, - height, trailingWhitespaceLength, newLineLength, firstCluster, @@ -800,28 +791,30 @@ namespace Avalonia.Media private void Set(ref T field, T value) { - _glyphRunImpl?.Dispose(); + _platformImpl?.Dispose(); - _glyphRunImpl = null; + _platformImpl = null; _glyphRunMetrics = null; - _baselineOrigin = null; - field = value; } - private IGlyphRunImpl CreateGlyphRunImpl() + private IRef CreateGlyphRunImpl() { var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); + var platformImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); + + _platformImpl = RefCountable.Create(platformImpl); + + return _platformImpl; } public void Dispose() { - _glyphRunImpl?.Dispose(); - _glyphRunImpl = null; + _platformImpl?.Dispose(); + _platformImpl = null; } } } diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index 492b5214cd..09b183d044 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -2,27 +2,20 @@ { public readonly record struct GlyphRunMetrics { - public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height, - int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) + public GlyphRunMetrics(double width, int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) { Width = width; - WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace; - Height = height; TrailingWhitespaceLength = trailingWhitespaceLength; - NewLineLength= newLineLength; + NewLineLength = newLineLength; FirstCluster = firstCluster; LastCluster = lastCluster; } public double Width { get; } - public double WidthIncludingTrailingWhitespace { get; } - - public double Height { get; } - public int TrailingWhitespaceLength { get; } - - public int NewLineLength { get; } + + public int NewLineLength { get; } public int FirstCluster { get; } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index eb6f105680..7d9534c414 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -182,7 +182,7 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IImmutableBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IImmutableBrush foreground, IRef glyphRun) { _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index d6b2841214..dc9e5cb907 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -218,7 +218,7 @@ namespace Avalonia.Media { var offsetY = glyphRun.BaselineOrigin.Y - origin.Y; - var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); + var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); if (intersections != null && intersections.Count > 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 245104f8fe..260fcaccbe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1320,7 +1320,7 @@ namespace Avalonia.Media.TextFormatting newLineLength = textRun.GlyphRun.Metrics.NewLineLength; } - widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; + widthIncludingWhitespace += textRun.Size.Width; break; } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 6aa5eeea3d..c05c04c22e 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun); + void DrawGlyphRun(IBrush foreground, IRef glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 6a8ae4d954..46b065b04e 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -10,6 +10,23 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - IReadOnlyList GetIntersections(float lowerBound, float upperBound); + + /// + /// Gets the conservative bounding box of the glyph run./>. + /// + Size Size { get; } + + /// + /// Gets the baseline origin of the glyph run./>. + /// + Point BaselineOrigin { get; } + + /// + /// Gets the intersections of specified upper and lower limit. + /// + /// Upper limit. + /// Lower limit. + /// + IReadOnlyList GetIntersections(float lowerLimit, float upperLimit); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index aae1fcb90e..05488a558f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -159,7 +159,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW public object? GetFeature(Type t) => null; /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { var next = NextDrawAs(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index e6bbba6ec0..c58beebe7f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -86,7 +86,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawEllipse(brush, pen, rect); } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { _impl.DrawGlyphRun(foreground, glyphRun); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 32923a5257..ebab39cee8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -72,7 +72,7 @@ internal class FpsCounter { var run = _runs[ch - FirstChar]; context.Transform = Matrix.CreateTranslation(offset, 0); - context.DrawGlyphRun(Brushes.White, run); + context.DrawGlyphRun(Brushes.White, run.PlatformImpl); offset += run.Size.Width; } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index d6766fa9b8..b1d8301557 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -206,7 +206,7 @@ namespace Avalonia.Rendering.SceneGraph public object? GetFeature(Type t) => null; /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { var next = NextDrawAs(); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1d85e95835..a2d914bdd7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Rendering.SceneGraph { @@ -19,13 +20,13 @@ namespace Avalonia.Rendering.SceneGraph public GlyphRunNode( Matrix transform, IBrush foreground, - GlyphRun glyphRun, + IRef glyphRun, IDisposable? aux = null) - : base(new Rect(glyphRun.Size), transform, aux) + : base(new Rect(glyphRun.Item.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); - GlyphRun = glyphRun; + GlyphRun = glyphRun.Clone(); } /// @@ -41,7 +42,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the glyph run to draw. /// - public GlyphRun GlyphRun { get; } + public IRef GlyphRun { get; } /// public override void Render(IDrawingContextImpl context) @@ -61,14 +62,19 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) + internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && Equals(foreground, Foreground) && - Equals(glyphRun, GlyphRun); + Equals(glyphRun.Item, GlyphRun.Item); } /// public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + + public override void Dispose() + { + GlyphRun?.Dispose(); + } } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index e368ddc373..739a648bac 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -126,13 +126,17 @@ namespace Avalonia.Headless class HeadlessGlyphRunStub : IGlyphRunImpl { + public Size Size => new Size(8, 12); + + public Point BaselineOrigin => new Point(0, 8); + public void Dispose() { } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) { - throw new NotImplementedException(); + return null; } } @@ -463,7 +467,7 @@ namespace Avalonia.Headless { } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 8b71f4e17e..dcb20d2a44 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -492,15 +492,15 @@ namespace Avalonia.Skia } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { CheckLease(); - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) { - var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; - Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.BaselineOrigin.X, - (float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint); + Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.Item.BaselineOrigin.X, + (float)glyphRun.Item.BaselineOrigin.Y, paintWrapper.Paint); } } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index dd7ed31a6e..cc669f9aaa 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -11,9 +11,13 @@ namespace Avalonia.Skia [Unstable] public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKTextBlob textBlob) + public GlyphRunImpl(SKTextBlob textBlob, Size size, Point baselineOrigin) { TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); + + Size = size; + + BaselineOrigin = baselineOrigin; } /// @@ -21,6 +25,10 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } + public Size Size { get; } + + public Point BaselineOrigin { get; } + public IReadOnlyList GetIntersections(float upperBound, float lowerBound) => TextBlob.GetIntercepts(lowerBound, upperBound); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 3fb7491898..6630f0707e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -81,7 +81,7 @@ namespace Avalonia.Skia SKPath path = new SKPath(); - var (currentX, currentY) = glyphRun.BaselineOrigin; + var (currentX, currentY) = glyphRun.PlatformImpl.Item.BaselineOrigin; for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { @@ -236,7 +236,7 @@ namespace Avalonia.Skia var glyphSpan = runBuffer.GetGlyphSpan(); var positionSpan = runBuffer.GetPositionSpan(); - var currentX = 0.0; + var width = 0.0; for (int i = 0; i < count; i++) { @@ -245,12 +245,16 @@ namespace Avalonia.Skia glyphSpan[i] = glyphInfo.GlyphIndex; - positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); + positionSpan[i] = new SKPoint((float)(width + offset.X), (float)offset.Y); - currentX += glyphInfo.GlyphAdvance; + width += glyphInfo.GlyphAdvance; } - return new GlyphRunImpl(builder.Build()); + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; + var height = glyphTypeface.Metrics.LineSpacing * scale; + var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale); + + return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d9cd0590fc..a2f99d9d71 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -181,9 +181,15 @@ namespace Avalonia.Direct2D1 run.Advances = new float[glyphCount]; + var width = 0.0; + for (var i = 0; i < glyphCount; i++) { - run.Advances[i] = (float)glyphInfos[i].GlyphAdvance; + var advance = glyphInfos[i].GlyphAdvance; + + width += advance; + + run.Advances[i] = (float)advance; } run.Offsets = new GlyphOffset[glyphCount]; @@ -199,7 +205,11 @@ namespace Avalonia.Direct2D1 }; } - return new GlyphRunImpl(run); + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; + var height = glyphTypeface.Metrics.LineSpacing * scale; + var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale); + + return new GlyphRunImpl(run, new Size(width, height), baselineOrigin); } class D2DApi : IPlatformRenderInterfaceContext diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 3f2298eb22..d5d6cd8c29 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -390,13 +390,13 @@ namespace Avalonia.Direct2D1.Media /// /// The foreground. /// The glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Item.Size)) { - var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; - _renderTarget.DrawGlyphRun(glyphRun.BaselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, + _renderTarget.DrawGlyphRun(glyphRun.Item.BaselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 67418613a4..24b8fc04b3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -1,20 +1,26 @@ using System.Collections.Generic; using Avalonia.Platform; +using SharpDX.DirectWrite; namespace Avalonia.Direct2D1.Media { internal class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun) + public GlyphRunImpl(GlyphRun glyphRun, Size size, Point baselineOrigin) { + Size = size; + BaselineOrigin = baselineOrigin; GlyphRun = glyphRun; } - public SharpDX.DirectWrite.GlyphRun GlyphRun { get; } + public Size Size { get; } + + public Point BaselineOrigin { get; } + + public GlyphRun GlyphRun { get; } public void Dispose() { - //SharpDX already handles this. //GlyphRun?.Dispose(); } diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 3573ba6b07..a05bfbea4c 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests.Media [Theory] public void Should_Get_Distance_From_CharacterHit(double[] advances, int[] clusters, int start, int trailingLength, double expectedDistance) { - using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using(UnitTestApplication.Start(TestServices.StyledWindow)) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var characterHit = new CharacterHit(start, trailingLength); @@ -44,7 +44,7 @@ namespace Avalonia.Base.UnitTests.Media public void Should_Get_CharacterHit_FromDistance(double[] advances, int[] clusters, double distance, int start, int trailingLengthExpected, bool isInsideExpected) { - using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using(UnitTestApplication.Start(TestServices.StyledWindow)) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside); diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index 8436881122..e83b2d7598 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Benchmarks { } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { } diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index 1c2c7c0d7d..c4707c78c8 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -5,6 +5,10 @@ namespace Avalonia.Benchmarks { internal class NullGlyphRun : IGlyphRunImpl { + public Size Size => default; + + public Point BaselineOrigin => default; + public void Dispose() { } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a802cd0958..7a17efa59b 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -123,7 +123,7 @@ namespace Avalonia.Benchmarks public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { - return new MockGlyphRun(); + return new MockGlyphRun(glyphInfos); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 894dc5b996..3b2ca51976 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -205,7 +205,8 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl()); + textShaperImpl: new MockTextShaperImpl(), + renderInterface: new MockPlatformRenderInterface()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index c1d9fad6f4..2732f6a5fb 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -107,7 +107,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void CaretIndex_Can_Moved_To_Position_After_The_End_Of_Text_With_Arrow_Key() { - using (Start()) + using (Start(TestServices.StyledWindow)) { var target = new MaskedTextBox { @@ -182,7 +182,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { - using (Start()) + using (Start(TestServices.StyledWindow)) { MaskedTextBox textBox = new MaskedTextBox { @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No_Selection() { - using (Start()) + using (Start(TestServices.StyledWindow)) { var textBox = new MaskedTextBox { @@ -810,7 +810,7 @@ namespace Avalonia.Controls.UnitTests bool fromClipboard, string expected) { - using (Start()) + using (Start(TestServices.StyledWindow)) { var target = new MaskedTextBox { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 1775d7ef70..7d129d987e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -2131,9 +2131,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private static IDisposable Start() { - return UnitTestApplication.Start(new TestServices( - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl())); + return UnitTestApplication.Start(TestServices.StyledWindow); } private static void Prepare(SelectingItemsControl target) diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index 2fd10010a6..e0bf230d99 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -101,7 +101,8 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl()); + textShaperImpl: new MockTextShaperImpl(), + renderInterface: new MockPlatformRenderInterface()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index f2d6670be5..59c7ac3786 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -110,7 +110,7 @@ namespace Avalonia.Skia.UnitTests.Media if (glyphRun.IsLeftToRight) { var characterHit = - glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _); + glyphRun.GetCharacterHitFromDistance(glyphRun.Size.Width, out _); Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } @@ -159,7 +159,7 @@ namespace Avalonia.Skia.UnitTests.Media { var height = glyphRun.Size.Height; - var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace; + var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Size.Width; var rects = new List(glyphRun.GlyphInfos!.Count); diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index f525e4736b..45e7b47f62 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,10 +1,21 @@ using System.Collections.Generic; +using System.Linq; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; namespace Avalonia.UnitTests { public class MockGlyphRun : IGlyphRunImpl { + public MockGlyphRun(IReadOnlyList glyphInfos) + { + Size = new Size(glyphInfos.Sum(x=> x.GlyphAdvance), 10); + } + + public Size Size { get; } + + public Point BaselineOrigin => new Point(0, 8); + public void Dispose() { diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index d56e360e22..2a00ef8e40 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -149,7 +149,7 @@ namespace Avalonia.UnitTests public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { - return Mock.Of(); + return new MockGlyphRun(glyphInfos); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; From f6d664128a8c13c8969ec2e3ecf8b05eb424ef0f Mon Sep 17 00:00:00 2001 From: ahopper Date: Sat, 21 Jan 2023 13:14:37 +0000 Subject: [PATCH 067/326] fix space key input on x11 --- src/Avalonia.X11/X11Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 861fed0803..84331aa43a 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -731,7 +731,7 @@ namespace Avalonia.X11 void DispatchInput(RawInputEventArgs args) { Input?.Invoke(args); - if (!args.Handled && args is RawKeyEventArgsWithText text && !string.IsNullOrWhiteSpace(text.Text)) + if (!args.Handled && args is RawKeyEventArgsWithText text && !string.IsNullOrEmpty(text.Text)) Input?.Invoke(new RawTextInputEventArgs(_keyboard, args.Timestamp, _inputRoot, text.Text)); } From 10a3b79d128e2053161833de95e8029727614962 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 12:18:38 +0100 Subject: [PATCH 068/326] Perf: various misc text layout optimizations --- src/Avalonia.Base/Avalonia.Base.csproj | 1 + .../Media/Fonts/FamilyNameCollection.cs | 74 ++---- .../Media/TextFormatting/TextCharacters.cs | 9 +- .../Media/TextFormatting/TextFormatterImpl.cs | 11 +- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 41 ++-- .../Media/TextFormatting/Unicode/Codepoint.cs | 117 +++++----- .../Media/TextFormatting/Unicode/Grapheme.cs | 4 + .../Unicode/GraphemeEnumerator.cs | 217 ++++++++---------- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 6 +- .../Media/TextShaperImpl.cs | 6 +- .../Text/HugeTextLayout.cs | 12 +- .../HarfBuzzTextShaperImpl.cs | 6 +- .../Avalonia.UnitTests/MockTextShaperImpl.cs | 3 +- 13 files changed, 241 insertions(+), 266 deletions(-) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 4a67191132..35a453ce59 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index eb42f6443b..f2350f5aea 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs @@ -1,13 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Text; using Avalonia.Utilities; namespace Avalonia.Media.Fonts { public sealed class FamilyNameCollection : IReadOnlyList { + private readonly string[] _names; + /// /// Initializes a new instance of the class. /// @@ -20,13 +21,20 @@ namespace Avalonia.Media.Fonts throw new ArgumentNullException(nameof(familyNames)); } - Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); + _names = SplitNames(familyNames); - PrimaryFamilyName = Names[0]; + PrimaryFamilyName = _names[0]; - HasFallbacks = Names.Count > 1; + HasFallbacks = _names.Length > 1; } + private static string[] SplitNames(string names) +#if NET6_0_OR_GREATER + => names.Split(',', StringSplitOptions.TrimEntries); +#else + => Array.ConvertAll(names.Split(','), p => p.Trim()); +#endif + /// /// Gets the primary family name. /// @@ -43,14 +51,6 @@ namespace Avalonia.Media.Fonts /// public bool HasFallbacks { get; } - /// - /// Gets the internal collection of names. - /// - /// - /// The names. - /// - internal IReadOnlyList Names { get; } - /// /// Returns an enumerator for the name collection. /// @@ -76,23 +76,7 @@ namespace Avalonia.Media.Fonts /// A that represents this instance. /// public override string ToString() - { - var builder = StringBuilderCache.Acquire(); - - for (var index = 0; index < Names.Count; index++) - { - builder.Append(Names[index]); - - if (index == Names.Count - 1) - { - break; - } - - builder.Append(", "); - } - - return StringBuilderCache.GetStringAndRelease(builder); - } + => String.Join(", ", _names); /// /// Returns a hash code for this instance. @@ -102,7 +86,7 @@ namespace Avalonia.Media.Fonts /// public override int GetHashCode() { - if (Count == 0) + if (_names.Length == 0) { return 0; } @@ -111,9 +95,9 @@ namespace Avalonia.Media.Fonts { int hash = 17; - for (var i = 0; i < Names.Count; i++) + for (var i = 0; i < _names.Length; i++) { - string name = Names[i]; + string name = _names[i]; hash = hash * 23 + name.GetHashCode(); } @@ -145,30 +129,10 @@ namespace Avalonia.Media.Fonts /// true if the specified is equal to this instance; otherwise, false. /// public override bool Equals(object? obj) - { - if (!(obj is FamilyNameCollection other)) - { - return false; - } - - if (other.Count != Count) - { - return false; - } - - for (int i = 0; i < Count; i++) - { - if (Names[i] != other.Names[i]) - { - return false; - } - } - - return true; - } + => obj is FamilyNameCollection other && _names.AsSpan().SequenceEqual(other._names); - public int Count => Names.Count; + public int Count => _names.Length; - public string this[int index] => Names[index]; + public string this[int index] => _names[index]; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index c1f3816e54..9e76418ac9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -47,13 +47,13 @@ namespace Avalonia.Media.TextFormatting /// /// The shapeable text characters. internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties, RentedList results) + FontManager fontManager, ref TextRunProperties? previousProperties, RentedList results) { var properties = Properties; while (!text.IsEmpty) { - var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); + var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, fontManager, ref previousProperties); results.Add(shapeableRun); @@ -72,7 +72,8 @@ namespace Avalonia.Media.TextFormatting /// /// A list of shapeable text runs. private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, - TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties) + TextRunProperties defaultProperties, sbyte biDiLevel, FontManager fontManager, + ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; @@ -121,7 +122,7 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback var matchFound = - FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, + fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index bf9f6f77f8..b0242be87e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -393,6 +393,7 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; ReadOnlyMemory runText = default; + var fontManager = FontManager.Current; for (var i = 0; i < textCharacters.Count; i++) { @@ -427,8 +428,8 @@ namespace Avalonia.Media.TextFormatting if (j == runTextSpan.Length) { - currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, - processedRuns); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager, + ref previousProperties, processedRuns); runLevel = levels[levelIndex]; @@ -441,8 +442,8 @@ namespace Avalonia.Media.TextFormatting } // End of this run - currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, - processedRuns); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager, + ref previousProperties, processedRuns); runText = runText.Slice(j); runTextSpan = runText.Span; @@ -459,7 +460,7 @@ namespace Avalonia.Media.TextFormatting return; } - currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns); + currentRun.GetShapeableCharacters(runText, runLevel, fontManager, ref previousProperties, processedRuns); } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 3a81784152..36e9e6eb79 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -343,6 +343,17 @@ namespace Avalonia.Media.TextFormatting.Unicode return 0; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsIsolateStart(BidiClass type) + { + const uint mask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate); + + return ((1U << (int)type) & mask) != 0U; + } + /// /// Build a list of matching isolates for a directionality slice /// Implements BD9 @@ -701,28 +712,19 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastType = _workingClasses[lastCharIndex]; int nextLevel; - switch (lastType) + if (IsIsolateStart(lastType)) { - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: + nextLevel = _paragraphEmbeddingLevel; + } + else + { + i = lastCharIndex + 1; + while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i])) { - nextLevel = _paragraphEmbeddingLevel; - - break; + i++; } - default: - { - i = lastCharIndex + 1; - while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i])) - { - i++; - } - nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i]; - - break; - } + nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i]; } var eos = DirectionFromLevel(Math.Max(nextLevel, level)); @@ -831,8 +833,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // PDI and concatenate that run to this one var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastType = _originalClasses[lastCharacterIndex]; - if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && - _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) + if (IsIsolateStart(lastType) && _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 22f7b50fd4..6433a37b22 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode @@ -11,13 +10,19 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// The replacement codepoint that is used for non supported values. /// - public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD'); - - public Codepoint(uint value) + public static Codepoint ReplacementCodepoint { - _value = value; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new('\uFFFD'); } + /// + /// Creates a new instance of with the specified value. + /// + /// The codepoint value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Codepoint(uint value) => _value = value; + /// /// Get the codepoint's value. /// @@ -87,19 +92,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public bool IsWhiteSpace { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - switch (GeneralCategory) - { - case GeneralCategory.Control: - case GeneralCategory.NonspacingMark: - case GeneralCategory.Format: - case GeneralCategory.SpaceSeparator: - case GeneralCategory.SpacingMark: - return true; - } - - return false; + const ulong whiteSpaceMask = + (1UL << (int)GeneralCategory.Control) | + (1UL << (int)GeneralCategory.NonspacingMark) | + (1UL << (int)GeneralCategory.Format) | + (1UL << (int)GeneralCategory.SpaceSeparator) | + (1UL << (int)GeneralCategory.SpacingMark); + + return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0L; } } @@ -166,56 +169,62 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The index to read at. /// The count of character that were read. /// +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) { + // Perf note: this method is performance critical for text layout, modify with care! + count = 1; - if (index >= text.Length) + // Perf note: uint check allows the JIT to ellide the next bound check + if ((uint)index >= (uint)text.Length) { return ReplacementCodepoint; } - var code = text[index]; - - ushort hi, low; + uint code = text[index]; - //# High surrogate - if (0xD800 <= code && code <= 0xDBFF) + //# Surrogate + if (IsInRangeInclusive(code, 0xD800U, 0xDFFFU)) { - hi = code; - - if (index + 1 == text.Length) - { - return ReplacementCodepoint; - } - - low = text[index + 1]; - - if (0xDC00 <= low && low <= 0xDFFF) - { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); - } - - return ReplacementCodepoint; - } + uint hi, low; - //# Low surrogate - if (0xDC00 <= code && code <= 0xDFFF) - { - if (index == 0) + //# High surrogate + if (code <= 0xDBFF) { - return ReplacementCodepoint; + if ((uint)(index + 1) < (uint)text.Length) + { + hi = code; + low = text[index + 1]; + + if (IsInRangeInclusive(low, 0xDC00U, 0xDFFFU)) + { + count = 2; + // Perf note: the code is written as below to become just two instructions: shl, lea. + // See https://github.com/dotnet/runtime/blob/7ec3634ee579d89b6024f72b595bfd7118093fc5/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs#L38 + return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16))); + } + } } - hi = text[index - 1]; - - low = code; - - if (0xD800 <= hi && hi <= 0xDBFF) + //# Low surrogate + else { - count = 2; - return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)); + if (index > 0) + { + low = code; + hi = text[index - 1]; + + if (IsInRangeInclusive(hi, 0xD800U, 0xDBFFU)) + { + count = 2; + return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16))); + } + } } return ReplacementCodepoint; @@ -224,12 +233,16 @@ namespace Avalonia.Media.TextFormatting.Unicode return new Codepoint(code); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound) + => value - lowerBound <= upperBound - lowerBound; + /// /// Returns if is between /// and , inclusive. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRangeInclusive(Codepoint cp, uint lowerBound, uint upperBound) - => (cp._value - lowerBound) <= (upperBound - lowerBound); + => IsInRangeInclusive(cp._value, lowerBound, upperBound); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index fa8e8ac976..5a4d891917 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -22,5 +22,9 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text of the grapheme cluster /// public ReadOnlySpan Text { get; } + + /// + public override string ToString() + => Text.ToString(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index 812bb99d99..a6a9453b8a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -4,57 +4,79 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; -using System.Runtime.InteropServices; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct GraphemeEnumerator { - private ReadOnlySpan _text; + private readonly ReadOnlySpan _text; + private int _currentCodeUnitOffset; + private int _codeUnitLengthOfCurrentCodepoint; + private Codepoint _currentCodepoint; + + /// + /// Will be if invalid data or EOF reached. + /// Caller shouldn't need to special-case this since the normal rules will halt on this condition. + /// + private GraphemeBreakClass _currentType; public GraphemeEnumerator(ReadOnlySpan text) { _text = text; - Current = default; + _currentCodeUnitOffset = 0; + _codeUnitLengthOfCurrentCodepoint = 0; + _currentCodepoint = Codepoint.ReplacementCodepoint; + _currentType = GraphemeBreakClass.Other; } - /// - /// Gets the current . - /// - public Grapheme Current { get; private set; } - /// /// Moves to the next . /// /// - public bool MoveNext() + public bool MoveNext(out Grapheme grapheme) { - if (_text.IsEmpty) + var startOffset = _currentCodeUnitOffset; + + if ((uint)startOffset >= (uint)_text.Length) { + grapheme = default; return false; } // Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules. - var processor = new Processor(_text); - - processor.MoveNext(); + if (startOffset == 0) + { + ReadNextCodepoint(); + } - var firstCodepoint = processor.CurrentCodepoint; + var firstCodepoint = _currentCodepoint; // First, consume as many Prepend scalars as we can (rule GB9b). - while (processor.CurrentType == GraphemeBreakClass.Prepend) + if (_currentType == GraphemeBreakClass.Prepend) { - processor.MoveNext(); + do + { + ReadNextCodepoint(); + } while (_currentType == GraphemeBreakClass.Prepend); + + // There were only Prepend scalars in the text + if ((uint)_currentCodeUnitOffset >= (uint)_text.Length) + { + goto Return; + } } // Next, make sure we're not about to violate control character restrictions. // Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5). - if (processor.CurrentCodeUnitOffset > 0) + if (_currentCodeUnitOffset > startOffset) { - if (processor.CurrentType == GraphemeBreakClass.Control - || processor.CurrentType == GraphemeBreakClass.CR - || processor.CurrentType == GraphemeBreakClass.LF) + const uint controlCrLfMask = + (1U << (int)GraphemeBreakClass.Control) | + (1U << (int)GraphemeBreakClass.CR) | + (1U << (int)GraphemeBreakClass.LF); + + if (((1U << (int)_currentType) & controlCrLfMask) != 0U) { goto Return; } @@ -62,19 +84,19 @@ namespace Avalonia.Media.TextFormatting.Unicode // Now begin the main state machine. - var previousClusterBreakType = processor.CurrentType; + var previousClusterBreakType = _currentType; - processor.MoveNext(); + ReadNextCodepoint(); switch (previousClusterBreakType) { case GraphemeBreakClass.CR: - if (processor.CurrentType != GraphemeBreakClass.LF) + if (_currentType != GraphemeBreakClass.LF) { goto Return; // rules GB3 & GB4 (only can follow ) } - processor.MoveNext(); + ReadNextCodepoint(); goto case GraphemeBreakClass.LF; case GraphemeBreakClass.Control: @@ -82,53 +104,57 @@ namespace Avalonia.Media.TextFormatting.Unicode goto Return; // rule GB4 (no data after Control | LF) case GraphemeBreakClass.L: - if (processor.CurrentType == GraphemeBreakClass.L) + { + if (_currentType == GraphemeBreakClass.L) { - processor.MoveNext(); // rule GB6 (L x L) + ReadNextCodepoint(); // rule GB6 (L x L) goto case GraphemeBreakClass.L; } - else if (processor.CurrentType == GraphemeBreakClass.V) + else if (_currentType == GraphemeBreakClass.V) { - processor.MoveNext(); // rule GB6 (L x V) + ReadNextCodepoint(); // rule GB6 (L x V) goto case GraphemeBreakClass.V; } - else if (processor.CurrentType == GraphemeBreakClass.LV) + else if (_currentType == GraphemeBreakClass.LV) { - processor.MoveNext(); // rule GB6 (L x LV) + ReadNextCodepoint(); // rule GB6 (L x LV) goto case GraphemeBreakClass.LV; } - else if (processor.CurrentType == GraphemeBreakClass.LVT) + else if (_currentType == GraphemeBreakClass.LVT) { - processor.MoveNext(); // rule GB6 (L x LVT) + ReadNextCodepoint(); // rule GB6 (L x LVT) goto case GraphemeBreakClass.LVT; } else { break; } + } case GraphemeBreakClass.LV: case GraphemeBreakClass.V: - if (processor.CurrentType == GraphemeBreakClass.V) + { + if (_currentType == GraphemeBreakClass.V) { - processor.MoveNext(); // rule GB7 (LV | V x V) + ReadNextCodepoint(); // rule GB7 (LV | V x V) goto case GraphemeBreakClass.V; } - else if (processor.CurrentType == GraphemeBreakClass.T) + else if (_currentType == GraphemeBreakClass.T) { - processor.MoveNext(); // rule GB7 (LV | V x T) + ReadNextCodepoint(); // rule GB7 (LV | V x T) goto case GraphemeBreakClass.T; } else { break; } + } case GraphemeBreakClass.LVT: case GraphemeBreakClass.T: - if (processor.CurrentType == GraphemeBreakClass.T) + if (_currentType == GraphemeBreakClass.T) { - processor.MoveNext(); // rule GB8 (LVT | T x T) + ReadNextCodepoint(); // rule GB8 (LVT | T x T) goto case GraphemeBreakClass.T; } else @@ -139,123 +165,76 @@ namespace Avalonia.Media.TextFormatting.Unicode case GraphemeBreakClass.ExtendedPictographic: // Attempt processing extended pictographic (rules GB11, GB9). // First, drain any Extend scalars that might exist - while (processor.CurrentType == GraphemeBreakClass.Extend) + while (_currentType == GraphemeBreakClass.Extend) { - processor.MoveNext(); + ReadNextCodepoint(); } // Now see if there's a ZWJ + extended pictograph again. - if (processor.CurrentType != GraphemeBreakClass.ZWJ) + if (_currentType != GraphemeBreakClass.ZWJ) { break; } - processor.MoveNext(); - if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic) + ReadNextCodepoint(); + if (_currentType != GraphemeBreakClass.ExtendedPictographic) { break; } - processor.MoveNext(); + ReadNextCodepoint(); goto case GraphemeBreakClass.ExtendedPictographic; case GraphemeBreakClass.RegionalIndicator: // We've consumed a single RI scalar. Try to consume another (to make it a pair). - if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator) + if (_currentType == GraphemeBreakClass.RegionalIndicator) { - processor.MoveNext(); + ReadNextCodepoint(); } // Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers. break; // nothing but trailers after the final RI - - default: - break; } // rules GB9, GB9a - while (processor.CurrentType == GraphemeBreakClass.Extend - || processor.CurrentType == GraphemeBreakClass.ZWJ - || processor.CurrentType == GraphemeBreakClass.SpacingMark) + while (_currentType is GraphemeBreakClass.Extend + or GraphemeBreakClass.ZWJ + or GraphemeBreakClass.SpacingMark) { - processor.MoveNext(); + ReadNextCodepoint(); } Return: - Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset)); - - _text = _text.Slice(processor.CurrentCodeUnitOffset); + var graphemeLength = _currentCodeUnitOffset - startOffset; + grapheme = new Grapheme(firstCodepoint, startOffset, graphemeLength); return true; // rules GB2, GB999 } - [StructLayout(LayoutKind.Auto)] - private ref struct Processor + private void ReadNextCodepoint() { - private readonly ReadOnlySpan _buffer; - private int _codeUnitLengthOfCurrentScalar; - - internal Processor(ReadOnlySpan buffer) - { - _buffer = buffer; - _codeUnitLengthOfCurrentScalar = 0; - CurrentCodepoint = Codepoint.ReplacementCodepoint; - CurrentType = GraphemeBreakClass.Other; - CurrentCodeUnitOffset = 0; - } - - public int CurrentCodeUnitOffset { get; private set; } - - /// - /// Will be if invalid data or EOF reached. - /// Caller shouldn't need to special-case this since the normal rules will halt on this condition. - /// - public GraphemeBreakClass CurrentType { get; private set; } - - /// - /// Get the currently processed . - /// - public Codepoint CurrentCodepoint { get; private set; } - - public void MoveNext() - { - // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on - // the decoder's default behavior of interpreting these ill-formed subsequences as - // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property - // of Other (XX), which matches the modifications made to UAX#29, Rev. 35. - // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications - // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file - // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt - // has the line "D800..DFFF ; Control # Cs [2048] ..", - // but starting with Unicode 12.0 that line has been removed. - // - // If a later version of the Unicode Standard further modifies this guidance we should reflect - // that here. - - if (CurrentCodeUnitOffset == _buffer.Length) - { - CurrentCodepoint = Codepoint.ReplacementCodepoint; - } - else - { - CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar; - - if (CurrentCodeUnitOffset < _buffer.Length) - { - CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset, - out _codeUnitLengthOfCurrentScalar); - } - else - { - CurrentCodepoint = Codepoint.ReplacementCodepoint; - } - } - - CurrentType = CurrentCodepoint.GraphemeBreakClass; - } + // For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on + // the decoder's default behavior of interpreting these ill-formed subsequences as + // equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property + // of Other (XX), which matches the modifications made to UAX#29, Rev. 35. + // See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications + // This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file + // https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt + // has the line "D800..DFFF ; Control # Cs [2048] ..", + // but starting with Unicode 12.0 that line has been removed. + // + // If a later version of the Unicode Standard further modifies this guidance we should reflect + // that here. + + _currentCodeUnitOffset += _codeUnitLengthOfCurrentCodepoint; + + _currentCodepoint = Codepoint.ReadAt(_text, _currentCodeUnitOffset, + out _codeUnitLengthOfCurrentCodepoint); + + _currentType = _currentCodepoint.GraphemeBreakClass; } } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index def2482af3..e1a6b93692 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -52,6 +52,8 @@ namespace Avalonia.Skia var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; + var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -77,9 +79,7 @@ namespace Avalonia.Skia 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - - shapedBuffer[i] = targetInfo; + targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index ac441108e3..ff0fff6b14 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -52,6 +52,8 @@ namespace Avalonia.Direct2D1.Media var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; + var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -77,9 +79,7 @@ namespace Avalonia.Direct2D1.Media 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - - shapedBuffer[i] = targetInfo; + targetInfos[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index c96edbef5c..0adabc75f1 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -77,7 +77,17 @@ In respect that the structure of the sufficient amount poses problems and challe public TextLayout BuildEmojisTextLayout() => MakeLayout(Emojis); [Benchmark] - public TextLayout[] BuildManySmallTexts() => _manySmallStrings.Select(MakeLayout).ToArray(); + public TextLayout[] BuildManySmallTexts() + { + var results = new TextLayout[_manySmallStrings.Length]; + + for (var i = 0; i < _manySmallStrings.Length; i++) + { + results[i] = MakeLayout(_manySmallStrings[i]); + } + + return results; + } [Benchmark] public void VirtualizeTextBlocks() diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index baf5ffb07c..0448ecd41f 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -52,6 +52,8 @@ namespace Avalonia.UnitTests var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; + var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -77,9 +79,7 @@ namespace Avalonia.UnitTests 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); - - shapedBuffer[i] = targetInfo; + targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index b5f4777192..b810caabd9 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -13,6 +13,7 @@ namespace Avalonia.UnitTests var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var targetInfos = shapedBuffer.GlyphInfos; var textSpan = text.Span; var textStartIndex = TextTestHelper.GetStartCharIndex(text); @@ -26,7 +27,7 @@ namespace Avalonia.UnitTests for (var j = 0; j < count; ++j) { - shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); + targetInfos[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); } i += count; From 2f429062a1dfe826351f8a48a785eaedda4c584f Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 16:43:42 +0100 Subject: [PATCH 069/326] Perf: improved GraphemeEnumerator by avoiding double codepoint iteration --- .../TextFormatting/FormattedTextSource.cs | 6 ++---- .../Media/TextFormatting/TextCharacters.cs | 12 ++++------- .../Media/TextFormatting/Unicode/Grapheme.cs | 20 +++++++++---------- .../Unicode/GraphemeEnumerator.cs | 9 ++++++--- src/Avalonia.Controls/TextBox.cs | 6 ++---- .../GraphemeBreakClassTrieGeneratorTests.cs | 8 ++++---- .../Media/TextFormatting/TextLayoutTests.cs | 17 ++++++++-------- 7 files changed, 36 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index 5c28989c7d..2f8c4ad263 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -128,11 +128,9 @@ namespace Avalonia.Media.TextFormatting var graphemeEnumerator = new GraphemeEnumerator(text); - while (graphemeEnumerator.MoveNext()) + while (graphemeEnumerator.MoveNext(out var grapheme)) { - var grapheme = graphemeEnumerator.Current; - - finalLength += grapheme.Text.Length; + finalLength += grapheme.Length; if (finalLength >= length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 9e76418ac9..3ccfb40c4a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -140,16 +140,14 @@ namespace Avalonia.Media.TextFormatting var enumerator = new GraphemeEnumerator(textSpan); - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var grapheme)) { - var grapheme = enumerator.Current; - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } - count += grapheme.Text.Length; + count += grapheme.Length; } return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel); @@ -184,10 +182,8 @@ namespace Avalonia.Media.TextFormatting var enumerator = new GraphemeEnumerator(text); - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var currentGrapheme)) { - var currentGrapheme = enumerator.Current; - var currentScript = currentGrapheme.FirstCodepoint.Script; if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) @@ -217,7 +213,7 @@ namespace Avalonia.Media.TextFormatting } } - length += currentGrapheme.Text.Length; + length += currentGrapheme.Length; } return length > 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index 5a4d891917..fcc12d3526 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,16 +1,15 @@ -using System; - -namespace Avalonia.Media.TextFormatting.Unicode +namespace Avalonia.Media.TextFormatting.Unicode { /// /// Represents the smallest unit of a writing system of any given language. /// public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) + public Grapheme(Codepoint firstCodepoint, int offset, int length) { FirstCodepoint = firstCodepoint; - Text = text; + Offset = offset; + Length = length; } /// @@ -19,12 +18,13 @@ namespace Avalonia.Media.TextFormatting.Unicode public Codepoint FirstCodepoint { get; } /// - /// The text of the grapheme cluster + /// Gets the starting code unit offset of this grapheme inside its containing text. /// - public ReadOnlySpan Text { get; } + public int Offset { get; } - /// - public override string ToString() - => Text.ToString(); + /// + /// Gets the length of this grapheme, in code units. + /// + public int Length { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index a6a9453b8a..dd01662155 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -198,10 +198,13 @@ namespace Avalonia.Media.TextFormatting.Unicode break; // nothing but trailers after the final RI } + const uint gb9Mask = + (1U << (int)GraphemeBreakClass.Extend) | + (1U << (int)GraphemeBreakClass.ZWJ) | + (1U << (int)GraphemeBreakClass.SpacingMark); + // rules GB9, GB9a - while (_currentType is GraphemeBreakClass.Extend - or GraphemeBreakClass.ZWJ - or GraphemeBreakClass.SpacingMark) + while (((1U << (int)_currentType) & gb9Mask) != 0U) { ReadNextCodepoint(); } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 9d07fb024a..78caf350b7 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -963,10 +963,8 @@ namespace Avalonia.Controls var graphemeEnumerator = new GraphemeEnumerator(input.AsSpan()); - while (graphemeEnumerator.MoveNext()) + while (graphemeEnumerator.MoveNext(out var grapheme)) { - var grapheme = graphemeEnumerator.Current; - if (grapheme.FirstCodepoint.IsBreakChar) { if (lineCount + 1 > MaxLines) @@ -979,7 +977,7 @@ namespace Avalonia.Controls } } - length += grapheme.Text.Length; + length += grapheme.Length; } if (length < input.Length) diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index a022039000..0e49669a04 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -40,9 +40,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var enumerator = new GraphemeEnumerator(text); - enumerator.MoveNext(); + enumerator.MoveNext(out var g); - var actual = enumerator.Current.Text; + var actual = text.AsSpan(g.Offset, g.Length); bool pass = actual.Length == grapheme.Length; @@ -86,9 +86,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var count = 0; - while (enumerator.MoveNext()) + while (enumerator.MoveNext(out var grapheme)) { - Assert.Equal(1, enumerator.Current.Text.Length); + Assert.Equal(1, grapheme.Length); count++; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index a24a0fcf70..2b63f24cf6 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -151,9 +151,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting while (true) { - while (inner.MoveNext()) + Grapheme grapheme; + while (inner.MoveNext(out grapheme)) { - j += inner.Current.Text.Length; + j += grapheme.Length; if (j + i > text.Length) { @@ -184,14 +185,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - if (!outer.MoveNext()) + if (!outer.MoveNext(out grapheme)) { break; } inner = new GraphemeEnumerator(text); - i += outer.Current.Text.Length; + i += grapheme.Length; } } @@ -979,13 +980,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var graphemeEnumerator = new GraphemeEnumerator(text); - while (graphemeEnumerator.MoveNext()) + while (graphemeEnumerator.MoveNext(out var grapheme)) { - var grapheme = graphemeEnumerator.Current; + var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; - var textStyleOverrides = new[] { new ValueSpan(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; - - i += grapheme.Text.Length; + i += grapheme.Length; var layout = new TextLayout( text, From 63f6ef63af8a8597c9efb9ddce829a12305423b7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 19:00:37 +0100 Subject: [PATCH 070/326] Perf: pass FontManager and typefaces around during text layout --- src/Avalonia.Base/Media/FontManager.cs | 2 +- .../Media/TextFormatting/ShapedTextRun.cs | 2 +- .../Media/TextFormatting/TextCharacters.cs | 54 +++++++++---------- .../Media/TextFormatting/TextFormatterImpl.cs | 37 +++++++------ .../Media/TextFormatting/TextLayout.cs | 7 +-- .../Media/TextFormatting/TextLineImpl.cs | 9 ++-- .../Media/TextFormatting/TextRunProperties.cs | 5 ++ 7 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index e82d5b7ba5..2dabb29e76 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -132,7 +132,7 @@ namespace Avalonia.Media { typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch); - var glyphTypeface = typeface.GlyphTypeface; + var glyphTypeface = GetOrAddGlyphTypeface(typeface); if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){ return true; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index d444a58297..ac196bf7e0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting { ShapedBuffer = shapedBuffer; Properties = properties; - TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize); + TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); } public bool IsReversed { get; private set; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 3ccfb40c4a..94db739d4d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -69,6 +69,7 @@ namespace Avalonia.Media.TextFormatting /// The characters to create text runs from. /// The default text run properties. /// The bidi level of the run. + /// The font manager to use. /// /// A list of shapeable text runs. private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, @@ -76,31 +77,32 @@ namespace Avalonia.Media.TextFormatting ref TextRunProperties? previousProperties) { var defaultTypeface = defaultProperties.Typeface; - var currentTypeface = defaultTypeface; + var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface; var previousTypeface = previousProperties?.Typeface; + var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script)) { - if (script == Script.Common && previousTypeface is not null) + if (script == Script.Common && previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _)) { return new UnshapedTextRun(text.Slice(0, fallbackCount), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); + defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); } } - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); } - if (previousTypeface is not null) + if (previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _)) { return new UnshapedTextRun(text.Slice(0, count), - defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel); + defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); } } @@ -124,25 +126,23 @@ namespace Avalonia.Media.TextFormatting var matchFound = fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, - out currentTypeface); + out var fallbackTypeface); - if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _)) + var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); + + if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _)) { //Fallback found - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface), + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), biDiLevel); } // no fallback found - currentTypeface = defaultTypeface; - - var glyphTypeface = currentTypeface.GlyphTypeface; - var enumerator = new GraphemeEnumerator(textSpan); while (enumerator.MoveNext(out var grapheme)) { - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } @@ -157,15 +157,15 @@ namespace Avalonia.Media.TextFormatting /// Tries to get a shapeable length that is supported by the specified typeface. /// /// The characters to shape. - /// The typeface that is used to find matching characters. - /// + /// The typeface that is used to find matching characters. + /// The default typeface. /// The shapeable length. /// /// internal static bool TryGetShapeableLength( ReadOnlySpan text, - Typeface typeface, - Typeface? defaultTypeface, + IGlyphTypeface glyphTypeface, + IGlyphTypeface? defaultGlyphTypeface, out int length, out Script script) { @@ -177,22 +177,22 @@ namespace Avalonia.Media.TextFormatting return false; } - var font = typeface.GlyphTypeface; - var defaultFont = defaultTypeface?.GlyphTypeface; - var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext(out var currentGrapheme)) { - var currentScript = currentGrapheme.FirstCodepoint.Script; + var currentCodepoint = currentGrapheme.FirstCodepoint; + var currentScript = currentCodepoint.Script; - if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsWhiteSpace + && defaultGlyphTypeface != null + && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } //Stop at the first missing glyph - if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) + if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _)) { break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index b0242be87e..bc19690196 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -27,6 +27,7 @@ namespace Avalonia.Media.TextFormatting TextLineBreak? nextLineBreak = null; IReadOnlyList? textRuns; var objectPool = FormattingObjectPool.Instance; + var fontManager = FontManager.Current; var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); @@ -42,7 +43,7 @@ namespace Avalonia.Media.TextFormatting } else { - shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection); textRuns = shapedTextRuns; if (nextLineBreak == null && textEndOfLine != null) @@ -72,7 +73,7 @@ namespace Avalonia.Media.TextFormatting case TextWrapping.Wrap: { textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); break; } default: @@ -178,12 +179,13 @@ namespace Avalonia.Media.TextFormatting /// The default paragraph properties. /// The resolved flow direction. /// A pool used to get reusable formatting objects. + /// The font manager to use. /// /// A list of shaped text characters. /// private static RentedList ShapeTextRuns(IReadOnlyList textRuns, TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, - out FlowDirection resolvedFlowDirection) + FontManager fontManager, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; var shapedRuns = objectPool.TextRunLists.Rent(); @@ -223,7 +225,7 @@ namespace Avalonia.Media.TextFormatting var processedRuns = objectPool.TextRunLists.Rent(); - CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns); bidiData.Reset(); bidiAlgorithm.Reset(); @@ -240,7 +242,9 @@ namespace Avalonia.Media.TextFormatting { groupedRuns.Clear(); groupedRuns.Add(shapeableRun); + var text = shapeableRun.Text; + var properties = shapeableRun.Properties; while (index + 1 < processedRuns.Count) { @@ -251,7 +255,7 @@ namespace Avalonia.Media.TextFormatting if (shapeableRun.BidiLevel == nextRun.BidiLevel && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText) - && CanShapeTogether(shapeableRun.Properties, nextRun.Properties)) + && CanShapeTogether(properties, nextRun.Properties)) { groupedRuns.Add(nextRun); index++; @@ -263,10 +267,10 @@ namespace Avalonia.Media.TextFormatting break; } - var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface, - currentRun.Properties.FontRenderingEmSize, - shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, - paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); + var shaperOptions = new TextShaperOptions( + properties.CachedGlyphTypeface, + properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, + paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); @@ -377,10 +381,11 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. + /// The font manager to use. /// A list that will be filled with the processed runs. /// private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, - RentedList processedRuns) + FontManager fontManager, RentedList processedRuns) { if (levels.Length == 0) { @@ -393,7 +398,6 @@ namespace Avalonia.Media.TextFormatting TextRunProperties? previousProperties = null; TextCharacters? currentRun = null; ReadOnlyMemory runText = default; - var fontManager = FontManager.Current; for (var i = 0; i < textCharacters.Count; i++) { @@ -638,11 +642,11 @@ namespace Avalonia.Media.TextFormatting /// /// The empty text line. public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) + TextParagraphProperties paragraphProperties, FontManager fontManager) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; - var glyphTypeface = properties.Typeface.GlyphTypeface; + var glyphTypeface = properties.CachedGlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; @@ -666,14 +670,15 @@ namespace Avalonia.Media.TextFormatting /// /// The current line break if the line was explicitly broken. /// A pool used to get reusable formatting objects. + /// The font manager to use. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) + TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -869,7 +874,7 @@ namespace Avalonia.Media.TextFormatting { var textShaper = TextShaper.Current; - var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface; + var glyphTypeface = textRun.Properties!.CachedGlyphTypeface; var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 7a74dc89ae..bb58e0d692 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -427,11 +427,12 @@ namespace Avalonia.Media.TextFormatting private TextLine[] CreateTextLines() { var objectPool = FormattingObjectPool.Instance; + var fontManager = FontManager.Current; if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, - FormattingObjectPool.Instance); + fontManager); Bounds = new Rect(0, 0, 0, textLine.Height); @@ -458,7 +459,7 @@ namespace Avalonia.Media.TextFormatting if (previousLine != null && previousLine.NewLineLength > 0) { var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, - _paragraphProperties, objectPool); + _paragraphProperties, fontManager); textLines.Add(emptyTextLine); @@ -517,7 +518,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager); textLines.Add(textLine); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 260fcaccbe..ad3244a3a5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1256,7 +1256,7 @@ namespace Avalonia.Media.TextFormatting private TextLineMetrics CreateLineMetrics() { - var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics; + var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics; var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; @@ -1285,12 +1285,13 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextRun textRun: { + var properties = textRun.Properties; var textMetrics = - new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize); + new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); - if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) + if (fontRenderingEmSize < properties.FontRenderingEmSize) { - fontRenderingEmSize = textRun.Properties.FontRenderingEmSize; + fontRenderingEmSize = properties.FontRenderingEmSize; if (ascent > textMetrics.Ascent) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 7bad99f33f..1622bc3b6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting /// public abstract class TextRunProperties : IEquatable { + private IGlyphTypeface? _cachedGlyphTypeFace; + /// /// Run typeface /// @@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting /// public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline; + internal IGlyphTypeface CachedGlyphTypeface + => _cachedGlyphTypeFace ??= Typeface.GlyphTypeface; + public bool Equals(TextRunProperties? other) { if (ReferenceEquals(null, other)) From 900299b6a8b002a77d1bff970ce140a9f26cd00d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 21 Jan 2023 00:31:07 +0100 Subject: [PATCH 071/326] Perf: improved LineBreakEnumerator by using less branches --- .../Media/TextFormatting/Unicode/Codepoint.cs | 2 +- .../Unicode/LineBreakEnumerator.cs | 229 ++++++++++++------ 2 files changed, 155 insertions(+), 76 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index 6433a37b22..23a1e4a275 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -102,7 +102,7 @@ namespace Avalonia.Media.TextFormatting.Unicode (1UL << (int)GeneralCategory.SpaceSeparator) | (1UL << (int)GeneralCategory.SpacingMark); - return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0L; + return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0UL; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 877ab76ce5..31ef47f47b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -3,6 +3,7 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; +using System.Runtime.CompilerServices; namespace Avalonia.Media.TextFormatting.Unicode { @@ -118,13 +119,14 @@ namespace Avalonia.Media.TextFormatting.Unicode return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static LineBreakClass MapClass(Codepoint cp) { if (cp.Value == 327685) { return LineBreakClass.Alphabetic; } - + // LB 1 // ========================================== // Resolved Original General_Category @@ -133,26 +135,38 @@ namespace Avalonia.Media.TextFormatting.Unicode // CM SA Only Mn or Mc // AL SA Any except Mn and Mc // NS CJ Any - switch (cp.LineBreakClass) - { - case LineBreakClass.Ambiguous: - case LineBreakClass.Surrogate: - case LineBreakClass.Unknown: - return LineBreakClass.Alphabetic; + var cls = cp.LineBreakClass; - case LineBreakClass.ComplexContext: - return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark - ? LineBreakClass.CombiningMark - : LineBreakClass.Alphabetic; + const ulong specialMask = + (1UL << (int)LineBreakClass.Ambiguous) | + (1UL << (int)LineBreakClass.Surrogate) | + (1UL << (int)LineBreakClass.Unknown) | + (1UL << (int)LineBreakClass.ComplexContext) | + (1UL << (int)LineBreakClass.ConditionalJapaneseStarter); - case LineBreakClass.ConditionalJapaneseStarter: - return LineBreakClass.Nonstarter; - - default: - return cp.LineBreakClass; + if (((1UL << (int)cls) & specialMask) != 0UL) + { + switch (cls) + { + case LineBreakClass.Ambiguous: + case LineBreakClass.Surrogate: + case LineBreakClass.Unknown: + return LineBreakClass.Alphabetic; + + case LineBreakClass.ComplexContext: + return cp.GeneralCategory is GeneralCategory.NonspacingMark or GeneralCategory.SpacingMark + ? LineBreakClass.CombiningMark + : LineBreakClass.Alphabetic; + + case LineBreakClass.ConditionalJapaneseStarter: + return LineBreakClass.Nonstarter; + } } + + return cls; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static LineBreakClass MapFirst(LineBreakClass c) { switch (c) @@ -169,10 +183,80 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsAlphaNumeric(LineBreakClass cls) - => cls == LineBreakClass.Alphabetic - || cls == LineBreakClass.HebrewLetter - || cls == LineBreakClass.Numeric; + { + const ulong mask = + (1UL << (int)LineBreakClass.Alphabetic) | + (1UL << (int)LineBreakClass.HebrewLetter) | + (1UL << (int)LineBreakClass.Numeric); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPrefixPostfixNumericOrSpace(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.PostfixNumeric) | + (1UL << (int)LineBreakClass.PrefixNumeric) | + (1UL << (int)LineBreakClass.Space); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPrefixPostfixNumeric(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.PostfixNumeric) | + (1UL << (int)LineBreakClass.PrefixNumeric); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClosePunctuationOrParenthesis(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.ClosePunctuation) | + (1UL << (int)LineBreakClass.CloseParenthesis); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsClosePunctuationOrInfixNumericOrBreakSymbols(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.ClosePunctuation) | + (1UL << (int)LineBreakClass.InfixNumeric) | + (1UL << (int)LineBreakClass.BreakSymbols); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSpaceOrWordJoinerOrAlphabetic(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.WordJoiner) | + (1UL << (int)LineBreakClass.Alphabetic); + + return ((1UL << (int)cls) & mask) != 0UL; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMandatoryBreakOrLineFeedOrCarriageReturn(LineBreakClass cls) + { + const ulong mask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.CarriageReturn); + + return ((1UL << (int)cls) & mask) != 0UL; + } private LineBreakClass PeekNextCharClass() { @@ -198,83 +282,77 @@ namespace Avalonia.Media.TextFormatting.Unicode // Track combining mark exceptions. LB22 if (cls == LineBreakClass.CombiningMark) { - switch (_currentClass) + const ulong lb22ExMask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.ContingentBreak) | + (1UL << (int)LineBreakClass.Exclamation) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.NextLine) | + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.ZWSpace) | + (1UL << (int)LineBreakClass.CarriageReturn); + + if (((1UL << (int)_currentClass) & lb22ExMask) != 0UL) { - case LineBreakClass.MandatoryBreak: - case LineBreakClass.ContingentBreak: - case LineBreakClass.Exclamation: - case LineBreakClass.LineFeed: - case LineBreakClass.NextLine: - case LineBreakClass.Space: - case LineBreakClass.ZWSpace: - case LineBreakClass.CarriageReturn: - _lb22ex = true; - break; + _lb22ex = true; } - } - // Track combining mark exceptions. LB31 - if (_first && cls == LineBreakClass.CombiningMark) - { - _lb31 = true; + const ulong lb31Mask = + (1UL << (int)LineBreakClass.MandatoryBreak) | + (1UL << (int)LineBreakClass.ContingentBreak) | + (1UL << (int)LineBreakClass.Exclamation) | + (1UL << (int)LineBreakClass.LineFeed) | + (1UL << (int)LineBreakClass.NextLine) | + (1UL << (int)LineBreakClass.Space) | + (1UL << (int)LineBreakClass.ZWSpace) | + (1UL << (int)LineBreakClass.CarriageReturn) | + (1UL << (int)LineBreakClass.ZWJ); + + // Track combining mark exceptions. LB31 + if (_first || ((1UL << (int)_currentClass) & lb31Mask) != 0UL) + { + _lb31 = true; + } } - if (cls == LineBreakClass.CombiningMark) + if (_first) { - switch (_currentClass) + // Rule LB24 + if (IsClosePunctuationOrParenthesis(cls)) { - case LineBreakClass.MandatoryBreak: - case LineBreakClass.ContingentBreak: - case LineBreakClass.Exclamation: - case LineBreakClass.LineFeed: - case LineBreakClass.NextLine: - case LineBreakClass.Space: - case LineBreakClass.ZWSpace: - case LineBreakClass.CarriageReturn: - case LineBreakClass.ZWJ: - _lb31 = true; - break; + _lb24ex = true; } - } - if (_first - && (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) - { - _lb31 = true; + // Rule LB25 + if (IsClosePunctuationOrInfixNumericOrBreakSymbols(cls)) + { + _lb25ex = true; + } + + if (IsPrefixPostfixNumericOrSpace(cls)) + { + _lb31 = true; + } } - if (_currentClass == LineBreakClass.Alphabetic && - (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space)) + if (_currentClass == LineBreakClass.Alphabetic && IsPrefixPostfixNumericOrSpace(cls)) { _lb31 = true; } // Reset LB31 if next is U+0028 (Left Opening Parenthesis) if (_lb31 - && _currentClass != LineBreakClass.PostfixNumeric - && _currentClass != LineBreakClass.PrefixNumeric - && cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028) + && !IsPrefixPostfixNumeric(_currentClass) + && cls == LineBreakClass.OpenPunctuation + && cp.Value == 0x0028) { _lb31 = false; } - // Rule LB24 - if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis)) - { - _lb24ex = true; - } - - // Rule LB25 - if (_first - && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols)) - { - _lb25ex = true; - } - - if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic) + if (IsSpaceOrWordJoinerOrAlphabetic(cls)) { var next = PeekNextCharClass(); - if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols) + if (IsClosePunctuationOrInfixNumericOrBreakSymbols(next)) { _lb25ex = true; } @@ -295,6 +373,7 @@ namespace Avalonia.Media.TextFormatting.Unicode return cls; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool? GetSimpleBreak() { // handle classes not handled by the pair table @@ -317,6 +396,7 @@ namespace Avalonia.Media.TextFormatting.Unicode return null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] // quite long but only one usage private bool GetPairTableBreak(LineBreakClass lastClass) { // If not handled already, use the pair table @@ -477,8 +557,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var cls = cp.LineBreakClass; - if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed || - cls == LineBreakClass.CarriageReturn) + if (IsMandatoryBreakOrLineFeedOrCarriageReturn(cls)) { from -= count; } From dccad9aa5714e3b34a1e73f3dde8b7d4e75a27c3 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 21 Jan 2023 02:08:50 +0100 Subject: [PATCH 072/326] Perf: improved BidiAlgorithm by using less branches --- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 170 ++++++++---------- .../Media/TextFormatting/Unicode/BiDiData.cs | 37 ++-- 2 files changed, 89 insertions(+), 118 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 36e9e6eb79..3406432ce7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -870,74 +870,59 @@ namespace Avalonia.Media.TextFormatting.Unicode _runDirection = DirectionFromLevel(runLevel); _runLength = _runResolvedClasses.Length; - // By tracking the types of characters known to be in the current run, we can - // skip some of the rules that we know won't apply. The flags will be - // initialized while we're processing rule W1 below. - var hasEN = false; - var hasAL = false; - var hasES = false; - var hasCS = false; - var hasAN = false; - var hasET = false; - // Rule W1 // Also, set hasXX flags int i; var previousClass = sos; + const uint isolateMask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); + + const uint wRulesMask = + (1U << (int)BidiClass.EuropeanNumber) | + (1U << (int)BidiClass.ArabicLetter) | + (1U << (int)BidiClass.EuropeanSeparator) | + (1U << (int)BidiClass.CommonSeparator) | + (1U << (int)BidiClass.ArabicNumber) | + (1U << (int)BidiClass.EuropeanTerminator); + + uint wRules = 0; + for (i = 0; i < _runLength; i++) { var resolvedClass = _runResolvedClasses[i]; - - switch (resolvedClass) - { - case BidiClass.NonspacingMark: - _runResolvedClasses[i] = previousClass; - break; - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: + if (resolvedClass == BidiClass.NonspacingMark) + { + _runResolvedClasses[i] = previousClass; + } + else + { + var classBit = 1U << (int)resolvedClass; + if ((classBit & isolateMask) != 0U) + { previousClass = BidiClass.OtherNeutral; - break; - - case BidiClass.EuropeanNumber: - hasEN = true; - previousClass = resolvedClass; - break; - - case BidiClass.ArabicLetter: - hasAL = true; - previousClass = resolvedClass; - break; - - case BidiClass.EuropeanSeparator: - hasES = true; - previousClass = resolvedClass; - break; - - case BidiClass.CommonSeparator: - hasCS = true; - previousClass = resolvedClass; - break; - - case BidiClass.ArabicNumber: - hasAN = true; - previousClass = resolvedClass; - break; - - case BidiClass.EuropeanTerminator: - hasET = true; - previousClass = resolvedClass; - break; - - default: + } + else + { + wRules |= classBit & wRulesMask; previousClass = resolvedClass; - break; + } } } + // By tracking the types of characters known to be in the current run, we can + // skip some of the rules that we know won't apply. + var hasEN = (wRules & (1U << (int)BidiClass.EuropeanNumber)) != 0U; + var hasAL = (wRules & (1U << (int)BidiClass.ArabicLetter)) != 0U; + var hasES = (wRules & (1U << (int)BidiClass.EuropeanSeparator)) != 0U; + var hasCS = (wRules & (1U << (int)BidiClass.CommonSeparator)) != 0U; + var hasAN = (wRules & (1U << (int)BidiClass.ArabicNumber)) != 0U; + var hasET = (wRules & (1U << (int)BidiClass.EuropeanTerminator)) != 0U; + // Rule W2 if (hasEN) { @@ -1549,23 +1534,20 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsWhitespace(BidiClass biDiClass) { - switch (biDiClass) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.RightToLeftEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - case BidiClass.BoundaryNeutral: - case BidiClass.WhiteSpace: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat) | + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate) | + (1U << (int)BidiClass.BoundaryNeutral) | + (1U << (int)BidiClass.WhiteSpace); + + return ((1U << (int)biDiClass) & mask) != 0U; } /// @@ -1586,18 +1568,15 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsRemovedByX9(BidiClass biDiClass) { - switch (biDiClass) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.RightToLeftEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - case BidiClass.BoundaryNeutral: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat) | + (1U << (int)BidiClass.BoundaryNeutral); + + return ((1U << (int)biDiClass) & mask) != 0U; } /// @@ -1606,20 +1585,17 @@ namespace Avalonia.Media.TextFormatting.Unicode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsNeutralClass(BidiClass direction) { - switch (direction) - { - case BidiClass.ParagraphSeparator: - case BidiClass.SegmentSeparator: - case BidiClass.WhiteSpace: - case BidiClass.OtherNeutral: - case BidiClass.RightToLeftIsolate: - case BidiClass.LeftToRightIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - return true; - default: - return false; - } + const uint mask = + (1U << (int)BidiClass.ParagraphSeparator) | + (1U << (int)BidiClass.SegmentSeparator) | + (1U << (int)BidiClass.WhiteSpace) | + (1U << (int)BidiClass.OtherNeutral) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); + + return ((1U << (int)direction) & mask) != 0U; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 5cc222b813..214ea07c98 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -73,6 +73,19 @@ namespace Avalonia.Media.TextFormatting.Unicode // bracket values for all code points int i = Length; + + const uint embeddingMask = + (1U << (int)BidiClass.LeftToRightEmbedding) | + (1U << (int)BidiClass.LeftToRightOverride) | + (1U << (int)BidiClass.RightToLeftEmbedding) | + (1U << (int)BidiClass.RightToLeftOverride) | + (1U << (int)BidiClass.PopDirectionalFormat); + + const uint isolateMask = + (1U << (int)BidiClass.LeftToRightIsolate) | + (1U << (int)BidiClass.RightToLeftIsolate) | + (1U << (int)BidiClass.FirstStrongIsolate) | + (1U << (int)BidiClass.PopDirectionalIsolate); var codePointEnumerator = new CodepointEnumerator(text); @@ -85,27 +98,9 @@ namespace Avalonia.Media.TextFormatting.Unicode _classes[i] = dir; - switch (dir) - { - case BidiClass.LeftToRightEmbedding: - case BidiClass.LeftToRightOverride: - case BidiClass.RightToLeftEmbedding: - case BidiClass.RightToLeftOverride: - case BidiClass.PopDirectionalFormat: - { - HasEmbeddings = true; - break; - } - - case BidiClass.LeftToRightIsolate: - case BidiClass.RightToLeftIsolate: - case BidiClass.FirstStrongIsolate: - case BidiClass.PopDirectionalIsolate: - { - HasIsolates = true; - break; - } - } + var dirBit = 1U << (int)dir; + HasEmbeddings = (dirBit & embeddingMask) != 0U; + HasIsolates = (dirBit & isolateMask) != 0U; // Lookup paired bracket types var pbt = codepoint.PairedBracketType; From f951929d54ec51213c9db26dd85302350c05918c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sat, 21 Jan 2023 02:56:34 +0100 Subject: [PATCH 073/326] Perf: improved CodepointEnumerator --- .../TextFormatting/InterWordJustification.cs | 4 +- .../Media/TextFormatting/TextCharacters.cs | 6 +-- .../TextFormatting/TextEllipsisHelper.cs | 4 +- .../Media/TextFormatting/TextFormatterImpl.cs | 47 +++++++++---------- .../Media/TextFormatting/Unicode/BiDiData.cs | 4 +- .../Unicode/CodepointEnumerator.cs | 24 ++++------ .../Unicode/LineBreakEnumerator.cs | 15 +++--- .../LineBreakEnumuratorTests.cs | 47 ++++++++++--------- .../TextFormatting/TextFormatterTests.cs | 4 +- 9 files changed, 70 insertions(+), 85 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 7afb758038..efcd866bbc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -60,10 +60,8 @@ namespace Avalonia.Media.TextFormatting var lineBreakEnumerator = new LineBreakEnumerator(text.Span); - while (lineBreakEnumerator.MoveNext()) + while (lineBreakEnumerator.MoveNext(out var currentBreak)) { - var currentBreak = lineBreakEnumerator.Current; - if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length) { breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 94db739d4d..82cf3297fd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -110,14 +110,14 @@ namespace Avalonia.Media.TextFormatting var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span); - while (codepointEnumerator.MoveNext()) + while (codepointEnumerator.MoveNext(out var cp)) { - if (codepointEnumerator.Current.IsWhiteSpace) + if (cp.IsWhiteSpace) { continue; } - codepoint = codepointEnumerator.Current; + codepoint = cp; break; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index e6743f5533..47973e37b5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -48,9 +48,9 @@ namespace Avalonia.Media.TextFormatting var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak)) { - var nextBreakPosition = lineBreaker.Current.PositionMeasure; + var nextBreakPosition = lineBreak.PositionMeasure; if (nextBreakPosition == 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index bc19690196..7de842ab39 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -231,6 +231,7 @@ namespace Avalonia.Media.TextFormatting bidiAlgorithm.Reset(); var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); + var textShaper = TextShaper.Current; for (var index = 0; index < processedRuns.Count; index++) { @@ -272,7 +273,7 @@ namespace Avalonia.Media.TextFormatting properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); + ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns); break; } @@ -360,9 +361,9 @@ namespace Avalonia.Media.TextFormatting && x.BaselineAlignment == y.BaselineAlignment; private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, - TextShaperOptions options, RentedList results) + TextShaperOptions options, TextShaper textShaper, RentedList results) { - var shapedBuffer = TextShaper.Current.ShapeText(text, options); + var shapedBuffer = textShaper.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) { @@ -559,15 +560,13 @@ namespace Avalonia.Media.TextFormatting var lineBreakEnumerator = new LineBreakEnumerator(text.Span); - while (lineBreakEnumerator.MoveNext()) + while (lineBreakEnumerator.MoveNext(out lineBreak)) { - if (!lineBreakEnumerator.Current.Required) + if (!lineBreak.Required) { continue; } - lineBreak = lineBreakEnumerator.Current; - return lineBreak.PositionWrap >= textRun.Length || true; } @@ -704,20 +703,20 @@ namespace Avalonia.Media.TextFormatting { var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - if (lineBreaker.Current.Required && - currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) + if (lineBreak.Required && + currentLength + lineBreak.PositionMeasure <= measuredLength) { //Explicit break found breakFound = true; - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; break; } - if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength) + if (currentLength + lineBreak.PositionMeasure > measuredLength) { if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) { @@ -733,21 +732,21 @@ namespace Avalonia.Media.TextFormatting //Find next possible wrap position (overflow) if (index < textRuns.Count - 1) { - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreak.PositionWrap != currentRun.Length) { //We already found the next possible wrap position. breakFound = true; - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; break; } - while (lineBreaker.MoveNext() && index < textRuns.Count) + while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count) { - currentPosition += lineBreaker.Current.PositionWrap; + currentPosition += lineBreak.PositionWrap; - if (lineBreaker.Current.PositionWrap != currentRun.Length) + if (lineBreak.PositionWrap != currentRun.Length) { break; } @@ -766,7 +765,7 @@ namespace Avalonia.Media.TextFormatting } else { - currentPosition = currentLength + lineBreaker.Current.PositionWrap; + currentPosition = currentLength + lineBreak.PositionWrap; } breakFound = true; @@ -782,9 +781,9 @@ namespace Avalonia.Media.TextFormatting break; } - if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap) + if (lineBreak.PositionMeasure != lineBreak.PositionWrap) { - lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; + lastWrapPosition = currentLength + lineBreak.PositionWrap; } } @@ -806,18 +805,18 @@ namespace Avalonia.Media.TextFormatting var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); - var lineBreak = postSplitRuns?.Count > 0 ? + var textLineBreak = postSplitRuns?.Count > 0 ? new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : null; - if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) + if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) { - lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); + textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, - lineBreak); + textLineBreak); textLine.FinalizeLine(); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 214ea07c98..b8094056f2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -89,10 +89,8 @@ namespace Avalonia.Media.TextFormatting.Unicode var codePointEnumerator = new CodepointEnumerator(text); - while (codePointEnumerator.MoveNext()) + while (codePointEnumerator.MoveNext(out var codepoint)) { - var codepoint = codePointEnumerator.Current; - // Look up BiDiClass var dir = codepoint.BiDiClass; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index d21f30ab7e..47a2b7d46a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -4,35 +4,27 @@ namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private ReadOnlySpan _text; + private readonly ReadOnlySpan _text; + private int _offset; public CodepointEnumerator(ReadOnlySpan text) - { - _text = text; - Current = Codepoint.ReplacementCodepoint; - } - - /// - /// Gets the current . - /// - public Codepoint Current { get; private set; } + => _text = text; /// /// Moves to the next . /// /// - public bool MoveNext() + public bool MoveNext(out Codepoint codepoint) { - if (_text.IsEmpty) + if ((uint)_offset >= (uint)_text.Length) { - Current = Codepoint.ReplacementCodepoint; - + codepoint = Codepoint.ReplacementCodepoint; return false; } - Current = Codepoint.ReadAt(_text, 0, out var count); + codepoint = Codepoint.ReadAt(_text, _offset, out var count); - _text = _text.Slice(count); + _offset += count; return true; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 31ef47f47b..5e12b7458e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -47,10 +47,8 @@ namespace Avalonia.Media.TextFormatting.Unicode _lb30 = false; _lb30a = 0; } - - public LineBreak Current { get; private set; } - - public bool MoveNext() + + public bool MoveNext(out LineBreak lineBreak) { // Get the first char if we're at the beginning of the string. if (_first) @@ -76,7 +74,7 @@ namespace Avalonia.Media.TextFormatting.Unicode case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed: { _currentClass = MapFirst(_nextClass); - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true); return true; } } @@ -88,7 +86,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (shouldBreak) { - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition); return true; } } @@ -109,13 +107,12 @@ namespace Avalonia.Media.TextFormatting.Unicode break; } - Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); + lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required); return true; } } - Current = default; - + lineBreak = default; return false; } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs index d198fe81a6..3db9a32b65 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -24,32 +24,33 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting public void BasicLatinTest() { var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test."); + LineBreak lineBreak; - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(6, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(6, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(13, lineBreaker.Current.PositionWrap); - Assert.True(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(13, lineBreak.PositionWrap); + Assert.True(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(18, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(18, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(21, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(21, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(23, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(23, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.True(lineBreaker.MoveNext()); - Assert.Equal(28, lineBreaker.Current.PositionWrap); - Assert.False(lineBreaker.Current.Required); + Assert.True(lineBreaker.MoveNext(out lineBreak)); + Assert.Equal(28, lineBreak.PositionWrap); + Assert.False(lineBreak.Required); - Assert.False(lineBreaker.MoveNext()); + Assert.False(lineBreaker.MoveNext(out lineBreak)); } @@ -72,9 +73,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { var breaks = new List(); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - breaks.Add(lineBreaker.Current); + breaks.Add(lineBreak); } return breaks; @@ -104,9 +105,9 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var foundBreaks = new List(); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - foundBreaks.Add(lineBreaker.Current.PositionWrap); + foundBreaks.Add(lineBreak.PositionWrap); } // Check the same diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 6b9fb579b1..7822d6624b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -283,9 +283,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expected = new List(); - while (lineBreaker.MoveNext()) + while (lineBreaker.MoveNext(out var lineBreak)) { - expected.Add(lineBreaker.Current.PositionWrap - 1); + expected.Add(lineBreak.PositionWrap - 1); } var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + From 7a1f74a3d3952141aaeaee886865d384321176b1 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 22 Jan 2023 13:36:46 +0100 Subject: [PATCH 074/326] Benchmarks: option to use Skia for text layout --- src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 1 + .../Avalonia.Benchmarks.csproj | 1 + .../Text/HugeTextLayout.cs | 32 +++++++++++++------ tests/Avalonia.UnitTests/MockGlyphRun.cs | 10 ++++-- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index ffe8352865..4c3cfe2ef4 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 941d377a17..0ddee2ad7a 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 0adabc75f1..4dad8442de 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -3,6 +3,7 @@ using System.Linq; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using Avalonia.Skia; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -13,24 +14,35 @@ namespace Avalonia.Benchmarks.Text; [MaxWarmupCount(15)] public class HugeTextLayout : IDisposable { + private static readonly Random s_rand = new(); + private static readonly bool s_useSkia = true; + private readonly IDisposable _app; - private string[] _manySmallStrings; - private static Random _rand = new Random(); - + private readonly string[] _manySmallStrings; + private static string RandomString(int length) { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&?%$@"; - return new string(Enumerable.Repeat(chars, length).Select(s => s[_rand.Next(s.Length)]).ToArray()); + return new string(Enumerable.Repeat(chars, length).Select(s => s[s_rand.Next(s.Length)]).ToArray()); } public HugeTextLayout() { - _manySmallStrings = Enumerable.Range(0, 1000).Select(x => RandomString(_rand.Next(2, 15))).ToArray(); - _app = UnitTestApplication.Start( - TestServices.StyledWindow.With( - renderInterface: new NullRenderingPlatform(), - threadingInterface: new NullThreadingPlatform(), - standardCursorFactory: new NullCursorFactory())); + _manySmallStrings = Enumerable.Range(0, 1000).Select(_ => RandomString(s_rand.Next(2, 15))).ToArray(); + + var testServices = TestServices.StyledWindow.With( + renderInterface: new NullRenderingPlatform(), + threadingInterface: new NullThreadingPlatform(), + standardCursorFactory: new NullCursorFactory()); + + if (s_useSkia) + { + testServices = testServices.With( + textShaperImpl: new TextShaperImpl(), + fontManagerImpl: new FontManagerImpl()); + } + + _app = UnitTestApplication.Start(testServices); } private const string Text = @"Though, the objectives of the development of the prominent landmarks can be neglected in most cases, it should be realized that after the completion of the strategic decision gives rise to The Expertise of Regular Program (Carlton Cartwright in The Book of the Key Factor) diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 45e7b47f62..477f34565f 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Avalonia.Media.TextFormatting; using Avalonia.Platform; @@ -9,7 +8,14 @@ namespace Avalonia.UnitTests { public MockGlyphRun(IReadOnlyList glyphInfos) { - Size = new Size(glyphInfos.Sum(x=> x.GlyphAdvance), 10); + var width = 0.0; + + for (var i = 0; i < glyphInfos.Count; ++i) + { + width += glyphInfos[i].GlyphAdvance; + } + + Size = new Size(width, 10); } public Size Size { get; } From 3c7c9b458a4a57f9a7a50684056aef91431f275a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 22 Jan 2023 14:18:07 -0800 Subject: [PATCH 075/326] Update ObservableStreamPlugin.cs --- src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 2b9da0a61a..9cf25281f2 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -15,7 +15,7 @@ namespace Avalonia.Data.Core.Plugins private static MethodInfo? s_observableGeneric; private static MethodInfo? s_observableSelect; - [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")] + [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")] public ObservableStreamPlugin() { From a24e48f1056ae0984a98ccf79d4043c46ca9cfe2 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 23 Jan 2023 00:03:45 +0100 Subject: [PATCH 076/326] Text layout: ensure RentedList are returned in case of exceptions --- .../TextFormatting/FormattingObjectPool.cs | 11 +- .../TextFormatting/TextEllipsisHelper.cs | 20 ++- .../Media/TextFormatting/TextFormatterImpl.cs | 154 ++++++++-------- .../Media/TextFormatting/TextLayout.cs | 167 +++++++++--------- .../TextLeadingPrefixCharacterEllipsis.cs | 12 +- 5 files changed, 194 insertions(+), 170 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs index cb8168e693..c7cd58eb6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -93,16 +93,19 @@ namespace Avalonia.Media.TextFormatting [Conditional("DEBUG")] public void VerifyAllReturned() { - if (_pendingReturnCount > 0) + var pendingReturnCount = _pendingReturnCount; + _pendingReturnCount = 0; + + if (pendingReturnCount > 0) { throw new InvalidOperationException( - $"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!"); + $"{pendingReturnCount} RentedList<{typeof(T).Name}> haven't been returned to the pool!"); } - if (_pendingReturnCount < 0) + if (pendingReturnCount < 0) { throw new InvalidOperationException( - $"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!"); + $"{-pendingReturnCount} RentedList<{typeof(T).Name}> extra lists have been returned to the pool!"); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 47973e37b5..4c93a1d851 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -113,14 +113,18 @@ namespace Avalonia.Media.TextFormatting var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); - var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; - preSplitRuns.CopyTo(collapsedRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - - objectPool.TextRunLists.Return(ref preSplitRuns); - objectPool.TextRunLists.Return(ref postSplitRuns); - - return collapsedRuns; + try + { + var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } + finally + { + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + } } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7de842ab39..7505b9ccdd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -32,58 +32,64 @@ namespace Avalonia.Media.TextFormatting var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); - RentedList? shapedTextRuns; + RentedList? shapedTextRuns = null; - if (previousLineBreak?.RemainingRuns is { } remainingRuns) + try { - resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = remainingRuns; - nextLineBreak = previousLineBreak; - shapedTextRuns = null; - } - else - { - shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out resolvedFlowDirection); - textRuns = shapedTextRuns; - - if (nextLineBreak == null && textEndOfLine != null) + if (previousLineBreak?.RemainingRuns is { } remainingRuns) { - nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); + resolvedFlowDirection = previousLineBreak.FlowDirection; + textRuns = remainingRuns; + nextLineBreak = previousLineBreak; + shapedTextRuns = null; } - } + else + { + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, + out resolvedFlowDirection); + textRuns = shapedTextRuns; - TextLineImpl textLine; + if (nextLineBreak == null && textEndOfLine != null) + { + nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); + } + } - switch (textWrapping) - { - case TextWrapping.NoWrap: + TextLineImpl textLine; + + switch (textWrapping) { - // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + case TextWrapping.NoWrap: + { + // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); - textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); + textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); - textLine.FinalizeLine(); + textLine.FinalizeLine(); - break; - } - case TextWrapping.WrapWithOverflow: - case TextWrapping.Wrap: - { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); - break; + break; + } + case TextWrapping.WrapWithOverflow: + case TextWrapping.Wrap: + { + textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(textWrapping)); } - default: - throw new ArgumentOutOfRangeException(nameof(textWrapping)); - } - - objectPool.TextRunLists.Return(ref shapedTextRuns); - objectPool.TextRunLists.Return(ref fetchedRuns); - return textLine; + return textLine; + } + finally + { + objectPool.TextRunLists.Return(ref shapedTextRuns); + objectPool.TextRunLists.Return(ref fetchedRuns); + } } /// @@ -224,23 +230,26 @@ namespace Avalonia.Media.TextFormatting (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; var processedRuns = objectPool.TextRunLists.Rent(); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); - CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns); + try + { + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns); - bidiData.Reset(); - bidiAlgorithm.Reset(); + bidiData.Reset(); + bidiAlgorithm.Reset(); - var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); - var textShaper = TextShaper.Current; - for (var index = 0; index < processedRuns.Count; index++) - { - var currentRun = processedRuns[index]; + var textShaper = TextShaper.Current; - switch (currentRun) + for (var index = 0; index < processedRuns.Count; index++) { - case UnshapedTextRun shapeableRun: + var currentRun = processedRuns[index]; + + switch (currentRun) { + case UnshapedTextRun shapeableRun: + { groupedRuns.Clear(); groupedRuns.Add(shapeableRun); @@ -277,17 +286,20 @@ namespace Avalonia.Media.TextFormatting break; } - default: + default: { shapedRuns.Add(currentRun); break; } + } } } - - objectPool.TextRunLists.Return(ref processedRuns); - objectPool.UnshapedTextRunLists.Return(ref groupedRuns); + finally + { + objectPool.TextRunLists.Return(ref processedRuns); + objectPool.UnshapedTextRunLists.Return(ref groupedRuns); + } return shapedRuns; } @@ -805,25 +817,29 @@ namespace Avalonia.Media.TextFormatting var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); - var textLineBreak = postSplitRuns?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : - null; - - if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) + try { - textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); - } + var textLineBreak = postSplitRuns?.Count > 0 ? + new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : + null; - var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, - textLineBreak); - - textLine.FinalizeLine(); + if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) + { + textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); + } - objectPool.TextRunLists.Return(ref preSplitRuns); - objectPool.TextRunLists.Return(ref postSplitRuns); + var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, + textLineBreak); - return textLine; + textLine.FinalizeLine(); + return textLine; + } + finally + { + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + } } private struct TextRunEnumerator diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index bb58e0d692..4923cdbe32 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -441,128 +441,133 @@ namespace Avalonia.Media.TextFormatting var textLines = objectPool.TextLines.Rent(); - double left = double.PositiveInfinity, width = 0.0, height = 0.0; - - _textSourceLength = 0; + try + { + double left = double.PositiveInfinity, width = 0.0, height = 0.0; - TextLine? previousLine = null; + _textSourceLength = 0; - var textFormatter = TextFormatter.Current; + TextLine? previousLine = null; - while (true) - { - var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, - previousLine?.TextLineBreak); + var textFormatter = TextFormatter.Current; - if (textLine.Length == 0) + while (true) { - if (previousLine != null && previousLine.NewLineLength > 0) + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, + _paragraphProperties, previousLine?.TextLineBreak); + + if (textLine.Length == 0) { - var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, - _paragraphProperties, fontManager); + if (previousLine != null && previousLine.NewLineLength > 0) + { + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, + _paragraphProperties, fontManager); - textLines.Add(emptyTextLine); + textLines.Add(emptyTextLine); - UpdateBounds(emptyTextLine, ref left, ref width, ref height); - } + UpdateBounds(emptyTextLine, ref left, ref width, ref height); + } - break; - } + break; + } - _textSourceLength += textLine.Length; + _textSourceLength += textLine.Length; - //Fulfill max height constraint - if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight) - { - if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) + //Fulfill max height constraint + if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) + && height + textLine.Height > MaxHeight) { - var collapsedLine = - previousLine.Collapse(GetCollapsingProperties(MaxWidth)); + if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) + { + var collapsedLine = + previousLine.Collapse(GetCollapsingProperties(MaxWidth)); - textLines[textLines.Count - 1] = collapsedLine; - } + textLines[textLines.Count - 1] = collapsedLine; + } - break; - } + break; + } - var hasOverflowed = textLine.HasOverflowed; + var hasOverflowed = textLine.HasOverflowed; - if (hasOverflowed && _textTrimming != TextTrimming.None) - { - textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); - } + if (hasOverflowed && _textTrimming != TextTrimming.None) + { + textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); + } - textLines.Add(textLine); + textLines.Add(textLine); - UpdateBounds(textLine, ref left, ref width, ref height); + UpdateBounds(textLine, ref left, ref width, ref height); - previousLine = textLine; + previousLine = textLine; - //Fulfill max lines constraint - if (MaxLines > 0 && textLines.Count >= MaxLines) - { - if(textLine.TextLineBreak?.RemainingRuns is not null) + //Fulfill max lines constraint + if (MaxLines > 0 && textLines.Count >= MaxLines) { - textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); + if (textLine.TextLineBreak?.RemainingRuns is not null) + { + textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); + } + + break; } - break; + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + { + break; + } } - if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph) + //Make sure the TextLayout always contains at least on empty line + if (textLines.Count == 0) { - break; - } - } + var textLine = + TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager); - //Make sure the TextLayout always contains at least on empty line - if (textLines.Count == 0) - { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager); - - textLines.Add(textLine); - - UpdateBounds(textLine, ref left, ref width, ref height); - } + textLines.Add(textLine); - Bounds = new Rect(left, 0, width, height); + UpdateBounds(textLine, ref left, ref width, ref height); + } - if (_paragraphProperties.TextAlignment == TextAlignment.Justify) - { - var whitespaceWidth = 0d; + Bounds = new Rect(left, 0, width, height); - for (var i = 0; i < textLines.Count; i++) + if (_paragraphProperties.TextAlignment == TextAlignment.Justify) { - var line = textLines[i]; - var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; + var whitespaceWidth = 0d; - if (lineWhitespaceWidth > whitespaceWidth) + for (var i = 0; i < textLines.Count; i++) { - whitespaceWidth = lineWhitespaceWidth; - } - } + var line = textLines[i]; + var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; - var justificationWidth = width - whitespaceWidth; + if (lineWhitespaceWidth > whitespaceWidth) + { + whitespaceWidth = lineWhitespaceWidth; + } + } - if (justificationWidth > 0) - { - var justificationProperties = new InterWordJustification(justificationWidth); + var justificationWidth = width - whitespaceWidth; - for (var i = 0; i < textLines.Count - 1; i++) + if (justificationWidth > 0) { - var line = textLines[i]; + var justificationProperties = new InterWordJustification(justificationWidth); - line.Justify(justificationProperties); + for (var i = 0; i < textLines.Count - 1; i++) + { + var line = textLines[i]; + + line.Justify(justificationProperties); + } } } - } - var result = textLines.ToArray(); - - objectPool.TextLines.Return(ref textLines); - objectPool.VerifyAllReturned(); - - return result; + return textLines.ToArray(); + } + finally + { + objectPool.TextLines.Return(ref textLines); + objectPool.VerifyAllReturned(); + } } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 0d777ad043..2e85b1e187 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -86,7 +86,6 @@ namespace Avalonia.Media.TextFormatting RentedList? rentedPreSplitRuns = null; RentedList? rentedPostSplitRuns = null; - TextRun[]? results; try { @@ -113,9 +112,7 @@ namespace Avalonia.Media.TextFormatting if (measuredLength <= _prefixLength || effectivePostSplitRuns is null) { - results = collapsedRuns.ToArray(); - objectPool.TextRunLists.Return(ref collapsedRuns); - return results; + return collapsedRuns.ToArray(); } var availableSuffixWidth = availableWidth; @@ -157,16 +154,15 @@ namespace Avalonia.Media.TextFormatting } } } + + return collapsedRuns.ToArray(); } finally { objectPool.TextRunLists.Return(ref rentedPreSplitRuns); objectPool.TextRunLists.Return(ref rentedPostSplitRuns); + objectPool.TextRunLists.Return(ref collapsedRuns); } - - results = collapsedRuns.ToArray(); - objectPool.TextRunLists.Return(ref collapsedRuns); - return results; } return new TextRun[] { shapedSymbol }; From 79661f6d851aef68a605560c1ba00c16bbcbaffb Mon Sep 17 00:00:00 2001 From: Jeff Hube Date: Sun, 22 Jan 2023 23:36:46 -0500 Subject: [PATCH 077/326] Fix TraceLogSink not releasing StringBuilder --- src/Avalonia.Base/Logging/TraceLogSink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Logging/TraceLogSink.cs b/src/Avalonia.Base/Logging/TraceLogSink.cs index fc3897fade..a1b4dfe3aa 100644 --- a/src/Avalonia.Base/Logging/TraceLogSink.cs +++ b/src/Avalonia.Base/Logging/TraceLogSink.cs @@ -141,7 +141,7 @@ namespace Avalonia.Logging result.Append(')'); } - return result.ToString(); + return StringBuilderCache.GetStringAndRelease(result); } } } From adefd574b69ba0e984e3b57547999c4277482f88 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Jan 2023 11:25:16 +0100 Subject: [PATCH 078/326] Devirtualize AvaloniaProperty properties. For performance reasons. --- src/Avalonia.Base/AttachedProperty.cs | 4 +--- src/Avalonia.Base/AvaloniaProperty.cs | 8 ++++---- src/Avalonia.Base/DirectProperty.cs | 13 ++++--------- src/Avalonia.Base/DirectPropertyBase.cs | 6 +++--- src/Avalonia.Base/StyledPropertyBase.cs | 17 ++--------------- 5 files changed, 14 insertions(+), 34 deletions(-) diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs index a43194153c..31b6cad8ab 100644 --- a/src/Avalonia.Base/AttachedProperty.cs +++ b/src/Avalonia.Base/AttachedProperty.cs @@ -24,11 +24,9 @@ namespace Avalonia Func? validate = null) : base(name, ownerType, metadata, inherits, validate) { + IsAttached = true; } - /// - public override bool IsAttached => true; - /// /// Attaches the property as a non-attached property on the specified type. /// diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index e0782c51a2..5db4d81f03 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -107,22 +107,22 @@ namespace Avalonia /// /// Gets a value indicating whether the property inherits its value. /// - public virtual bool Inherits => false; + public bool Inherits { get; private protected set; } /// /// Gets a value indicating whether this is an attached property. /// - public virtual bool IsAttached => false; + public bool IsAttached { get; private protected set; } /// /// Gets a value indicating whether this is a direct property. /// - public virtual bool IsDirect => false; + public bool IsDirect { get; private protected set; } /// /// Gets a value indicating whether this is a readonly property. /// - public virtual bool IsReadOnly => false; + public bool IsReadOnly { get; private protected set; } /// /// Gets an observable that is fired when this property changes on any diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index 729240e5a1..d02e277074 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -33,6 +33,8 @@ namespace Avalonia { Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Setter = setter; + IsDirect = true; + IsReadOnly = setter is null; } /// @@ -51,17 +53,10 @@ namespace Avalonia { Getter = getter ?? throw new ArgumentNullException(nameof(getter)); Setter = setter; + IsDirect = true; + IsReadOnly = setter is null; } - /// - public override bool IsDirect => true; - - /// - public override bool IsReadOnly => Setter == null; - - /// - public override Type Owner => typeof(TOwner); - /// /// Gets the getter function. /// diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index ec9eba6d61..9ee1eee0fa 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -1,8 +1,6 @@ using System; using Avalonia.Data; using Avalonia.PropertyStore; -using Avalonia.Reactive; -using Avalonia.Styling; namespace Avalonia { @@ -28,6 +26,7 @@ namespace Avalonia AvaloniaPropertyMetadata metadata) : base(name, ownerType, metadata) { + Owner = ownerType; } /// @@ -42,12 +41,13 @@ namespace Avalonia AvaloniaPropertyMetadata metadata) : base(source, ownerType, metadata) { + Owner = ownerType; } /// /// Gets the type that registered the property. /// - public abstract Type Owner { get; } + public Type Owner { get; } /// /// Gets the value of the property on the instance. diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index a281a7b7f6..b39f45189c 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -1,10 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Avalonia.Data; using Avalonia.PropertyStore; -using Avalonia.Reactive; -using Avalonia.Styling; using Avalonia.Utilities; namespace Avalonia @@ -14,8 +11,6 @@ namespace Avalonia /// public abstract class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor { - private readonly bool _inherits; - /// /// Initializes a new instance of the class. /// @@ -34,7 +29,7 @@ namespace Avalonia Action? notifying = null) : base(name, ownerType, metadata, notifying) { - _inherits = inherits; + Inherits = inherits; ValidateValue = validate; HasCoercion |= metadata.CoerceValue != null; @@ -53,17 +48,9 @@ namespace Avalonia protected StyledPropertyBase(StyledPropertyBase source, Type ownerType) : base(source, ownerType, null) { - _inherits = source.Inherits; + Inherits = source.Inherits; } - /// - /// Gets a value indicating whether the property inherits its value. - /// - /// - /// A value indicating whether the property inherits its value. - /// - public override bool Inherits => _inherits; - /// /// Gets the value validation callback for the property. /// From 17c3291c80acbfe7e112a366eb30a69924adea9a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Jan 2023 12:06:40 +0100 Subject: [PATCH 079/326] Remove StyledPropertyBase class. Originally `StyledPropertyBase` was the base class for `StyledProperty` and `AttachedProperty` however #1499 made `AttachedProperty` derive directly from `StyledProperty` meaning that there is no longer any need for a separate `StyledPropertyBase` class. --- src/Avalonia.Base/AvaloniaObject.cs | 16 +- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 8 +- .../PropertyStore/EffectiveValue`1.cs | 20 +- .../PropertyStore/ImmediateValueEntry.cs | 4 +- .../PropertyStore/ImmediateValueFrame.cs | 8 +- .../LocalValueBindingObserver.cs | 6 +- .../LocalValueUntypedBindingObserver.cs | 4 +- .../SourceUntypedBindingEntry.cs | 4 +- .../PropertyStore/TypedBindingEntry.cs | 6 +- .../PropertyStore/UntypedValueUtils.cs | 2 +- src/Avalonia.Base/PropertyStore/ValueStore.cs | 18 +- src/Avalonia.Base/StyledProperty.cs | 208 ++++++++++++++- src/Avalonia.Base/StyledPropertyBase.cs | 237 ------------------ .../Styling/PropertySetterInstance.cs | 4 +- .../AvaloniaXamlIlWellKnownTypes.cs | 2 +- 15 files changed, 248 insertions(+), 299 deletions(-) delete mode 100644 src/Avalonia.Base/StyledPropertyBase.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index dc94dfba40..1946d4ba5c 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -132,7 +132,7 @@ namespace Avalonia switch (property) { - case StyledPropertyBase styled: + case StyledProperty styled: ClearValue(styled); break; case DirectPropertyBase direct: @@ -147,7 +147,7 @@ namespace Avalonia /// Clears a 's local value. /// /// The property. - public void ClearValue(StyledPropertyBase property) + public void ClearValue(StyledProperty property) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); @@ -220,7 +220,7 @@ namespace Avalonia /// The type of the property. /// The property. /// The value. - public T GetValue(StyledPropertyBase property) + public T GetValue(StyledProperty property) { _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); @@ -243,7 +243,7 @@ namespace Avalonia } /// - public Optional GetBaseValue(StyledPropertyBase property) + public Optional GetBaseValue(StyledProperty property) { _ = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); @@ -309,7 +309,7 @@ namespace Avalonia /// An if setting the property can be undone, otherwise null. /// public IDisposable? SetValue( - StyledPropertyBase property, + StyledProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) { @@ -373,7 +373,7 @@ namespace Avalonia /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { @@ -396,7 +396,7 @@ namespace Avalonia /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { @@ -419,7 +419,7 @@ namespace Avalonia /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( - StyledPropertyBase property, + StyledProperty property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 7b17b9152d..6231483ff8 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -146,7 +146,7 @@ namespace Avalonia return property switch { - StyledPropertyBase styled => target.Bind(styled, source, priority), + StyledProperty styled => target.Bind(styled, source, priority), DirectPropertyBase direct => target.Bind(direct, source), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), }; @@ -170,7 +170,7 @@ namespace Avalonia { return property switch { - StyledPropertyBase styled => target.Bind(styled, source, priority), + StyledProperty styled => target.Bind(styled, source, priority), DirectPropertyBase direct => target.Bind(direct, source), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type."), }; @@ -231,7 +231,7 @@ namespace Avalonia return property switch { - StyledPropertyBase styled => target.GetValue(styled), + StyledProperty styled => target.GetValue(styled), DirectPropertyBase direct => target.GetValue(direct), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") }; @@ -280,7 +280,7 @@ namespace Avalonia return property switch { - StyledPropertyBase styled => target.GetBaseValue(styled), + StyledProperty styled => target.GetBaseValue(styled), DirectPropertyBase direct => target.GetValue(direct), _ => throw new NotSupportedException("Unsupported AvaloniaProperty type.") }; diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 93fffb3755..3e20dcce56 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -19,7 +19,7 @@ namespace Avalonia.PropertyStore private T? _baseValue; private UncommonFields? _uncommon; - public EffectiveValue(AvaloniaObject owner, StyledPropertyBase property) + public EffectiveValue(AvaloniaObject owner, StyledProperty property) { Priority = BindingPriority.Unset; BasePriority = BindingPriority.Unset; @@ -57,12 +57,12 @@ namespace Avalonia.PropertyStore Debug.Assert(priority != BindingPriority.LocalValue); UpdateValueEntry(value, priority); - SetAndRaiseCore(owner, (StyledPropertyBase)value.Property, GetValue(value), priority); + SetAndRaiseCore(owner, (StyledProperty)value.Property, GetValue(value), priority); } public void SetLocalValueAndRaise( ValueStore owner, - StyledPropertyBase property, + StyledProperty property, T value) { SetAndRaiseCore(owner, property, value, BindingPriority.LocalValue); @@ -82,7 +82,7 @@ namespace Avalonia.PropertyStore { Debug.Assert(oldValue is not null || newValue is not null); - var p = (StyledPropertyBase)property; + var p = (StyledProperty)property; var o = oldValue is not null ? ((EffectiveValue)oldValue).Value : _metadata.DefaultValue; var n = newValue is not null ? ((EffectiveValue)newValue).Value : _metadata.DefaultValue; var priority = newValue is not null ? BindingPriority.Inherited : BindingPriority.Unset; @@ -98,7 +98,7 @@ namespace Avalonia.PropertyStore Debug.Assert(Priority != BindingPriority.Animation); Debug.Assert(BasePriority != BindingPriority.Unset); UpdateValueEntry(null, BindingPriority.Animation); - SetAndRaiseCore(owner, (StyledPropertyBase)property, _baseValue!, BasePriority); + SetAndRaiseCore(owner, (StyledProperty)property, _baseValue!, BasePriority); } public override void CoerceValue(ValueStore owner, AvaloniaProperty property) @@ -107,7 +107,7 @@ namespace Avalonia.PropertyStore return; SetAndRaiseCore( owner, - (StyledPropertyBase)property, + (StyledProperty)property, _uncommon._uncoercedValue!, Priority, _uncommon._uncoercedBaseValue!, @@ -117,10 +117,10 @@ namespace Avalonia.PropertyStore public override void DisposeAndRaiseUnset(ValueStore owner, AvaloniaProperty property) { UnsubscribeValueEntries(); - DisposeAndRaiseUnset(owner, (StyledPropertyBase)property); + DisposeAndRaiseUnset(owner, (StyledProperty)property); } - public void DisposeAndRaiseUnset(ValueStore owner, StyledPropertyBase property) + public void DisposeAndRaiseUnset(ValueStore owner, StyledProperty property) { BindingPriority priority; T oldValue; @@ -156,7 +156,7 @@ namespace Avalonia.PropertyStore private void SetAndRaiseCore( ValueStore owner, - StyledPropertyBase property, + StyledProperty property, T value, BindingPriority priority) { @@ -203,7 +203,7 @@ namespace Avalonia.PropertyStore private void SetAndRaiseCore( ValueStore owner, - StyledPropertyBase property, + StyledProperty property, T value, BindingPriority priority, T baseValue, diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs index 364b4e1225..d8a353dc70 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueEntry.cs @@ -9,7 +9,7 @@ namespace Avalonia.PropertyStore public ImmediateValueEntry( ImmediateValueFrame owner, - StyledPropertyBase property, + StyledProperty property, T value) { _owner = owner; @@ -17,7 +17,7 @@ namespace Avalonia.PropertyStore Property = property; } - public StyledPropertyBase Property { get; } + public StyledProperty Property { get; } public bool HasValue => true; AvaloniaProperty IValueEntry.Property => Property; diff --git a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs index 50d5333b9f..7e9f3ab312 100644 --- a/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs +++ b/src/Avalonia.Base/PropertyStore/ImmediateValueFrame.cs @@ -15,7 +15,7 @@ namespace Avalonia.PropertyStore } public TypedBindingEntry AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable> source) { var e = new TypedBindingEntry(this, property, source); @@ -24,7 +24,7 @@ namespace Avalonia.PropertyStore } public TypedBindingEntry AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source) { var e = new TypedBindingEntry(this, property, source); @@ -33,7 +33,7 @@ namespace Avalonia.PropertyStore } public SourceUntypedBindingEntry AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source) { var e = new SourceUntypedBindingEntry(this, property, source); @@ -41,7 +41,7 @@ namespace Avalonia.PropertyStore return e; } - public ImmediateValueEntry AddValue(StyledPropertyBase property, T value) + public ImmediateValueEntry AddValue(StyledProperty property, T value) { var e = new ImmediateValueEntry(this, property, value); Add(e); diff --git a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs index 8acb885604..f89cb029b6 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueBindingObserver.cs @@ -11,13 +11,13 @@ namespace Avalonia.PropertyStore private readonly ValueStore _owner; private IDisposable? _subscription; - public LocalValueBindingObserver(ValueStore owner, StyledPropertyBase property) + public LocalValueBindingObserver(ValueStore owner, StyledProperty property) { _owner = owner; Property = property; } - public StyledPropertyBase Property { get;} + public StyledProperty Property { get;} public void Start(IObservable source) { @@ -41,7 +41,7 @@ namespace Avalonia.PropertyStore public void OnNext(T value) { - static void Execute(ValueStore owner, StyledPropertyBase property, T value) + static void Execute(ValueStore owner, StyledProperty property, T value) { if (property.ValidateValue?.Invoke(value) != false) owner.SetValue(property, value, BindingPriority.LocalValue); diff --git a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs index 7c529591b6..2d157b2519 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueUntypedBindingObserver.cs @@ -11,13 +11,13 @@ namespace Avalonia.PropertyStore private readonly ValueStore _owner; private IDisposable? _subscription; - public LocalValueUntypedBindingObserver(ValueStore owner, StyledPropertyBase property) + public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty property) { _owner = owner; Property = property; } - public StyledPropertyBase Property { get; } + public StyledProperty Property { get; } public void Start(IObservable source) { diff --git a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs index b4ac06d2bf..b56d0d4529 100644 --- a/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/SourceUntypedBindingEntry.cs @@ -13,14 +13,14 @@ namespace Avalonia.PropertyStore public SourceUntypedBindingEntry( ValueFrame frame, - StyledPropertyBase property, + StyledProperty property, IObservable source) : base(frame, property, source) { _validate = property.ValidateValue; } - public new StyledPropertyBase Property => (StyledPropertyBase)base.Property; + public new StyledProperty Property => (StyledProperty)base.Property; protected override BindingValue ConvertAndValidate(object? value) { diff --git a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs index 2276991a18..697725c87b 100644 --- a/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs +++ b/src/Avalonia.Base/PropertyStore/TypedBindingEntry.cs @@ -11,7 +11,7 @@ namespace Avalonia.PropertyStore { public TypedBindingEntry( ValueFrame frame, - StyledPropertyBase property, + StyledProperty property, IObservable source) : base(frame, property, source) { @@ -19,13 +19,13 @@ namespace Avalonia.PropertyStore public TypedBindingEntry( ValueFrame frame, - StyledPropertyBase property, + StyledProperty property, IObservable> source) : base(frame, property, source) { } - public new StyledPropertyBase Property => (StyledPropertyBase)base.Property; + public new StyledProperty Property => (StyledProperty)base.Property; protected override BindingValue ConvertAndValidate(T value) { diff --git a/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs index 5c5591dcb5..372a808fb2 100644 --- a/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs +++ b/src/Avalonia.Base/PropertyStore/UntypedValueUtils.cs @@ -26,7 +26,7 @@ namespace Avalonia.PropertyStore [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] public static bool TryConvertAndValidate( - StyledPropertyBase property, + StyledProperty property, object? value, [MaybeNullWhen(false)] out T result) { diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 92e5288255..f36a96992b 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -43,7 +43,7 @@ namespace Avalonia.PropertyStore } public IDisposable AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable> source, BindingPriority priority) { @@ -71,7 +71,7 @@ namespace Avalonia.PropertyStore } public IDisposable AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority) { @@ -99,7 +99,7 @@ namespace Avalonia.PropertyStore } public IDisposable AddBinding( - StyledPropertyBase property, + StyledProperty property, IObservable source, BindingPriority priority) { @@ -165,7 +165,7 @@ namespace Avalonia.PropertyStore } } - public IDisposable? SetValue(StyledPropertyBase property, T value, BindingPriority priority) + public IDisposable? SetValue(StyledProperty property, T value, BindingPriority priority) { if (property.ValidateValue?.Invoke(value) == false) { @@ -219,7 +219,7 @@ namespace Avalonia.PropertyStore return GetDefaultValue(property); } - public T GetValue(StyledPropertyBase property) + public T GetValue(StyledProperty property) { if (_effectiveValues.TryGetValue(property, out var v)) return ((EffectiveValue)v).Value; @@ -248,7 +248,7 @@ namespace Avalonia.PropertyStore v.CoerceValue(this, property); } - public Optional GetBaseValue(StyledPropertyBase property) + public Optional GetBaseValue(StyledProperty property) { if (TryGetEffectiveValue(property, out var v) && ((EffectiveValue)v).TryGetBaseValue(out var baseValue)) @@ -450,7 +450,7 @@ namespace Avalonia.PropertyStore /// The old value of the property. /// The effective value instance. public void OnInheritedEffectiveValueChanged( - StyledPropertyBase property, + StyledProperty property, T oldValue, EffectiveValue value) { @@ -475,7 +475,7 @@ namespace Avalonia.PropertyStore /// /// The property whose value changed. /// The old value of the property. - public void OnInheritedEffectiveValueDisposed(StyledPropertyBase property, T oldValue) + public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue) { Debug.Assert(property.Inherits); @@ -520,7 +520,7 @@ namespace Avalonia.PropertyStore /// The old value of the property. /// The new value of the property. public void OnAncestorInheritedValueChanged( - StyledPropertyBase property, + StyledProperty property, T oldValue, T newValue) { diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs index 019ed09c20..79d1b9202d 100644 --- a/src/Avalonia.Base/StyledProperty.cs +++ b/src/Avalonia.Base/StyledProperty.cs @@ -1,14 +1,18 @@ using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; +using Avalonia.PropertyStore; +using Avalonia.Utilities; namespace Avalonia { /// /// A styled avalonia property. /// - public class StyledProperty : StyledPropertyBase + public class StyledProperty : AvaloniaProperty, IStyledPropertyAccessor { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the property. /// The type of the class that registers the property. @@ -23,20 +27,30 @@ namespace Avalonia bool inherits = false, Func? validate = null, Action? notifying = null) - : base(name, ownerType, metadata, inherits, validate, notifying) + : base(name, ownerType, metadata, notifying) { + Inherits = inherits; + ValidateValue = validate; + HasCoercion |= metadata.CoerceValue != null; + + if (validate?.Invoke(metadata.DefaultValue) == false) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); + } } /// - /// Initializes a new instance of the class. + /// Gets the value validation callback for the property. /// - /// The property to add the owner to. - /// The type of the class that registers the property. - internal StyledProperty(StyledPropertyBase source, Type ownerType) - : base(source, ownerType) - { - } - + public Func? ValidateValue { get; } + + /// + /// Gets a value indicating whether this property has any value coercion callbacks defined + /// in its metadata. + /// + internal bool HasCoercion { get; private set; } + /// /// Registers the property on another type. /// @@ -47,5 +61,177 @@ namespace Avalonia AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this); return this; } + + public TValue CoerceValue(AvaloniaObject instance, TValue baseValue) + { + var metadata = GetMetadata(instance.GetType()); + + if (metadata.CoerceValue != null) + { + return metadata.CoerceValue.Invoke(instance, baseValue); + } + + return baseValue; + } + + /// + /// Gets the default value for the property on the specified type. + /// + /// The type. + /// The default value. + public TValue GetDefaultValue(Type type) + { + return GetMetadata(type).DefaultValue; + } + + /// + /// Gets the property metadata for the specified type. + /// + /// The type. + /// + /// The property metadata. + /// + public new StyledPropertyMetadata GetMetadata(Type type) + { + _ = type ?? throw new ArgumentNullException(nameof(type)); + return (StyledPropertyMetadata)base.GetMetadata(type); + } + + /// + /// Overrides the default value for the property on the specified type. + /// + /// The type. + /// The default value. + public void OverrideDefaultValue(TValue defaultValue) where T : AvaloniaObject + { + OverrideDefaultValue(typeof(T), defaultValue); + } + + /// + /// Overrides the default value for the property on the specified type. + /// + /// The type. + /// The default value. + public void OverrideDefaultValue(Type type, TValue defaultValue) + { + OverrideMetadata(type, new StyledPropertyMetadata(defaultValue)); + } + + /// + /// Overrides the metadata for the property on the specified type. + /// + /// The type. + /// The metadata. + public void OverrideMetadata(StyledPropertyMetadata metadata) where T : AvaloniaObject + { + base.OverrideMetadata(typeof(T), metadata); + } + + /// + /// Overrides the metadata for the property on the specified type. + /// + /// The type. + /// The metadata. + public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) + { + if (ValidateValue != null) + { + if (!ValidateValue(metadata.DefaultValue)) + { + throw new ArgumentException( + $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); + } + } + + HasCoercion |= metadata.CoerceValue != null; + + base.OverrideMetadata(type, metadata); + } + + /// + /// Gets the string representation of the property. + /// + /// The property's string representation. + public override string ToString() + { + return Name; + } + + /// + object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + bool IStyledPropertyAccessor.ValidateValue(object? value) + { + if (value is null && !typeof(TValue).IsValueType) + return ValidateValue?.Invoke(default!) ?? true; + if (value is TValue typed) + return ValidateValue?.Invoke(typed) ?? true; + return false; + } + + internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) + { + return new EffectiveValue(o, this); + } + + /// + internal override void RouteClearValue(AvaloniaObject o) + { + o.ClearValue(this); + } + + /// + internal override object? RouteGetValue(AvaloniaObject o) + { + return o.GetValue(this); + } + + /// + internal override object? RouteGetBaseValue(AvaloniaObject o) + { + var value = o.GetBaseValue(this); + return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; + } + + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] + internal override IDisposable? RouteSetValue( + AvaloniaObject target, + object? value, + BindingPriority priority) + { + if (value == BindingOperations.DoNothing) + { + return null; + } + else if (value == UnsetValue) + { + target.ClearValue(this); + return null; + } + else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) + { + return target.SetValue(this, (TValue)converted!, priority); + } + else + { + var type = value?.GetType().FullName ?? "(null)"; + throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); + } + } + + internal override IDisposable RouteBind( + AvaloniaObject target, + IObservable source, + BindingPriority priority) + { + return target.Bind(this, source, priority); + } + + private object? GetDefaultBoxedValue(Type type) + { + _ = type ?? throw new ArgumentNullException(nameof(type)); + return GetMetadata(type).DefaultValue; + } } } diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs deleted file mode 100644 index b39f45189c..0000000000 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Avalonia.Data; -using Avalonia.PropertyStore; -using Avalonia.Utilities; - -namespace Avalonia -{ - /// - /// Base class for styled properties. - /// - public abstract class StyledPropertyBase : AvaloniaProperty, IStyledPropertyAccessor - { - /// - /// Initializes a new instance of the class. - /// - /// The name of the property. - /// The type of the class that registers the property. - /// The property metadata. - /// Whether the property inherits its value. - /// A value validation callback. - /// A callback. - protected StyledPropertyBase( - string name, - Type ownerType, - StyledPropertyMetadata metadata, - bool inherits = false, - Func? validate = null, - Action? notifying = null) - : base(name, ownerType, metadata, notifying) - { - Inherits = inherits; - ValidateValue = validate; - HasCoercion |= metadata.CoerceValue != null; - - if (validate?.Invoke(metadata.DefaultValue) == false) - { - throw new ArgumentException( - $"'{metadata.DefaultValue}' is not a valid default value for '{name}'."); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The property to add the owner to. - /// The type of the class that registers the property. - protected StyledPropertyBase(StyledPropertyBase source, Type ownerType) - : base(source, ownerType, null) - { - Inherits = source.Inherits; - } - - /// - /// Gets the value validation callback for the property. - /// - public Func? ValidateValue { get; } - - /// - /// Gets a value indicating whether this property has any value coercion callbacks defined - /// in its metadata. - /// - internal bool HasCoercion { get; private set; } - - public TValue CoerceValue(AvaloniaObject instance, TValue baseValue) - { - var metadata = GetMetadata(instance.GetType()); - - if (metadata.CoerceValue != null) - { - return metadata.CoerceValue.Invoke(instance, baseValue); - } - - return baseValue; - } - - /// - /// Gets the default value for the property on the specified type. - /// - /// The type. - /// The default value. - public TValue GetDefaultValue(Type type) - { - return GetMetadata(type).DefaultValue; - } - - /// - /// Gets the property metadata for the specified type. - /// - /// The type. - /// - /// The property metadata. - /// - public new StyledPropertyMetadata GetMetadata(Type type) - { - _ = type ?? throw new ArgumentNullException(nameof(type)); - return (StyledPropertyMetadata)base.GetMetadata(type); - } - - /// - /// Overrides the default value for the property on the specified type. - /// - /// The type. - /// The default value. - public void OverrideDefaultValue(TValue defaultValue) where T : AvaloniaObject - { - OverrideDefaultValue(typeof(T), defaultValue); - } - - /// - /// Overrides the default value for the property on the specified type. - /// - /// The type. - /// The default value. - public void OverrideDefaultValue(Type type, TValue defaultValue) - { - OverrideMetadata(type, new StyledPropertyMetadata(defaultValue)); - } - - /// - /// Overrides the metadata for the property on the specified type. - /// - /// The type. - /// The metadata. - public void OverrideMetadata(StyledPropertyMetadata metadata) where T : AvaloniaObject - { - base.OverrideMetadata(typeof(T), metadata); - } - - /// - /// Overrides the metadata for the property on the specified type. - /// - /// The type. - /// The metadata. - public void OverrideMetadata(Type type, StyledPropertyMetadata metadata) - { - if (ValidateValue != null) - { - if (!ValidateValue(metadata.DefaultValue)) - { - throw new ArgumentException( - $"'{metadata.DefaultValue}' is not a valid default value for '{Name}'."); - } - } - - HasCoercion |= metadata.CoerceValue != null; - - base.OverrideMetadata(type, metadata); - } - - /// - /// Gets the string representation of the property. - /// - /// The property's string representation. - public override string ToString() - { - return Name; - } - - /// - object? IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); - - bool IStyledPropertyAccessor.ValidateValue(object? value) - { - if (value is null && !typeof(TValue).IsValueType) - return ValidateValue?.Invoke(default!) ?? true; - if (value is TValue typed) - return ValidateValue?.Invoke(typed) ?? true; - return false; - } - - internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) - { - return new EffectiveValue(o, this); - } - - /// - internal override void RouteClearValue(AvaloniaObject o) - { - o.ClearValue(this); - } - - /// - internal override object? RouteGetValue(AvaloniaObject o) - { - return o.GetValue(this); - } - - /// - internal override object? RouteGetBaseValue(AvaloniaObject o) - { - var value = o.GetBaseValue(this); - return value.HasValue ? value.Value : AvaloniaProperty.UnsetValue; - } - - /// - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] - internal override IDisposable? RouteSetValue( - AvaloniaObject target, - object? value, - BindingPriority priority) - { - if (value == BindingOperations.DoNothing) - { - return null; - } - else if (value == UnsetValue) - { - target.ClearValue(this); - return null; - } - else if (TypeUtilities.TryConvertImplicit(PropertyType, value, out var converted)) - { - return target.SetValue(this, (TValue)converted!, priority); - } - else - { - var type = value?.GetType().FullName ?? "(null)"; - throw new ArgumentException($"Invalid value for Property '{Name}': '{value}' ({type})"); - } - } - - internal override IDisposable RouteBind( - AvaloniaObject target, - IObservable source, - BindingPriority priority) - { - return target.Bind(this, source, priority); - } - - private object? GetDefaultBoxedValue(Type type) - { - _ = type ?? throw new ArgumentNullException(nameof(type)); - return GetMetadata(type).DefaultValue; - } - } -} diff --git a/src/Avalonia.Base/Styling/PropertySetterInstance.cs b/src/Avalonia.Base/Styling/PropertySetterInstance.cs index 68a9b8aafe..af5540ecf0 100644 --- a/src/Avalonia.Base/Styling/PropertySetterInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterInstance.cs @@ -14,7 +14,7 @@ namespace Avalonia.Styling ISetterInstance { private readonly StyledElement _target; - private readonly StyledPropertyBase? _styledProperty; + private readonly StyledProperty? _styledProperty; private readonly DirectPropertyBase? _directProperty; private readonly T _value; private IDisposable? _subscription; @@ -22,7 +22,7 @@ namespace Avalonia.Styling public PropertySetterInstance( StyledElement target, - StyledPropertyBase property, + StyledProperty property, T value) { _target = target; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index aab6239a35..0b61316603 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -126,7 +126,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers AvaloniaObjectSetStyledPropertyValue = AvaloniaObject .FindMethod(m => m.IsPublic && !m.IsStatic && m.Name == "SetValue" && m.Parameters.Count == 3 - && m.Parameters[0].Name == "StyledPropertyBase`1" + && m.Parameters[0].Name == "StyledProperty`1" && m.Parameters[2].Equals(BindingPriority)); IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding"); IDisposable = cfg.TypeSystem.GetType("System.IDisposable"); From 51e9f8b61193fdc1144b49487e95d2b5e4b69192 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 23 Jan 2023 12:20:14 +0000 Subject: [PATCH 080/326] add irregular snap points to virtualizing stack panel --- src/Avalonia.Controls/ListBox.cs | 5 +- .../VirtualizingStackPanel.cs | 68 ++++++++++++------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 775e0ae0cb..8b1a307182 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -21,10 +21,7 @@ namespace Avalonia.Controls /// The default value for the property. /// private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new VirtualizingStackPanel() - { - AreVerticalSnapPointsRegular= true, - }); + new FuncTemplate(() => new VirtualizingStackPanel()); /// /// Defines the property. diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index f60c67b577..a59fffa704 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -703,21 +703,29 @@ namespace Avalonia.Controls throw new InvalidOperationException(); if (Orientation == Orientation.Horizontal) { - foreach (var child in VisualChildren) + var averageElementSize = EstimateElementSizeU(); + double snapPoint = 0; + for (var i = 0; i < Items.Count; i++) { - double snapPoint = 0; - - switch (snapPointsAlignment) + var container = ContainerFromIndex(i); + if (container != null) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = container.Bounds.Left; + break; + case SnapPointsAlignment.Center: + snapPoint = container.Bounds.Center.X; + break; + case SnapPointsAlignment.Far: + snapPoint = container.Bounds.Right; + break; + } + } + else { - case SnapPointsAlignment.Near: - snapPoint = child.Bounds.Left; - break; - case SnapPointsAlignment.Center: - snapPoint = child.Bounds.Center.X; - break; - case SnapPointsAlignment.Far: - snapPoint = child.Bounds.Right; - break; + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); @@ -729,21 +737,29 @@ namespace Avalonia.Controls throw new InvalidOperationException(); if (Orientation == Orientation.Vertical) { - foreach (var child in VisualChildren) + var averageElementSize = EstimateElementSizeU(); + double snapPoint = 0; + for (var i = 0; i < Items.Count; i++) { - double snapPoint = 0; - - switch (snapPointsAlignment) + var container = ContainerFromIndex(i); + if (container != null) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = container.Bounds.Top; + break; + case SnapPointsAlignment.Center: + snapPoint = container.Bounds.Center.Y; + break; + case SnapPointsAlignment.Far: + snapPoint = container.Bounds.Bottom; + break; + } + } + else { - case SnapPointsAlignment.Near: - snapPoint = child.Bounds.Top; - break; - case SnapPointsAlignment.Center: - snapPoint = child.Bounds.Center.Y; - break; - case SnapPointsAlignment.Far: - snapPoint = child.Bounds.Bottom; - break; + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); From feddc7e1c48c3d478bc94873e08be8fe2aea3f10 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 23 Jan 2023 13:46:46 +0000 Subject: [PATCH 081/326] make AreVerticalSnapPointsRegular and AreHorizontalSnapPointsRegular styled properties --- src/Avalonia.Controls/ItemsControl.cs | 35 ++++++++++++- .../Presenters/ItemsPresenter.cs | 49 ++++++++++++++++++- .../Primitives/IScrollSnapPointsInfo.cs | 8 +-- .../VirtualizingStackPanel.cs | 32 ++++++++++-- .../Controls/ItemsControl.xaml | 2 + .../Controls/ListBox.xaml | 3 +- .../Controls/ItemsControl.xaml | 2 + .../Controls/ListBox.xaml | 6 ++- 8 files changed, 124 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1948fda928..db49da85e8 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -74,6 +74,18 @@ namespace Avalonia.Controls /// public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Gets or sets the to use for binding to the display member of each item. @@ -94,6 +106,7 @@ namespace Avalonia.Controls private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; + private IScrollSnapPointsInfo? _scrolSnapPointInfo; /// /// Initializes a new instance of the class. @@ -245,6 +258,24 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } + /// /// Returns the container for the item at the specified index. /// @@ -296,8 +327,6 @@ namespace Avalonia.Controls /// Gets the currently realized containers. /// public IEnumerable GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty(); - public bool AreHorizontalSnapPointsRegular => _itemsPresenter?.AreHorizontalSnapPointsRegular ?? false; - public bool AreVerticalSnapPointsRegular => _itemsPresenter?.AreVerticalSnapPointsRegular ?? false; /// /// Creates or a container that can be used to display an item. @@ -399,6 +428,8 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); + + _scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e3332ef3a2..8594b584fa 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -21,8 +21,21 @@ namespace Avalonia.Controls.Presenters private PanelContainerGenerator? _generator; private ILogicalScrollable? _logicalScrollable; + private IScrollSnapPointsInfo? _scrollSnapPointsInfo; private EventHandler? _scrollInvalidated; + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + /// /// Defines the event. /// @@ -125,8 +138,23 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; - public bool AreHorizontalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreHorizontalSnapPointsRegular : false; - public bool AreVerticalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreVerticalSnapPointsRegular : false; + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } public override sealed void ApplyTemplate() { @@ -139,9 +167,16 @@ namespace Avalonia.Controls.Presenters Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); + _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); VisualChildren.Add(Panel); + if (_scrollSnapPointsInfo != null) + { + _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular; + _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular; + } + if (Panel is VirtualizingPanel v) v.Attach(ItemsControl); else @@ -205,6 +240,16 @@ namespace Avalonia.Controls.Presenters ResetState(); InvalidateMeasure(); } + else if(change.Property == AreHorizontalSnapPointsRegularProperty) + { + if (_scrollSnapPointsInfo != null) + _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular; + } + else if (change.Property == AreVerticalSnapPointsRegularProperty) + { + if (_scrollSnapPointsInfo != null) + _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular; + } } internal void Refresh() diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs index d0462aff9e..7b33db0df2 100644 --- a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs +++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs @@ -11,14 +11,14 @@ namespace Avalonia.Controls.Primitives public interface IScrollSnapPointsInfo { /// - /// Gets a value that indicates whether the horizontal snap points for the container are equidistant from each other. + /// Gets or sets a value that indicates whether the horizontal snap points for the container are equidistant from each other. /// - bool AreHorizontalSnapPointsRegular { get; } + bool AreHorizontalSnapPointsRegular { get; set; } /// - /// Gets a value that indicates whether the vertical snap points for the container are equidistant from each other. + /// Gets or sets a value that indicates whether the vertical snap points for the container are equidistant from each other. /// - bool AreVerticalSnapPointsRegular { get; } + bool AreVerticalSnapPointsRegular { get; set; } /// /// Returns the set of distances between irregular snap points for a specified orientation and alignment. diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index a59fffa704..6bcf3dcf5d 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Defines the event. @@ -725,7 +725,20 @@ namespace Avalonia.Controls } else { - snapPoint += averageElementSize; + if (snapPoint == 0) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Center: + snapPoint = averageElementSize / 2; + break; + case SnapPointsAlignment.Far: + snapPoint = averageElementSize; + break; + } + } + else + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); @@ -759,7 +772,20 @@ namespace Avalonia.Controls } else { - snapPoint += averageElementSize; + if (snapPoint == 0) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Center: + snapPoint = averageElementSize / 2; + break; + case SnapPointsAlignment.Far: + snapPoint = averageElementSize; + break; + } + } + else + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); diff --git a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml index 1306985f5f..19a29b9466 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml @@ -9,6 +9,8 @@ CornerRadius="{TemplateBinding CornerRadius}" Padding="{TemplateBinding Padding}"> diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index dc18d65797..4b9fb76b8a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -19,7 +19,6 @@ - @@ -37,6 +36,8 @@ IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml index d85cf193fa..8cf4e0be08 100644 --- a/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml @@ -10,6 +10,8 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml index e1257ef10f..eaa1f914ca 100644 --- a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml @@ -20,9 +20,13 @@ Background="{TemplateBinding Background}" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" - VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + VerticalSnapPointsType="{TemplateBinding (ScrollViewer.VerticalSnapPointsType)}" + HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}"> From 65d8e46fa67766db48453b939ef4807365073270 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Mon, 23 Jan 2023 14:42:29 +0300 Subject: [PATCH 082/326] VirtualizingStackPanel: fix selection wrapping --- src/Avalonia.Controls/VirtualizingStackPanel.cs | 2 +- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 3f539ce198..f2b42b0b7e 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -226,7 +226,7 @@ namespace Avalonia.Controls { if (toIndex < 0) toIndex = count - 1; - else if (toIndex >= count - 1) + else if (toIndex >= count) toIndex = 0; } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 3f1a3b6342..8170545f68 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -759,6 +759,7 @@ namespace Avalonia.Controls.UnitTests var lbItems = target.GetLogicalChildren().OfType().ToArray(); var first = lbItems.First(); + var beforeLast = lbItems[^2]; var last = lbItems.Last(); first.Focus(); @@ -769,6 +770,12 @@ namespace Avalonia.Controls.UnitTests RaiseKeyEvent(target, Key.Up); Assert.Equal(true, last.IsSelected); + RaiseKeyEvent(target, Key.Up); + Assert.Equal(true, beforeLast.IsSelected); + + RaiseKeyEvent(target, Key.Down); + Assert.Equal(true, last.IsSelected); + RaiseKeyEvent(target, Key.Down); Assert.Equal(true, first.IsSelected); From 042edf132b94c32d31b67c64c7bdae88d379c7bb Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:07:51 +0100 Subject: [PATCH 083/326] Remove unnecessary ItemGroup --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index e1e0a636ea..a77de9bb6a 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -24,8 +24,4 @@ - - - - From 4baed8b569e4f613c066e4fd67013e2a8d863c33 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:12:55 +0100 Subject: [PATCH 084/326] Delete DBusMenu.cs --- src/Avalonia.FreeDesktop/DBusMenu.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/Avalonia.FreeDesktop/DBusMenu.cs diff --git a/src/Avalonia.FreeDesktop/DBusMenu.cs b/src/Avalonia.FreeDesktop/DBusMenu.cs deleted file mode 100644 index e69de29bb2..0000000000 From c978887dd6e47e089e272718a2de404872392de3 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:13:06 +0100 Subject: [PATCH 085/326] Make GetProperty static --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 6f1810d1c1..deac7346cb 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -207,7 +207,7 @@ namespace Avalonia.FreeDesktop "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; - private DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + private static DBusVariantItem? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; From 9a26fef972a8d02328b26574927c0e338407fabb Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 24 Jan 2023 01:19:48 +0100 Subject: [PATCH 086/326] Remove NetAnalyzers.props import --- src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a77de9bb6a..c0f82a6e67 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -6,7 +6,6 @@ - From cb884c6e9e5bd55984edf0ae660b587451f96a92 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 24 Jan 2023 10:40:53 +0100 Subject: [PATCH 087/326] Reintroduce customizable GlyphRun.BaselineOrigin Reintroduce Brush IBrush inheritance --- src/Avalonia.Base/Media/Brush.cs | 2 +- src/Avalonia.Base/Media/GlyphRun.cs | 37 ++++++++++++++----- .../Media/TextFormatting/ShapedTextRun.cs | 2 +- .../Platform/IPlatformRenderInterface.cs | 3 +- .../HeadlessPlatformRenderInterface.cs | 6 ++- .../Avalonia.Skia/PlatformRenderInterface.cs | 7 +++- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 6 +-- .../Media/GlyphRunTests.cs | 2 +- .../VisualTree/MockRenderInterface.cs | 3 +- .../NullRenderingPlatform.cs | 3 +- .../Media/GlyphRunTests.cs | 2 +- .../MockPlatformRenderInterface.cs | 3 +- 12 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index b9a560ad8f..accabce145 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media /// Describes how an area is painted. /// [TypeConverter(typeof(BrushConverter))] - public abstract class Brush : Animatable + public abstract class Brush : Animatable, IBrush { /// /// Defines the property. diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 65575617d0..0ec7152359 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -13,14 +13,22 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { + private readonly static IPlatformRenderInterface s_renderInterface; + private IRef? _platformImpl; private double _fontRenderingEmSize; private int _biDiLevel; private GlyphRunMetrics? _glyphRunMetrics; private ReadOnlyMemory _characters; private IReadOnlyList _glyphInfos; + private Point? _baselineOrigin; private bool _hasOneCharPerCluster; // if true, character index and cluster are similar + static GlyphRun() + { + s_renderInterface = AvaloniaLocator.Current.GetRequiredService(); + } + /// /// Initializes a new instance of the class by specifying properties of the class. /// @@ -28,15 +36,17 @@ namespace Avalonia.Media /// The rendering em size. /// The characters. /// The glyph indices. + /// The baseline origin of the run. /// The bidi level. public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphIndices, + Point? baselineOrigin = null, int biDiLevel = 0) : this(glyphTypeface, fontRenderingEmSize, characters, - CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel) + CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), baselineOrigin, biDiLevel) { _hasOneCharPerCluster = true; } @@ -48,12 +58,14 @@ namespace Avalonia.Media /// The rendering em size. /// The characters. /// The list of glyphs used. + /// The baseline origin of the run. /// The bidi level. public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphInfos, + Point? baselineOrigin = null, int biDiLevel = 0) { GlyphTypeface = glyphTypeface; @@ -64,6 +76,8 @@ namespace Avalonia.Media _glyphInfos = glyphInfos; + _baselineOrigin = baselineOrigin; + _biDiLevel = biDiLevel; } @@ -72,6 +86,7 @@ namespace Avalonia.Media _glyphInfos = Array.Empty(); GlyphTypeface = Typeface.Default.GlyphTypeface; _platformImpl = platformImpl; + _baselineOrigin = platformImpl.Item.BaselineOrigin; } private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, @@ -147,9 +162,13 @@ namespace Avalonia.Media => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// - /// Gets the baseline origin of the. + /// Gets or sets the baseline origin of the. /// - public Point BaselineOrigin => PlatformImpl.Item.BaselineOrigin; + public Point BaselineOrigin + { + get => _baselineOrigin ?? default; + set => Set(ref _baselineOrigin, value); + } /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . @@ -204,9 +223,7 @@ namespace Avalonia.Media /// The geometry returned contains the combined geometry of all glyphs in the glyph run. public Geometry BuildGeometry() { - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - - var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this); + var geometryImpl = s_renderInterface.BuildGlyphRunGeometry(this); return new PlatformGeometry(geometryImpl); } @@ -802,9 +819,11 @@ namespace Avalonia.Media private IRef CreateGlyphRunImpl() { - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - - var platformImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); + var platformImpl = s_renderInterface.CreateGlyphRun( + GlyphTypeface, + FontRenderingEmSize, + GlyphInfos, + _baselineOrigin ?? new Point(0, -GlyphTypeface.Metrics.Ascent * Scale)); _platformImpl = RefCountable.Create(platformImpl); diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index ac196bf7e0..7f23ac98b4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -185,7 +185,7 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.FontRenderingEmSize, Text, ShapedBuffer, - BidiLevel); + biDiLevel: BidiLevel); } public void Dispose() diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index e2160f21d2..41e792d58e 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -168,8 +168,9 @@ namespace Avalonia.Platform /// The glyph typeface. /// The font rendering em size. /// The list of glyphs. + /// The baseline origin of the run. Can be null. /// An . - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos); + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin); /// /// Creates a backend-specific object using a low-level API graphics context diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 572ff1c876..514d3b3e07 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -120,7 +120,11 @@ namespace Avalonia.Headless return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) + public IGlyphRunImpl CreateGlyphRun( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + IReadOnlyList glyphInfos, + Point baselineOrigin) { return new HeadlessGlyphRunStub(); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 6630f0707e..b4297a7c33 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -201,7 +201,11 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) + public IGlyphRunImpl CreateGlyphRun( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + IReadOnlyList glyphInfos, + Point baselineOrigin) { if (glyphTypeface == null) { @@ -252,7 +256,6 @@ namespace Avalonia.Skia var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; var height = glyphTypeface.Metrics.LineSpacing * scale; - var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale); return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 461950b728..fbf8097ece 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -158,7 +158,8 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, + IReadOnlyList glyphInfos, Point baselineOrigin) { var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; @@ -207,7 +208,6 @@ namespace Avalonia.Direct2D1 var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; var height = glyphTypeface.Metrics.LineSpacing * scale; - var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale); return new GlyphRunImpl(run, new Size(width, height), baselineOrigin); } @@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1 sink.Close(); } - var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin; + var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin; var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry( Direct2D1Factory, diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index a05bfbea4c..43feb75c08 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -188,7 +188,7 @@ namespace Avalonia.Base.UnitTests.Media glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); } - return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel); + return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index ee501a86c1..76c7fe97fc 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -77,7 +77,8 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, + IReadOnlyList glyphInfos, Point baselineOrigin) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 9b148c798b..37b79855db 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -123,7 +123,8 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, + IReadOnlyList glyphInfos, Point baselineOrigin) { return new MockGlyphRun(glyphInfos); } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 59c7ac3786..bfe03030c6 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -217,7 +217,7 @@ namespace Avalonia.Skia.UnitTests.Media shapedBuffer.FontRenderingEmSize, shapedBuffer.Text, shapedBuffer.GlyphInfos, - shapedBuffer.BidiLevel); + biDiLevel: shapedBuffer.BidiLevel); if(shapedBuffer.BidiLevel == 1) { diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 30f949ccb8..93073faefb 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -149,7 +149,8 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, + IReadOnlyList glyphInfos, Point baselineOrigin) { return new MockGlyphRun(glyphInfos); } From 19b3dc335ad08355ac413c9d173e8d6cc9ab9bc4 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 24 Jan 2023 13:24:24 +0200 Subject: [PATCH 088/326] Don't process events when TopLevel is disposed. --- src/Avalonia.Controls/TopLevel.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index ff241dce7a..c2e997464c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -581,12 +581,21 @@ namespace Avalonia.Controls /// The event args. private void HandleInput(RawInputEventArgs e) { - if (e is RawPointerEventArgs pointerArgs) + if (PlatformImpl != null) { - pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); - } + if (e is RawPointerEventArgs pointerArgs) + { + pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); + } - _inputManager?.ProcessInput(e); + _inputManager?.ProcessInput(e); + } + else + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log( + this, + "PlatformImpl is null, couldn't handle input."); + } } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) From ef5734e223f1c72006e88c64949866656fb4da68 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 24 Jan 2023 14:06:57 +0200 Subject: [PATCH 089/326] Make TopLevel not can be in MirrorTransform state --- src/Avalonia.Controls/TopLevel.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index ff241dce7a..fc88055e01 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -606,6 +606,13 @@ namespace Avalonia.Controls KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); } + protected override bool BypassFlowDirectionPolicies => true; + + public override void InvalidateMirrorTransform() + { + // Do nothing becuase TopLevel should't apply MirrorTransform on himself. + } + ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => (PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod; } From 7fd6d43e3fd9db2f06adaaa7433247579c8151d5 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 24 Jan 2023 14:25:05 +0200 Subject: [PATCH 090/326] Apply FD on MainWindow instead MainView in ControlCatalog --- samples/ControlCatalog/MainView.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 1726028a3f..4b2f9bb7c3 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -60,7 +60,7 @@ namespace ControlCatalog { if (flowDirections.SelectedItem is FlowDirection flowDirection) { - this.FlowDirection = flowDirection; + ((TopLevel)this.VisualRoot!).FlowDirection = flowDirection; } }; From 23ab4294b1122befb80d5da20bb8db1357846a64 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 24 Jan 2023 12:46:23 +0000 Subject: [PATCH 091/326] move snapping samples to separate page --- samples/ControlCatalog/MainView.xaml | 5 +- .../ControlCatalog/Pages/ScrollSnapPage.xaml | 222 ++++++++++++++++++ .../Pages/ScrollSnapPage.xaml.cs | 67 ++++++ .../Pages/ScrollViewerPage.xaml | 136 ----------- .../Pages/ScrollViewerPage.xaml.cs | 37 --- 5 files changed, 293 insertions(+), 174 deletions(-) create mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml create mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 166b98436e..83776ec2c1 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -144,9 +144,12 @@ - + + + + diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml new file mode 100644 index 0000000000..e7e1060f31 --- /dev/null +++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml @@ -0,0 +1,222 @@ + + + Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen, or using the pointer wheel. + + + + + + + + + + + + + + + + Vertical Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs new file mode 100644 index 0000000000..97aeb6bcdb --- /dev/null +++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using MiniMvvm; + +namespace ControlCatalog.Pages +{ + public class ScrollSnapPageViewModel : ViewModelBase + { + private SnapPointsType _snapPointsType; + private SnapPointsAlignment _snapPointsAlignment; + private bool _areSnapPointsRegular; + + public ScrollSnapPageViewModel() + { + + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + } + + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + + public SnapPointsType SnapPointsType + { + get => _snapPointsType; + set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); + } + + public SnapPointsAlignment SnapPointsAlignment + { + get => _snapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); + } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } + } + public class ScrollSnapPage : UserControl + { + public ScrollSnapPage() + { + this.InitializeComponent(); + + DataContext = new ScrollSnapPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 0eceb9e07d..1741c796db 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -31,141 +31,5 @@ Source="/Assets/delicate-arch-896885_640.jpg" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index 10244af5f3..dcd7a88a56 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -11,9 +11,6 @@ namespace ControlCatalog.Pages private bool _allowAutoHide; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; - private SnapPointsType _verticalSnapPointsType; - private SnapPointsAlignment _verticalSnapPointsAlignment; - private bool _areSnapPointsRegular; public ScrollViewerPageViewModel() { @@ -25,20 +22,6 @@ namespace ControlCatalog.Pages ScrollBarVisibility.Disabled, }; - AvailableSnapPointsType = new List() - { - SnapPointsType.None, - SnapPointsType.Mandatory, - SnapPointsType.MandatorySingle - }; - - AvailableSnapPointsAlignment = new List() - { - SnapPointsAlignment.Near, - SnapPointsAlignment.Center, - SnapPointsAlignment.Far, - }; - HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; @@ -50,12 +33,6 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); } - public bool AreSnapPointsRegular - { - get => _areSnapPointsRegular; - set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); - } - public ScrollBarVisibility HorizontalScrollVisibility { get => _horizontalScrollVisibility; @@ -68,21 +45,7 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _verticalScrollVisibility, value); } - public SnapPointsType VerticalSnapPointsType - { - get => _verticalSnapPointsType; - set => this.RaiseAndSetIfChanged(ref _verticalSnapPointsType, value); - } - - public SnapPointsAlignment VerticalSnapPointsAlignment - { - get => _verticalSnapPointsAlignment; - set => this.RaiseAndSetIfChanged(ref _verticalSnapPointsAlignment, value); - } - public List AvailableVisibility { get; } - public List AvailableSnapPointsType { get; } - public List AvailableSnapPointsAlignment { get; } } public class ScrollViewerPage : UserControl From baab50e94cfd2ca10b134ed71be87bcc8217a257 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 24 Jan 2023 13:08:53 +0000 Subject: [PATCH 092/326] fix center snap alignment --- samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs | 1 + src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs index 97aeb6bcdb..384dc67c66 100644 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs @@ -50,6 +50,7 @@ namespace ControlCatalog.Pages public List AvailableSnapPointsType { get; } public List AvailableSnapPointsAlignment { get; } } + public class ScrollSnapPage : UserControl { public ScrollSnapPage() diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 3b291841fe..7d5b5e1490 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -869,7 +869,7 @@ namespace Avalonia.Controls.Presenters if (!_areVerticalSnapPointsRegular) { - _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, HorizontalSnapPointsAlignment); + _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment); } else { From 5ffd961742d888106ba582dc02a71927abc3b92c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 23 Jan 2023 18:21:21 +0100 Subject: [PATCH 093/326] Perf: improved text wrapping --- .../TextFormatting/InterWordJustification.cs | 16 +-- .../Media/TextFormatting/TextFormatterImpl.cs | 120 ++++++++++-------- .../Media/TextFormatting/TextLayout.cs | 17 ++- .../Media/TextFormatting/TextLineBreak.cs | 15 +-- .../Media/TextFormatting/TextLineImpl.cs | 8 +- .../TextFormatting/WrappingTextLineBreak.cs | 30 +++++ .../Text/HugeTextLayout.cs | 2 +- .../TextFormatting/TextFormatterTests.cs | 5 +- 8 files changed, 124 insertions(+), 89 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index efcd866bbc..0d85f3e7c5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -15,9 +15,7 @@ namespace Avalonia.Media.TextFormatting public override void Justify(TextLine textLine) { - var lineImpl = textLine as TextLineImpl; - - if(lineImpl is null) + if (textLine is not TextLineImpl lineImpl) { return; } @@ -34,14 +32,9 @@ namespace Avalonia.Media.TextFormatting return; } - var textLineBreak = lineImpl.TextLineBreak; - - if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null) + if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false }) { - if (textLineBreak.RemainingRuns is null || textLineBreak.RemainingRuns.Count == 0) - { - return; - } + return; } var breakOportunities = new Queue(); @@ -107,7 +100,8 @@ namespace Avalonia.Media.TextFormatting var glyphIndex = glyphRun.FindGlyphIndex(characterIndex); var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; - shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); + shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, + glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7505b9ccdd..c7ec28f16d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -2,7 +2,6 @@ using System; using System.Buffers; using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -22,68 +21,55 @@ namespace Avalonia.Media.TextFormatting public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { - var textWrapping = paragraphProperties.TextWrapping; - FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - IReadOnlyList? textRuns; var objectPool = FormattingObjectPool.Instance; var fontManager = FontManager.Current; - var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, - out var textEndOfLine, out var textSourceLength); + // we've wrapped the previous line and need to continue wrapping: ignore the textSource and do that instead + if (previousLineBreak is WrappingTextLineBreak wrappingTextLineBreak + && wrappingTextLineBreak.AcquireRemainingRuns() is { } remainingRuns + && paragraphProperties.TextWrapping != TextWrapping.NoWrap) + { + return PerformTextWrapping(remainingRuns, true, firstTextSourceIndex, paragraphWidth, + paragraphProperties, previousLineBreak.FlowDirection, previousLineBreak, objectPool); + } + RentedList? fetchedRuns = null; RentedList? shapedTextRuns = null; - try { - if (previousLineBreak?.RemainingRuns is { } remainingRuns) - { - resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = remainingRuns; - nextLineBreak = previousLineBreak; - shapedTextRuns = null; - } - else - { - shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, - out resolvedFlowDirection); - textRuns = shapedTextRuns; + fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, + out var textSourceLength); - if (nextLineBreak == null && textEndOfLine != null) - { - nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); - } - } + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, + out var resolvedFlowDirection); - TextLineImpl textLine; + if (nextLineBreak == null && textEndOfLine != null) + { + nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); + } - switch (textWrapping) + switch (paragraphProperties.TextWrapping) { case TextWrapping.NoWrap: { - // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); - - textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, + var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex, + textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); - break; + return textLine; } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, - paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager); - break; + return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); } default: - throw new ArgumentOutOfRangeException(nameof(textWrapping)); + throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping)); } - - return textLine; } finally { @@ -108,15 +94,16 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < textRuns.Count; i++) { var currentRun = textRuns[i]; + var currentRunLength = currentRun.Length; - if (currentLength + currentRun.Length < length) + if (currentLength + currentRunLength < length) { - currentLength += currentRun.Length; + currentLength += currentRunLength; continue; } - var firstCount = currentRun.Length >= 1 ? i + 1 : i; + var firstCount = currentRunLength >= 1 ? i + 1 : i; if (firstCount > 1) { @@ -128,13 +115,13 @@ namespace Avalonia.Media.TextFormatting var secondCount = textRuns.Count - firstCount; - if (currentLength + currentRun.Length == length) + if (currentLength + currentRunLength == length) { var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; if (second != null) { - var offset = currentRun.Length >= 1 ? 1 : 0; + var offset = currentRunLength >= 1 ? 1 : 0; for (var j = 0; j < secondCount; j++) { @@ -653,7 +640,7 @@ namespace Avalonia.Media.TextFormatting /// /// The empty text line. public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties, FontManager fontManager) + TextParagraphProperties paragraphProperties) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; @@ -675,21 +662,21 @@ namespace Avalonia.Media.TextFormatting /// Performs text wrapping returns a list of text lines. /// /// + /// Whether can be reused to store the split runs. /// The first text source index. /// The paragraph width. /// The text paragraph properties. /// /// The current line break if the line was explicitly broken. /// A pool used to get reusable formatting objects. - /// The font manager to use. /// The wrapped text line. - private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, - double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager) + private static TextLineImpl PerformTextWrapping(List textRuns, bool canReuseTextRunList, + int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, + FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -819,13 +806,37 @@ namespace Avalonia.Media.TextFormatting try { - var textLineBreak = postSplitRuns?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : - null; + TextLineBreak? textLineBreak; + if (postSplitRuns?.Count > 0) + { + List remainingRuns; + + // reuse the list as much as possible: + // if canReuseTextRunList == true it's coming from previous remaining runs + if (canReuseTextRunList) + { + remainingRuns = textRuns; + remainingRuns.Clear(); + } + else + { + remainingRuns = new List(); + } - if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null) + for (var i = 0; i < postSplitRuns.Count; ++i) + { + remainingRuns.Add(postSplitRuns[i]); + } + + textLineBreak = new WrappingTextLineBreak(null, resolvedFlowDirection, remainingRuns); + } + else if (currentLineBreak?.TextEndOfLine is { } textEndOfLine) { - textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); + textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection); + } + else + { + textLineBreak = null; } var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, @@ -833,6 +844,7 @@ namespace Avalonia.Media.TextFormatting textLineBreak); textLine.FinalizeLine(); + return textLine; } finally diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 4923cdbe32..8e85c10bba 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -416,9 +416,11 @@ namespace Avalonia.Media.TextFormatting width = lineWidth; } - if (left > textLine.Start) + var start = textLine.Start; + + if (left > start) { - left = textLine.Start; + left = start; } height += textLine.Height; @@ -427,12 +429,10 @@ namespace Avalonia.Media.TextFormatting private TextLine[] CreateTextLines() { var objectPool = FormattingObjectPool.Instance; - var fontManager = FontManager.Current; if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, - fontManager); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); Bounds = new Rect(0, 0, 0, textLine.Height); @@ -461,7 +461,7 @@ namespace Avalonia.Media.TextFormatting if (previousLine != null && previousLine.NewLineLength > 0) { var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, - _paragraphProperties, fontManager); + _paragraphProperties); textLines.Add(emptyTextLine); @@ -504,7 +504,7 @@ namespace Avalonia.Media.TextFormatting //Fulfill max lines constraint if (MaxLines > 0 && textLines.Count >= MaxLines) { - if (textLine.TextLineBreak?.RemainingRuns is not null) + if (textLine.TextLineBreak is { IsSplit: true }) { textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); } @@ -521,8 +521,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { - var textLine = - TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); textLines.Add(textLine); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs index bf26ac5df4..3b3464b46e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { public class TextLineBreak { - public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, - IReadOnlyList? remainingRuns = null) + public TextLineBreak(TextEndOfLine? textEndOfLine = null, + FlowDirection flowDirection = FlowDirection.LeftToRight, bool isSplit = false) { TextEndOfLine = textEndOfLine; FlowDirection = flowDirection; - RemainingRuns = remainingRuns; + IsSplit = isSplit; } /// @@ -23,8 +21,9 @@ namespace Avalonia.Media.TextFormatting public FlowDirection FlowDirection { get; } /// - /// Get the remaining runs that were split up by the during the formatting process. + /// Gets whether there were remaining runs after this line break, + /// that were split up by the during the formatting process. /// - public IReadOnlyList? RemainingRuns { get; } + public bool IsSplit { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ad3244a3a5..4a7916275d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1285,13 +1285,11 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextRun textRun: { - var properties = textRun.Properties; - var textMetrics = - new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize); + var textMetrics = textRun.TextMetrics; - if (fontRenderingEmSize < properties.FontRenderingEmSize) + if (fontRenderingEmSize < textMetrics.FontRenderingEmSize) { - fontRenderingEmSize = properties.FontRenderingEmSize; + fontRenderingEmSize = textMetrics.FontRenderingEmSize; if (ascent > textMetrics.Ascent) { diff --git a/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs new file mode 100644 index 0000000000..dacff9e589 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// Represents a line break that occurred due to wrapping. + internal sealed class WrappingTextLineBreak : TextLineBreak + { + private List? _remainingRuns; + + public WrappingTextLineBreak(TextEndOfLine? textEndOfLine, FlowDirection flowDirection, + List remainingRuns) + : base(textEndOfLine, flowDirection, isSplit: true) + { + Debug.Assert(remainingRuns.Count > 0); + _remainingRuns = remainingRuns; + } + + /// + /// Gets the remaining runs from this line break, and clears them from this line break. + /// + /// A list of text runs. + public List? AcquireRemainingRuns() + { + var remainingRuns = _remainingRuns; + _remainingRuns = null; + return remainingRuns; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 4dad8442de..03b85840a7 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -15,7 +15,7 @@ namespace Avalonia.Benchmarks.Text; public class HugeTextLayout : IDisposable { private static readonly Random s_rand = new(); - private static readonly bool s_useSkia = true; + private static readonly bool s_useSkia = false; private readonly IDisposable _app; private readonly string[] _manySmallStrings; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 7822d6624b..e8b87ebe00 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -558,7 +558,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 33, paragraphProperties); - Assert.NotNull(textLine.TextLineBreak?.RemainingRuns); + var remainingRunsLineBreak = Assert.IsType(textLine.TextLineBreak); + var remainingRuns = remainingRunsLineBreak.AcquireRemainingRuns(); + Assert.NotNull(remainingRuns); + Assert.NotEmpty(remainingRuns); } } From 7e8c34fc924581a8598dc1e8476c5ac0fe6b50fe Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 24 Jan 2023 19:06:31 +0200 Subject: [PATCH 094/326] Use GetTopLevel method --- samples/ControlCatalog/MainView.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 4b2f9bb7c3..a9bf150ff9 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -60,7 +60,7 @@ namespace ControlCatalog { if (flowDirections.SelectedItem is FlowDirection flowDirection) { - ((TopLevel)this.VisualRoot!).FlowDirection = flowDirection; + TopLevel.GetTopLevel(this).FlowDirection = flowDirection; } }; From 78abd6d4b588a2913441114219969085c34ce01b Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 24 Jan 2023 20:03:02 +0000 Subject: [PATCH 095/326] Add IsScrollInertiaEnabled attached property to scrollviewer --- .../Pages/ScrollViewerPage.xaml | 2 ++ .../Pages/ScrollViewerPage.xaml.cs | 8 +++++++ .../ScrollGestureRecognizer.cs | 24 +++++++++++++++++-- src/Avalonia.Controls/ScrollViewer.cs | 17 +++++++++++++ .../Controls/ScrollViewer.xaml | 3 ++- 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 1903e50ed7..5042d7823b 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -9,6 +9,7 @@ + @@ -24,6 +25,7 @@ diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index dcd7a88a56..a097f1f951 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -9,6 +9,7 @@ namespace ControlCatalog.Pages public class ScrollViewerPageViewModel : ViewModelBase { private bool _allowAutoHide; + private bool _enableInertia; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; @@ -25,6 +26,7 @@ namespace ControlCatalog.Pages HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; + EnableInertia = true; } public bool AllowAutoHide @@ -33,6 +35,12 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); } + public bool EnableInertia + { + get => _enableInertia; + set => this.RaiseAndSetIfChanged(ref _enableInertia, value); + } + public ScrollBarVisibility HorizontalScrollVisibility { get => _horizontalScrollVisibility; diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 790439245a..fdab2df0bf 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -23,7 +23,8 @@ namespace Avalonia.Input.GestureRecognizers // Movement per second private Vector _inertia; private ulong? _lastMoveTimestamp; - + private bool _isScrollInertiaEnabled; + /// /// Defines the property. /// @@ -42,6 +43,15 @@ namespace Avalonia.Input.GestureRecognizers o => o.CanVerticallyScroll, (o, v) => o.CanVerticallyScroll = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsScrollInertiaEnabled), + o => o.IsScrollInertiaEnabled, + (o, v) => o.IsScrollInertiaEnabled = v); + /// /// Defines the property. /// @@ -68,6 +78,15 @@ namespace Avalonia.Input.GestureRecognizers get => _canVerticallyScroll; set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } + + /// + /// Gets or sets whether the gesture should include inertia in it's behavior. + /// + public bool IsScrollInertiaEnabled + { + get => _isScrollInertiaEnabled; + set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value); + } /// /// Gets or sets a value indicating the distance the pointer moves before scrolling is started @@ -168,7 +187,8 @@ namespace Avalonia.Input.GestureRecognizers if (_inertia == default || e.Timestamp == 0 || _lastMoveTimestamp == 0 - || e.Timestamp - _lastMoveTimestamp > 200) + || e.Timestamp - _lastMoveTimestamp > 200 + || !IsScrollInertiaEnabled) EndGesture(); else { diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 503187e2d3..d16cd209c0 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -193,6 +193,14 @@ namespace Avalonia.Controls nameof(IsScrollChainingEnabled), defaultValue: true); + /// + /// Defines the property. + /// + public static readonly AttachedProperty IsScrollInertiaEnabledProperty = + AvaloniaProperty.RegisterAttached( + nameof(IsScrollInertiaEnabled), + defaultValue: true); + /// /// Defines the event. /// @@ -444,6 +452,15 @@ namespace Avalonia.Controls set => SetValue(IsScrollChainingEnabledProperty, value); } + /// + /// Gets or sets whether scroll gestures should include inertia in their behavior and value. + /// + public bool IsScrollInertiaEnabled + { + get => GetValue(IsScrollInertiaEnabledProperty); + set => SetValue(IsScrollInertiaEnabledProperty, value); + } + /// /// Scrolls the content up one line. /// diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index 71df4d419f..15cd9428f3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -39,7 +39,8 @@ IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> + CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" + IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"/> Date: Tue, 24 Jan 2023 15:37:45 -0500 Subject: [PATCH 096/326] Fix FilePickerHandler default folder --- .../Screenshots/BaseRenderToStreamHandler.cs | 4 ++ .../Screenshots/FilePickerHandler.cs | 57 +++++++++---------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs index acf2e01264..103ac669b6 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/BaseRenderToStreamHandler.cs @@ -17,7 +17,11 @@ namespace Avalonia.Diagnostics.Screenshots public async Task Take(Control control) { +#if NET6_0_OR_GREATER + await using var output = await GetStream(control); +#else using var output = await GetStream(control); +#endif if (output is { }) { control.RenderTo(output); diff --git a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs index 2117996b96..a7d279741e 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs @@ -14,57 +14,54 @@ namespace Avalonia.Diagnostics.Screenshots /// public sealed class FilePickerHandler : BaseRenderToStreamHandler { + private readonly string _title; + private readonly string _screenshotRoot; + /// /// Instance FilePickerHandler /// - public FilePickerHandler() + public FilePickerHandler() : this(null, null) { } + /// /// Instance FilePickerHandler with specificated parameter /// /// SaveFilePicker Title /// - public FilePickerHandler(string? title - , string? screenshotRoot = default - ) + public FilePickerHandler( + string? title, + string? screenshotRoot = default) { - if (title is { }) - Title = title; - if (screenshotRoot is { }) - ScreenshotsRoot = screenshotRoot; + _title = title ?? "Save Screenshot to ..."; + _screenshotRoot = screenshotRoot ?? Conventions.DefaultScreenshotsRoot; } - /// - /// Get the root folder where screeshots well be stored. - /// The default root folder is [Environment.SpecialFolder.MyPictures]/Screenshots. - /// - public string ScreenshotsRoot { get; } - = Conventions.DefaultScreenshotsRoot; - /// - /// SaveFilePicker Title - /// - public string Title { get; } = "Save Screenshot to ..."; - - static Window GetWindow(Control control) + private static TopLevel GetTopLevel(Control control) { - var window = control.VisualRoot as Window; - var app = Application.Current; - if (app?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop) + // If possible, use devtools main window. + TopLevel? devToolsTopLevel = null; + if (Application.Current?.ApplicationLifetime is Lifetimes.IClassicDesktopStyleApplicationLifetime desktop) { - window = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow); + devToolsTopLevel = desktop.Windows.FirstOrDefault(w => w is Views.MainWindow); } - return window!; + + return devToolsTopLevel ?? TopLevel.GetTopLevel(control) + ?? throw new InvalidOperationException("No TopLevel is available."); } protected override async Task GetStream(Control control) { - var result = await GetWindow(control).StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + var storageProvider = GetTopLevel(control).StorageProvider; + var defaultFolder = await storageProvider.TryGetFolderFromPath(_screenshotRoot) + ?? await storageProvider.TryGetWellKnownFolder(WellKnownFolder.Pictures); + + var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { - SuggestedStartLocation = new BclStorageFolder(new DirectoryInfo(ScreenshotsRoot)), - Title = Title, - FileTypeChoices = new FilePickerFileType[] { new FilePickerFileType("PNG") { Patterns = new string[] { "*.png" } } }, + SuggestedStartLocation = defaultFolder, + Title = _title, + FileTypeChoices = new [] { FilePickerFileTypes.ImagePng }, DefaultExtension = ".png" }); if (result is null) @@ -73,7 +70,7 @@ namespace Avalonia.Diagnostics.Screenshots } if (!result.CanOpenWrite) { - throw new InvalidOperationException("ReadOnly file was opened"); + throw new InvalidOperationException("Read-only file was selected."); } return await result.OpenWriteAsync(); From 6b4b620a2293227b4ac14c9d763618c432729759 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 5 Jan 2023 18:56:50 -0500 Subject: [PATCH 097/326] Revert "Merge pull request #8860 from AvaloniaUI/remove-public-constructors-from-eventargs-from-avalonia.base" This reverts commit a26d515aa4cc4433fb0e0173e8c8f5a3b8419727, reversing changes made to 4670536b82044e9cc21d2d3f03c98e1781453efb. # Conflicts: # src/Avalonia.Base/Input/KeyEventArgs.cs # src/Avalonia.Base/Input/PointerDeltaEventArgs.cs # src/Avalonia.Base/Input/PointerEventArgs.cs # src/Avalonia.Base/Input/PointerWheelEventArgs.cs # src/Avalonia.Base/Input/TextInputEventArgs.cs --- src/Avalonia.Base/Input/DragEventArgs.cs | 5 ++++- src/Avalonia.Base/Input/PointerDeltaEventArgs.cs | 6 +++++- src/Avalonia.Base/Input/PointerEventArgs.cs | 5 ++++- src/Avalonia.Base/Input/PointerWheelEventArgs.cs | 6 +++++- src/Avalonia.Base/Input/ScrollGestureEventArgs.cs | 4 ++-- src/Avalonia.Base/Input/TappedEventArgs.cs | 3 ++- src/Avalonia.Base/Input/VectorEventArgs.cs | 5 ----- .../Layout/EffectiveViewportChangedEventArgs.cs | 2 +- src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs | 2 +- 9 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index 7a27c53023..ffa32054b4 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input @@ -32,7 +33,9 @@ namespace Avalonia.Input return point; } - internal DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) + [Unstable] + [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawDragEvent.")] + public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) : base(routedEvent) { Data = data; diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs index af0fa83382..84c6a58998 100644 --- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs @@ -1,4 +1,6 @@ +using System; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input @@ -7,7 +9,9 @@ namespace Avalonia.Input { public Vector Delta { get; set; } - internal PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, + [Unstable] + [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawPointerEventArgs or headless unit tests helpers.")] + public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) : base(routedEvent, source, pointer, rootVisual, rootVisualPosition, diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index d736253728..598f51309b 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Avalonia.Input.Raw; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input @@ -13,7 +14,9 @@ namespace Avalonia.Input private readonly PointerPointProperties _properties; private readonly Lazy?>? _previousPoints; - internal PointerEventArgs(RoutedEvent routedEvent, + [Unstable] + [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawPointerEventArgs or headless unit tests helpers.")] + public PointerEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, Visual? rootVisual, Point rootVisualPosition, diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs index a3de0eaaea..5095c8044d 100644 --- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs @@ -1,4 +1,6 @@ +using System; using Avalonia.Interactivity; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Input @@ -7,7 +9,9 @@ namespace Avalonia.Input { public Vector Delta { get; set; } - internal PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, + [Unstable] + [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawPointerEventArgs or headless unit tests helpers.")] + public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs index 55aaadff71..dd78080708 100644 --- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs @@ -14,7 +14,7 @@ namespace Avalonia.Input public static int GetNextFreeId() => _nextId++; - internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) + public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) { Id = id; Delta = delta; @@ -25,7 +25,7 @@ namespace Avalonia.Input { public int Id { get; } - internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) + public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) { Id = id; } diff --git a/src/Avalonia.Base/Input/TappedEventArgs.cs b/src/Avalonia.Base/Input/TappedEventArgs.cs index 3e15c4843a..663207a104 100644 --- a/src/Avalonia.Base/Input/TappedEventArgs.cs +++ b/src/Avalonia.Base/Input/TappedEventArgs.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -7,7 +8,7 @@ namespace Avalonia.Input { private readonly PointerEventArgs lastPointerEventArgs; - internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) + public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) : base(routedEvent) { this.lastPointerEventArgs = lastPointerEventArgs; diff --git a/src/Avalonia.Base/Input/VectorEventArgs.cs b/src/Avalonia.Base/Input/VectorEventArgs.cs index 3e8098f904..000fd52f69 100644 --- a/src/Avalonia.Base/Input/VectorEventArgs.cs +++ b/src/Avalonia.Base/Input/VectorEventArgs.cs @@ -5,11 +5,6 @@ namespace Avalonia.Input { public class VectorEventArgs : RoutedEventArgs { - internal VectorEventArgs() - { - - } - public Vector Vector { get; set; } } } diff --git a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs index 749d2ecc2b..1cdc775b13 100644 --- a/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs +++ b/src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs @@ -7,7 +7,7 @@ namespace Avalonia.Layout /// public class EffectiveViewportChangedEventArgs : EventArgs { - internal EffectiveViewportChangedEventArgs(Rect effectiveViewport) + public EffectiveViewportChangedEventArgs(Rect effectiveViewport) { EffectiveViewport = effectiveViewport; } diff --git a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs index 73840376fe..cac4d1693a 100644 --- a/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs +++ b/src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering /// /// The render root that has been updated. /// The updated area. - internal SceneInvalidatedEventArgs( + public SceneInvalidatedEventArgs( IRenderRoot root, Rect dirtyRect) { From af4d9fc2e2e29bc4af1b15f6b76616886c592f56 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 24 Jan 2023 16:45:24 -0500 Subject: [PATCH 098/326] Add more IHeadlessWindow methods and update Obsolete message --- src/Avalonia.Base/Input/DragEventArgs.cs | 2 +- src/Avalonia.Base/Input/PointerDeltaEventArgs.cs | 2 +- src/Avalonia.Base/Input/PointerEventArgs.cs | 2 +- src/Avalonia.Base/Input/PointerWheelEventArgs.cs | 2 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 12 ++++++++++++ src/Avalonia.Headless/IHeadlessWindow.cs | 3 +++ 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Input/DragEventArgs.cs b/src/Avalonia.Base/Input/DragEventArgs.cs index ffa32054b4..403dd6f23e 100644 --- a/src/Avalonia.Base/Input/DragEventArgs.cs +++ b/src/Avalonia.Base/Input/DragEventArgs.cs @@ -34,7 +34,7 @@ namespace Avalonia.Input } [Unstable] - [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawDragEvent.")] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")] public DragEventArgs(RoutedEvent routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) : base(routedEvent) { diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs index 84c6a58998..dd8a8f0385 100644 --- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs @@ -10,7 +10,7 @@ namespace Avalonia.Input public Vector Delta { get; set; } [Unstable] - [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawPointerEventArgs or headless unit tests helpers.")] + [Obsolete("This constructor might be removed in 12.0.")] public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 598f51309b..502da52597 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -15,7 +15,7 @@ namespace Avalonia.Input private readonly Lazy?>? _previousPoints; [Unstable] - [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawPointerEventArgs or headless unit tests helpers.")] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")] public PointerEventArgs(RoutedEvent routedEvent, object? source, IPointer pointer, diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs index 5095c8044d..3a23effa79 100644 --- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs @@ -10,7 +10,7 @@ namespace Avalonia.Input public Vector Delta { get; set; } [Unstable] - [Obsolete("This constructor may be removed in 12.0. Consider replacing it with RawPointerEventArgs or headless unit tests helpers.")] + [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, PointerPointProperties properties, KeyModifiers modifiers, Vector delta) diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 195328cd65..2c19b2f85f 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -284,6 +284,18 @@ namespace Avalonia.Headless button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, point, modifiers)); } + + void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) + { + Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot, + point, delta, modifiers)); + } + + void IHeadlessWindow.DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) + { + var device = AvaloniaLocator.Current.GetRequiredService(); + Input?.Invoke(new RawDragEvent(device, type, InputRoot, point, data, effects, modifiers)); + } void IWindowImpl.Move(PixelPoint point) { diff --git a/src/Avalonia.Headless/IHeadlessWindow.cs b/src/Avalonia.Headless/IHeadlessWindow.cs index 282662f98b..dfb3a4c433 100644 --- a/src/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Avalonia.Headless/IHeadlessWindow.cs @@ -1,4 +1,5 @@ using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Utilities; @@ -13,5 +14,7 @@ namespace Avalonia.Headless void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None); + void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers); } } From 39f232dcfff5ec9bc942192510aa620737feb74b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 24 Jan 2023 16:52:16 -0500 Subject: [PATCH 099/326] Remove public setter where possible on events or replace with "init;" --- src/Avalonia.Base/Input/GotFocusEventArgs.cs | 7 +++---- src/Avalonia.Base/Input/KeyEventArgs.cs | 6 +++--- src/Avalonia.Base/Input/KeyboardDevice.cs | 1 - src/Avalonia.Base/Input/PointerDeltaEventArgs.cs | 2 +- src/Avalonia.Base/Input/PointerWheelEventArgs.cs | 2 +- src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs | 2 +- src/Avalonia.Base/Input/VectorEventArgs.cs | 2 +- tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs | 3 --- tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs | 4 ---- 9 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Base/Input/GotFocusEventArgs.cs b/src/Avalonia.Base/Input/GotFocusEventArgs.cs index f3de55ebae..8d15c3f9ec 100644 --- a/src/Avalonia.Base/Input/GotFocusEventArgs.cs +++ b/src/Avalonia.Base/Input/GotFocusEventArgs.cs @@ -7,19 +7,18 @@ namespace Avalonia.Input /// public class GotFocusEventArgs : RoutedEventArgs { - internal GotFocusEventArgs() + public GotFocusEventArgs() : base(InputElement.GotFocusEvent) { - } /// /// Gets or sets a value indicating how the change in focus occurred. /// - public NavigationMethod NavigationMethod { get; set; } + public NavigationMethod NavigationMethod { get; init; } /// /// Gets or sets any key modifiers active at the time of focus. /// - public KeyModifiers KeyModifiers { get; set; } + public KeyModifiers KeyModifiers { get; init; } } } diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs index 35fa549995..0eaa7d43fb 100644 --- a/src/Avalonia.Base/Input/KeyEventArgs.cs +++ b/src/Avalonia.Base/Input/KeyEventArgs.cs @@ -10,10 +10,10 @@ namespace Avalonia.Input } - public IKeyboardDevice? Device { get; set; } + public IKeyboardDevice? Device { get; init; } - public Key Key { get; set; } + public Key Key { get; init; } - public KeyModifiers KeyModifiers { get; set; } + public KeyModifiers KeyModifiers { get; init; } } } diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs index 26ff71a4e7..c46834fff4 100644 --- a/src/Avalonia.Base/Input/KeyboardDevice.cs +++ b/src/Avalonia.Base/Input/KeyboardDevice.cs @@ -156,7 +156,6 @@ namespace Avalonia.Input interactive?.RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = method, KeyModifiers = keyModifiers, }); diff --git a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs index dd8a8f0385..c405cdfacd 100644 --- a/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerDeltaEventArgs.cs @@ -7,7 +7,7 @@ namespace Avalonia.Input { public class PointerDeltaEventArgs : PointerEventArgs { - public Vector Delta { get; set; } + public Vector Delta { get; } [Unstable] [Obsolete("This constructor might be removed in 12.0.")] diff --git a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs index 3a23effa79..903019d85d 100644 --- a/src/Avalonia.Base/Input/PointerWheelEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerWheelEventArgs.cs @@ -7,7 +7,7 @@ namespace Avalonia.Input { public class PointerWheelEventArgs : PointerEventArgs { - public Vector Delta { get; set; } + public Vector Delta { get; } [Unstable] [Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")] diff --git a/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs index 48c882197f..cd1cf29bcf 100644 --- a/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs +++ b/src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs @@ -12,6 +12,6 @@ namespace Avalonia.Input.Raw Text = text; } - public string Text { get; set; } + public string Text { get; } } } diff --git a/src/Avalonia.Base/Input/VectorEventArgs.cs b/src/Avalonia.Base/Input/VectorEventArgs.cs index 000fd52f69..2ce95cf35a 100644 --- a/src/Avalonia.Base/Input/VectorEventArgs.cs +++ b/src/Avalonia.Base/Input/VectorEventArgs.cs @@ -5,6 +5,6 @@ namespace Avalonia.Input { public class VectorEventArgs : RoutedEventArgs { - public Vector Vector { get; set; } + public Vector Vector { get; init; } } } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs index b358c98a62..942eb8bf5b 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs @@ -28,7 +28,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, KeyModifiers = KeyModifiers.Shift }); @@ -52,7 +51,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.Panel.Children[1].RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, KeyModifiers = KeyModifiers.Control }); @@ -77,7 +75,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, KeyModifiers = KeyModifiers.Control }); diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index cb4e81001f..131642fc4e 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -33,7 +33,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Tab, }); @@ -53,7 +52,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, }); @@ -73,7 +71,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, KeyModifiers = KeyModifiers.Control }); @@ -96,7 +93,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.Panel.Children[0].RaiseEvent(new GotFocusEventArgs { - RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = NavigationMethod.Directional, KeyModifiers = KeyModifiers.Control }); From 86afa5e7b546b535397f700d399a55c2ccaec999 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 24 Jan 2023 16:53:57 -0500 Subject: [PATCH 100/326] Remove redunant empty ctors --- src/Avalonia.Base/Input/KeyEventArgs.cs | 5 ----- src/Avalonia.Base/Input/TextInputEventArgs.cs | 4 ---- 2 files changed, 9 deletions(-) diff --git a/src/Avalonia.Base/Input/KeyEventArgs.cs b/src/Avalonia.Base/Input/KeyEventArgs.cs index 0eaa7d43fb..9fa097c4b1 100644 --- a/src/Avalonia.Base/Input/KeyEventArgs.cs +++ b/src/Avalonia.Base/Input/KeyEventArgs.cs @@ -5,11 +5,6 @@ namespace Avalonia.Input { public class KeyEventArgs : RoutedEventArgs { - public KeyEventArgs() - { - - } - public IKeyboardDevice? Device { get; init; } public Key Key { get; init; } diff --git a/src/Avalonia.Base/Input/TextInputEventArgs.cs b/src/Avalonia.Base/Input/TextInputEventArgs.cs index a027bec0c6..cda0103749 100644 --- a/src/Avalonia.Base/Input/TextInputEventArgs.cs +++ b/src/Avalonia.Base/Input/TextInputEventArgs.cs @@ -4,10 +4,6 @@ namespace Avalonia.Input { public class TextInputEventArgs : RoutedEventArgs { - public TextInputEventArgs() - { - - } public IKeyboardDevice? Device { get; set; } public string? Text { get; set; } From 2d1d27bc2e9f8914968d65cd18beab30ccbe973c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 25 Jan 2023 01:03:19 -0500 Subject: [PATCH 101/326] Fix TemplateBinding with converter inside of MultiBinding --- .../Avalonia.Markup/Data/TemplateBinding.cs | 4 +-- .../Data/TemplateBindingTests.cs | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs index 4270063f87..ba321db144 100644 --- a/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/TemplateBinding.cs @@ -137,9 +137,9 @@ namespace Avalonia.Data templatedParent.GetValue(Property) : _target.TemplatedParent; - if (Converter is not null && _targetType is not null) + if (Converter is not null) { - value = Converter.Convert(value, _targetType, ConverterParameter, CultureInfo.CurrentCulture); + value = Converter.Convert(value, _targetType ?? typeof(object), ConverterParameter, CultureInfo.CurrentCulture); } PublishNext(value); diff --git a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs index 979dbec674..1e56981b98 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs @@ -248,6 +248,38 @@ namespace Avalonia.Markup.UnitTests.Data // binding is initiated. Assert.Equal(new[] { "foo" }, converter.Values); } + + [Fact] + public void Should_Execute_Converter_Without_Specific_TargetType() + { + // See https://github.com/AvaloniaUI/Avalonia/issues/9766 + var source = new Button + { + Template = new FuncControlTemplate