From 4d34a2c6e76fc415d2c62b22142aa937305cdf0c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:26:43 -0500 Subject: [PATCH 01/54] Move headless projects to a subfolder --- Avalonia.sln | 8 ++++++-- .../ControlCatalog.NetCore/ControlCatalog.NetCore.csproj | 2 +- .../MobileSandbox.Desktop/MobileSandbox.Desktop.csproj | 2 +- .../Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj | 2 +- .../Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs | 0 .../HeadlessVncPlatformExtensions.cs | 0 .../Avalonia.Headless/Avalonia.Headless.csproj | 6 +++--- .../Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 0 .../Avalonia.Headless/HeadlessPlatformRenderInterface.cs | 0 .../Avalonia.Headless/HeadlessPlatformStubs.cs | 0 .../HeadlessPlatformThreadingInterface.cs | 0 .../Avalonia.Headless/HeadlessWindowImpl.cs | 0 src/{ => Headless}/Avalonia.Headless/IHeadlessWindow.cs | 0 13 files changed, 12 insertions(+), 8 deletions(-) rename src/{ => Headless}/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj (85%) rename src/{ => Headless}/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs (100%) rename src/{ => Headless}/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs (100%) rename src/{ => Headless}/Avalonia.Headless/Avalonia.Headless.csproj (60%) rename src/{ => Headless}/Avalonia.Headless/AvaloniaHeadlessPlatform.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformRenderInterface.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformStubs.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessWindowImpl.cs (100%) rename src/{ => Headless}/Avalonia.Headless/IHeadlessWindow.cs (100%) diff --git a/Avalonia.sln b/Avalonia.sln index e66b73de0e..295f7cb149 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -181,9 +181,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject @@ -246,6 +246,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -644,6 +646,8 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e465e9caf3..877d475fb6 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -26,7 +26,7 @@ - + diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj index a24e55de81..31a6b05175 100644 --- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj +++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj similarity index 85% rename from src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj rename to src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index c713440dc9..5bdedec81d 100644 --- a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -9,5 +9,5 @@ - + diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs similarity index 100% rename from src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs similarity index 100% rename from src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs diff --git a/src/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj similarity index 60% rename from src/Avalonia.Headless/Avalonia.Headless.csproj rename to src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index 95f7b79009..c3a472e950 100644 --- a/src/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -7,7 +7,7 @@ - - - + + + diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs similarity index 100% rename from src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs rename to src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformStubs.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessWindowImpl.cs rename to src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs diff --git a/src/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs similarity index 100% rename from src/Avalonia.Headless/IHeadlessWindow.cs rename to src/Headless/Avalonia.Headless/IHeadlessWindow.cs From a2bd18457463a847566ac2095abbc8bc62d80cde Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:46:22 -0500 Subject: [PATCH 02/54] Move NoopStorageProvider to the base project --- .../Platform/Storage/NoopStorageProvider.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/TopLevel.cs | 5 +++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs diff --git a/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs new file mode 100644 index 0000000000..153634027c --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform.Storage.FileIO; + +namespace Avalonia.Platform.Storage; + +internal class NoopStorageProvider : BclStorageProvider +{ + public override bool CanOpen => false; + public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } + + public override bool CanSave => false; + public override Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + return Task.FromResult(null); + } + + public override bool CanPickFolder => false; + public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index fdcb8cc537..eec6b08ecb 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -387,10 +387,13 @@ namespace Avalonia.Controls IStyleHost IStyleHost.StylingParent => _globalStyles!; + /// + /// File System storage service used for file pickers and bookmarks. + /// public IStorageProvider StorageProvider => _storageProvider ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() - ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); + ?? new NoopStorageProvider(); /// Point IRenderRoot.PointToClient(PixelPoint p) From 9e93791ea59a767067c4fb04b5f9641ded6e358b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:46:28 -0500 Subject: [PATCH 03/54] Enable nullable in headless platform --- .../Avalonia.Controls.csproj | 1 + .../Platform/ScreenHelper.cs | 3 +- .../Avalonia.Headless.Vnc.csproj | 3 + .../HeadlessVncFramebufferSource.cs | 2 +- .../HeadlessVncPlatformExtensions.cs | 5 +- .../Avalonia.Headless.csproj | 5 +- .../AvaloniaHeadlessPlatform.cs | 13 ++- .../HeadlessPlatformRenderInterface.cs | 43 +++++----- .../HeadlessPlatformStubs.cs | 73 +++++++--------- .../HeadlessPlatformThreadingInterface.cs | 14 ++-- .../Avalonia.Headless/HeadlessWindowImpl.cs | 83 +++++++++---------- .../Avalonia.Headless/IHeadlessWindow.cs | 2 +- 12 files changed, 120 insertions(+), 127 deletions(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 3195c38eef..5adb5a8f2e 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -16,5 +16,6 @@ + diff --git a/src/Avalonia.Controls/Platform/ScreenHelper.cs b/src/Avalonia.Controls/Platform/ScreenHelper.cs index 0bd2be69d0..59b29b4748 100644 --- a/src/Avalonia.Controls/Platform/ScreenHelper.cs +++ b/src/Avalonia.Controls/Platform/ScreenHelper.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Utilities; #nullable enable namespace Avalonia.Platform { - public static class ScreenHelper + internal static class ScreenHelper { public static Screen? ScreenFromPoint(PixelPoint point, IReadOnlyList screens) { diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 5bdedec81d..3812fb196d 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -9,5 +9,8 @@ + + + diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 18c149ce2e..be0f3579a5 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -17,7 +17,7 @@ namespace Avalonia.Headless.Vnc private VncButton _previousButtons; public HeadlessVncFramebufferSource(VncServerSession session, Window window) { - Window = (IHeadlessWindow)window.PlatformImpl; + Window = window.PlatformImpl as IHeadlessWindow ?? throw new InvalidOperationException("Invalid window parameter"); session.PointerChanged += (_, args) => { var pt = new Point(args.X, args.Y); diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index efc8c66fde..8e5cd1a316 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Sockets; using Avalonia.Controls; @@ -25,7 +26,7 @@ namespace Avalonia }) .AfterSetup(_ => { - var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); + var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance!.ApplicationLifetime!); lt.Startup += async delegate { while (true) @@ -38,7 +39,7 @@ namespace Avalonia var session = new VncServerSession(); session.SetFramebufferSource(new HeadlessVncFramebufferSource( - session, lt.MainWindow)); + session, lt.MainWindow ?? throw new InvalidOperationException("MainWindow wasn't initialized"))); session.Connect(client.GetStream(), options); } diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index c3a472e950..fbfa89f5a4 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -1,13 +1,14 @@  net6.0;netstandard2.0 - + - + + diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b0b1d731d2..6619b533f6 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,8 +1,6 @@ using System; using System.Diagnostics; using Avalonia.Reactive; -using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -14,11 +12,12 @@ namespace Avalonia.Headless { public static class AvaloniaHeadlessPlatform { - internal static Compositor Compositor { get; private set; } - class RenderTimer : DefaultRenderTimer + internal static Compositor? Compositor { get; private set; } + + private class RenderTimer : DefaultRenderTimer { private readonly int _framesPerSecond; - private Action _forceTick; + private Action? _forceTick; protected override IDisposable StartCore(Action tick) { bool cancelled = false; @@ -46,7 +45,7 @@ namespace Avalonia.Headless public void ForceTick() => _forceTick?.Invoke(); } - class HeadlessWindowingPlatform : IWindowingPlatform + private class HeadlessWindowingPlatform : IWindowingPlatform { public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false); @@ -54,7 +53,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() => null; + public ITrayIconImpl? CreateTrayIcon() => null; } internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 31aaebcdc7..9944a10f94 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Numerics; using System.Runtime.InteropServices; @@ -23,7 +24,7 @@ namespace Avalonia.Headless public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this; public bool SupportsIndividualRoundRects => false; @@ -52,7 +53,7 @@ namespace Avalonia.Headless public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -130,7 +131,7 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } - class HeadlessGlyphRunStub : IGlyphRunImpl + private class HeadlessGlyphRunStub : IGlyphRunImpl { public Size Size => new Size(8, 12); @@ -144,7 +145,7 @@ namespace Avalonia.Headless => Array.Empty(); } - class HeadlessGeometryStub : IGeometryImpl + private class HeadlessGeometryStub : IGeometryImpl { public HeadlessGeometryStub(Rect bounds) { @@ -157,7 +158,7 @@ namespace Avalonia.Headless public virtual bool FillContains(Point point) => Bounds.Contains(point); - public Rect GetRenderBounds(IPen pen) + public Rect GetRenderBounds(IPen? pen) { if(pen is null) { @@ -167,7 +168,7 @@ namespace Avalonia.Headless return Bounds.Inflate(pen.Thickness / 2); } - public bool StrokeContains(IPen pen, Point point) + public bool StrokeContains(IPen? pen, Point point) { return false; } @@ -191,21 +192,21 @@ namespace Avalonia.Headless return false; } - public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { segmentGeometry = null; return false; } } - class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl + private class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl { public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform)) { } - static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) + private static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) { if (b is HeadlessTransformedGeometryStub transformed) { @@ -227,7 +228,7 @@ namespace Avalonia.Headless public Matrix Transform { get; } } - class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl + private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { public HeadlessStreamingGeometryStub() : base(default) { @@ -243,7 +244,7 @@ namespace Avalonia.Headless return new HeadlessStreamingGeometryContextStub(this); } - class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl + private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl { private readonly HeadlessStreamingGeometryStub _parent; private double _x1, _y1, _x2, _y2; @@ -252,7 +253,7 @@ namespace Avalonia.Headless _parent = parent; } - void Track(Point pt) + private void Track(Point pt) { if (_x1 > pt.X) _x1 = pt.X; @@ -301,7 +302,7 @@ namespace Avalonia.Headless } } - class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl + private class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl { public Size Size { get; } @@ -363,7 +364,7 @@ namespace Avalonia.Headless } } - class HeadlessDrawingContextStub : IDrawingContextImpl + private class HeadlessDrawingContextStub : IDrawingContextImpl { public void Dispose() { @@ -437,16 +438,16 @@ namespace Avalonia.Headless } - public object GetFeature(Type t) + public object? GetFeature(Type t) { return null; } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { } - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { } @@ -464,16 +465,16 @@ namespace Avalonia.Headless } - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadow = default) + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadow = default) { } - public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { } @@ -484,7 +485,7 @@ namespace Avalonia.Headless } } - class HeadlessRenderTarget : IRenderTarget + private class HeadlessRenderTarget : IRenderTarget { public void Dispose() { diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 46e3515d11..ecdc7a1d35 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -18,17 +18,17 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessClipboardStub : IClipboard + internal class HeadlessClipboardStub : IClipboard { - private string _text; - private IDataObject _data; + private string? _text; + private IDataObject? _data; - public Task GetTextAsync() + public Task GetTextAsync() { return Task.Run(() => _text); } - public Task SetTextAsync(string text) + public Task SetTextAsync(string? text) { return Task.Run(() => _text = text); } @@ -45,16 +45,29 @@ namespace Avalonia.Headless public Task GetFormatsAsync() { - throw new NotImplementedException(); + return Task.Run(() => + { + if (_data is not null) + { + return _data.GetDataFormats().ToArray(); + } + + if (_text is not null) + { + return new[] { DataFormats.Text }; + } + + return Array.Empty(); + }); } - public async Task GetDataAsync(string format) + public async Task GetDataAsync(string format) { return await Task.Run(() => _data); } } - class HeadlessCursorFactoryStub : ICursorFactory + internal class HeadlessCursorFactoryStub : ICursorFactory { public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); @@ -65,7 +78,7 @@ namespace Avalonia.Headless } } - class HeadlessGlyphTypefaceImpl : IGlyphTypeface + internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface { public FontMetrics Metrics => new FontMetrics { @@ -117,7 +130,7 @@ namespace Avalonia.Headless public bool TryGetTable(uint tag, out byte[] table) { - table = null; + table = null!; return false; } @@ -133,7 +146,7 @@ namespace Avalonia.Headless } } - class HeadlessTextShaperStub : ITextShaperImpl + internal class HeadlessTextShaperStub : ITextShaperImpl { public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { @@ -145,7 +158,7 @@ namespace Avalonia.Headless } } - class HeadlessFontManagerStub : IFontManagerImpl + internal class HeadlessFontManagerStub : IFontManagerImpl { public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) { @@ -163,17 +176,16 @@ namespace Avalonia.Headless } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; } } - class HeadlessIconLoaderStub : IPlatformIconLoader + internal class HeadlessIconLoaderStub : IPlatformIconLoader { - - class IconStub : IWindowIconImpl + private class IconStub : IWindowIconImpl { public void Save(Stream outputStream) { @@ -196,7 +208,7 @@ namespace Avalonia.Headless } } - class HeadlessScreensStub : IScreenImpl + internal class HeadlessScreensStub : IScreenImpl { public int ScreenCount { get; } = 1; @@ -206,40 +218,19 @@ namespace Avalonia.Headless new PixelRect(0, 0, 1920, 1280), true), }; - public Screen ScreenFromPoint(PixelPoint point) + public Screen? ScreenFromPoint(PixelPoint point) { return ScreenHelper.ScreenFromPoint(point, AllScreens); } - public Screen ScreenFromRect(PixelRect rect) + public Screen? ScreenFromRect(PixelRect rect) { return ScreenHelper.ScreenFromRect(rect, AllScreens); } - public Screen ScreenFromWindow(IWindowBaseImpl window) + public Screen? ScreenFromWindow(IWindowBaseImpl window) { return ScreenHelper.ScreenFromWindow(window, AllScreens); } } - - internal class NoopStorageProvider : BclStorageProvider - { - public override bool CanOpen => false; - public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - - public override bool CanSave => false; - public override Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - return Task.FromResult(null); - } - - public override bool CanPickFolder => false; - public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs index 046e4645e3..fa455c8032 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs @@ -6,16 +6,16 @@ using Avalonia.Threading; namespace Avalonia.Headless { - class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface + internal class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface { public HeadlessPlatformThreadingInterface() { _thread = Thread.CurrentThread; } - private AutoResetEvent _event = new AutoResetEvent(false); - private Thread _thread; - private object _lock = new object(); + private readonly AutoResetEvent _event = new AutoResetEvent(false); + private readonly Thread? _thread; + private readonly object _lock = new object(); private DispatcherPriority? _signaledPriority; public void RunLoop(CancellationToken cancellationToken) @@ -40,7 +40,7 @@ namespace Avalonia.Headless interval = TimeSpan.FromMilliseconds(10); var stopped = false; - Timer timer = null; + Timer? timer = null; timer = new Timer(_ => { if (stopped) @@ -55,7 +55,7 @@ namespace Avalonia.Headless finally { if (!stopped) - timer.Change(interval, Timeout.InfiniteTimeSpan); + timer?.Change(interval, Timeout.InfiniteTimeSpan); } }); }, @@ -81,6 +81,6 @@ namespace Avalonia.Headless } public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; - public event Action Signaled; + public event Action? Signaled; } } diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 15e2c696ac..f2f31debad 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -18,20 +18,20 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow + internal class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow { - private IKeyboardDevice _keyboard; - private Stopwatch _st = Stopwatch.StartNew(); - private Pointer _mousePointer; - private WriteableBitmap _lastRenderedFrame; - private object _sync = new object(); + private readonly IKeyboardDevice _keyboard; + private readonly Stopwatch _st = Stopwatch.StartNew(); + private readonly Pointer _mousePointer; + private WriteableBitmap? _lastRenderedFrame; + private readonly object _sync = new object(); public bool IsPopup { get; } public HeadlessWindowImpl(bool isPopup) { IsPopup = isPopup; Surfaces = new object[] { this }; - _keyboard = AvaloniaLocator.Current.GetService(); + _keyboard = AvaloniaLocator.Current.GetRequiredService(); _mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); MouseDevice = new MouseDevice(_mousePointer); ClientSize = new Size(1024, 768); @@ -49,13 +49,13 @@ namespace Avalonia.Headless public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } - public Action Input { get; set; } - public Action Paint { get; set; } - public Action Resized { get; set; } - public Action ScalingChanged { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } + public Action? ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => - new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor, () => Surfaces); + new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor!, () => Surfaces); public void Invalidate(Rect rect) { @@ -66,18 +66,18 @@ namespace Avalonia.Headless InputRoot = inputRoot; } - public IInputRoot InputRoot { get; set; } + public IInputRoot? InputRoot { get; set; } public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); - public void SetCursor(ICursorImpl cursor) + public void SetCursor(ICursorImpl? cursor) { } - public Action Closed { get; set; } + public Action? Closed { get; set; } public IMouseDevice MouseDevice { get; } public void Show(bool activate, bool isDialog) @@ -102,14 +102,14 @@ namespace Avalonia.Headless } public PixelPoint Position { get; set; } - public Action PositionChanged { get; set; } + public Action? PositionChanged { get; set; } public void Activate() { Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); } - public Action Deactivated { get; set; } - public Action Activated { get; set; } + public Action? Deactivated { get; set; } + public Action? Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); public void Resize(Size clientSize, PlatformResizeReason reason) @@ -124,7 +124,7 @@ namespace Avalonia.Headless }); } - void DoResize(Size clientSize) + private void DoResize(Size clientSize) { // Uncomment this check and experience a weird bug in layout engine if (ClientSize != clientSize) @@ -146,8 +146,8 @@ namespace Avalonia.Headless public IScreenImpl Screen { get; } = new HeadlessScreensStub(); public WindowState WindowState { get; set; } - public Action WindowStateChanged { get; set; } - public void SetTitle(string title) + public Action? WindowStateChanged { get; set; } + public void SetTitle(string? title) { } @@ -157,7 +157,7 @@ namespace Avalonia.Headless } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { } @@ -172,9 +172,9 @@ namespace Avalonia.Headless } - public Func Closing { get; set; } + public Func? Closing { get; set; } - class FramebufferProxy : ILockedFramebuffer + private class FramebufferProxy : ILockedFramebuffer { private readonly ILockedFramebuffer _fb; private readonly Action _onDispose; @@ -215,7 +215,7 @@ namespace Avalonia.Headless }); } - public IRef GetLastRenderedFrame() + public IRef? GetLastRenderedFrame() { lock (_sync) return _lastRenderedFrame?.PlatformImpl?.CloneAs(); @@ -224,19 +224,19 @@ namespace Avalonia.Headless private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; // TODO: Hook recent Popup changes. - IPopupPositioner IPopupImpl.PopupPositioner => null; + IPopupPositioner IPopupImpl.PopupPositioner => null!; public Size MaxAutoSizeHint => new Size(1920, 1080); - public Action TransparencyLevelChanged { get; set; } + public Action? TransparencyLevelChanged { get; set; } public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; - public Action GotInputWhenDisabled { get; set; } + public Action? GotInputWhenDisabled { get; set; } public bool IsClientAreaExtendedToDecorations => false; - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public Action? ExtendClientAreaToDecorationsChanged { get; set; } public bool NeedsManagedDecorations => false; @@ -244,32 +244,27 @@ namespace Avalonia.Headless public Thickness OffScreenMargin => new Thickness(); - public Action LostFocus { get; set; } + public Action? LostFocus { get; set; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); - public object TryGetFeature(Type featureType) + public object? TryGetFeature(Type featureType) { - if (featureType == typeof(IStorageProvider)) - { - return new NoopStorageProvider(); - } - return null; } void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyDown, key, modifiers)); } void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyUp, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); } void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, button == 0 ? RawPointerEventType.LeftButtonDown : button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, point, modifiers)); @@ -277,13 +272,13 @@ namespace Avalonia.Headless void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, RawPointerEventType.Move, point, modifiers)); } void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, button == 0 ? RawPointerEventType.LeftButtonUp : button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, point, modifiers)); @@ -291,14 +286,14 @@ namespace Avalonia.Headless void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot, + 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)); + Input?.Invoke(new RawDragEvent(device, type, InputRoot!, point, data, effects, modifiers)); } void IWindowImpl.Move(PixelPoint point) @@ -306,7 +301,7 @@ namespace Avalonia.Headless } - public IPopupImpl CreatePopup() + public IPopupImpl? CreatePopup() { // TODO: Hook recent Popup changes. return null; diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index dfb3a4c433..42ec38a934 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -8,7 +8,7 @@ namespace Avalonia.Headless { public interface IHeadlessWindow { - IRef GetLastRenderedFrame(); + IRef? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); From a26566548a548c63b55385619c656ebcdb387479 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:29:58 -0500 Subject: [PATCH 04/54] Add headless xunit integration project --- Avalonia.sln | 7 +++ src/Avalonia.Controls/AppBuilder.cs | 39 +++++++++++- .../Avalonia.Controls.csproj | 1 + .../Remote/RemoteDesignerEntryPoint.cs | 12 +--- .../Avalonia.Headless.XUnit.csproj | 19 ++++++ .../AvaloniaTestFramework.cs | 35 +++++++++++ .../AvaloniaTestFrameworkAttribute.cs | 45 ++++++++++++++ .../AvaloniaTestRunner.cs | 61 +++++++++++++++++++ 8 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs diff --git a/Avalonia.sln b/Avalonia.sln index 295f7cb149..6d0c4dee2a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -248,6 +248,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -579,6 +581,10 @@ Global {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -648,6 +654,7 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index cf79fcd1a8..871d6bf52a 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -116,6 +116,43 @@ namespace Avalonia }; } + /// + /// Begin configuring an . + /// Should only be used for testing and design purposes, as it relies on dynamic code. + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + /// An instance. If can't be created, thrown an exception. + internal static AppBuilder Configure( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type entryPointType) + { + var appBuilderObj = entryPointType + .GetMethod( + "BuildAvaloniaApp", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, + null, + Array.Empty(), + null)? + .Invoke(null, Array.Empty()); + + if (appBuilderObj is AppBuilder appBuilder) + { + return appBuilder; + } + + if (typeof(Application).IsAssignableFrom(entryPointType)) + { + return Configure(() => (Application)Activator.CreateInstance(entryPointType)!); + } + + throw new InvalidOperationException( + $"Unable to create AppBuilder from type {entryPointType.Name}." + + $"Input type either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application type."); + } + protected AppBuilder Self => this; public AppBuilder AfterSetup(Action callback) @@ -204,7 +241,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; return Self; } - + /// /// Sets up the platform-specific services for the . /// diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 5adb5a8f2e..b9e2d3d259 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -17,5 +17,6 @@ + diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 85605ccd9d..313063269b 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -179,17 +179,9 @@ namespace Avalonia.DesignerSupport.Remote var entryPoint = asm.EntryPoint; if (entryPoint == null) throw Die($"Assembly {args.AppPath} doesn't have an entry point"); - var builderMethod = entryPoint.DeclaringType.GetMethod( - BuilderMethodName, - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, - null, - Array.Empty(), - null); - if (builderMethod == null) - throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}"); + Log($"Obtaining AppBuilder instance from {entryPoint.DeclaringType!.FullName}"); + var appBuilder = AppBuilder.Configure(entryPoint.DeclaringType); Design.IsDesignMode = true; - Log($"Obtaining AppBuilder instance from {builderMethod.DeclaringType.FullName}.{builderMethod.Name}"); - var appBuilder = builderMethod.Invoke(null, null); Log($"Initializing application in design mode"); var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer)); transport = initializer.ConfigureApp(transport, args, appBuilder); diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj new file mode 100644 index 0000000000..c2c58b4f94 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -0,0 +1,19 @@ + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs new file mode 100644 index 0000000000..21086fa946 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestFramework : XunitTestFramework +{ + public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink) + { + } + + protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) + => new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); + + + private class Executor : XunitTestFrameworkExecutor + { + public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, + IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider, + diagnosticMessageSink) + { + } + + protected override async void RunTestCases(IEnumerable testCases, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) + { + executionOptions.SetValue("xunit.execution.DisableParallelization", false); + using (var assemblyRunner = new AvaloniaTestRunner( + TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, + executionOptions)) await assemblyRunner.RunAsync(); + } + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs new file mode 100644 index 0000000000..d0249a5b25 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +/// +/// +/// +[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] +public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute +{ + /// + /// Creates instance of . + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + public AvaloniaTestFrameworkAttribute( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type appBuilderEntryPointType) { } +} + +/// +/// Discoverer implementation for the Avalonia testing framework. +/// +public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer +{ + /// + /// Creates instance of . + /// + public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _) + { + } + + /// + public Type GetTestFrameworkType(IAttributeInfo attribute) + { + var builderType = attribute.GetConstructorArguments().First() as Type + ?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute."); + return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs new file mode 100644 index 0000000000..579232521d --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs @@ -0,0 +1,61 @@ +using Avalonia.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestRunner : XunitTestAssemblyRunner +{ + private CancellationTokenSource? _cancellationTokenSource; + + public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable testCases, + IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, + executionMessageSink, executionOptions) + { + } + + protected override void SetupSyncContext(int maxParallelThreads) + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new CancellationTokenSource(); + SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result); + } + + public override void Dispose() + { + _cancellationTokenSource?.Dispose(); + base.Dispose(); + } + + internal static Task InitNewApplicationContext(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + + new Thread(() => + { + try + { + var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry)); + + // If windowing subsystem wasn't initialized by user, force headless with default parameters. + if (appBuilder.WindowingSubsystemName != "Headless") + { + appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); + } + + appBuilder.SetupWithoutStarting(); + + tcs.SetResult(SynchronizationContext.Current!); + } + catch (Exception e) + { + tcs.SetException(e); + } + + Dispatcher.UIThread.MainLoop(cancellationToken); + }) { IsBackground = true }.Start(); + + return tcs.Task; + } +} From 5e4509deb167115b7b403754b6d2a38b2eceade9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:30:20 -0500 Subject: [PATCH 05/54] Run tests on Avalonia.Headless --- Avalonia.sln | 7 ++++ nukebuild/Build.cs | 1 + .../HeadlessPlatformRenderInterface.cs | 3 +- .../Avalonia.Headless/HeadlessWindowImpl.cs | 13 ++++++- .../Avalonia.Headless/IHeadlessWindow.cs | 2 +- .../Avalonia.Headless.UnitTests.csproj | 19 +++++++++ .../Avalonia.Headless.UnitTests/InputTests.cs | 37 ++++++++++++++++++ .../RenderingTests.cs | 39 +++++++++++++++++++ .../TestApplication.cs | 24 ++++++++++++ .../ThreadingTests.cs | 32 +++++++++++++++ 10 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj create mode 100644 tests/Avalonia.Headless.UnitTests/InputTests.cs create mode 100644 tests/Avalonia.Headless.UnitTests/RenderingTests.cs create mode 100644 tests/Avalonia.Headless.UnitTests/TestApplication.cs create mode 100644 tests/Avalonia.Headless.UnitTests/ThreadingTests.cs diff --git a/Avalonia.sln b/Avalonia.sln index 6d0c4dee2a..597eb632ca 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -250,6 +250,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -585,6 +587,10 @@ Global {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -655,6 +661,7 @@ Global {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 3704cee890..9883745bfa 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -212,6 +212,7 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Headless.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9944a10f94..7fa2e46c42 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -19,7 +19,8 @@ namespace Avalonia.Headless public static void Initialize() { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new HeadlessPlatformRenderInterface()); + .Bind().ToConstant(new HeadlessPlatformRenderInterface()) + .Bind().ToConstant(new HeadlessFontManagerStub()); } public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index f2f31debad..e1c09107c8 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -215,10 +215,19 @@ namespace Avalonia.Headless }); } - public IRef? GetLastRenderedFrame() + public Bitmap? GetLastRenderedFrame() { lock (_sync) - return _lastRenderedFrame?.PlatformImpl?.CloneAs(); + { + if (_lastRenderedFrame is null) + { + return null; + } + + using var lockedFramebuffer = _lastRenderedFrame.Lock(); + return new Bitmap(lockedFramebuffer.Format, AlphaFormat.Opaque, lockedFramebuffer.Address, + lockedFramebuffer.Size, lockedFramebuffer.Dpi, lockedFramebuffer.RowBytes); + } } private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index 42ec38a934..3391a103d1 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -8,7 +8,7 @@ namespace Avalonia.Headless { public interface IHeadlessWindow { - IRef? GetLastRenderedFrame(); + Bitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); diff --git a/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj new file mode 100644 index 0000000000..78a3ab186e --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + true + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs new file mode 100644 index 0000000000..c4d9f1a517 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class InputTests +{ + [Fact] + public void Should_Click_Button_On_Window() + { + var buttonClicked = false; + var button = new Button + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + + button.Click += (_, _) => buttonClicked = true; + + var window = new Window + { + Width = 100, + Height = 100, + Content = button + }; + window.Show(); + + Dispatcher.UIThread.RunJobs(); + + ((IHeadlessWindow)window.PlatformImpl!).MouseDown(new Point(50, 50), 0); + ((IHeadlessWindow)window.PlatformImpl!).MouseUp(new Point(50, 50), 0); + + Assert.True(buttonClicked); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs new file mode 100644 index 0000000000..67b99541d6 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class RenderingTests +{ + [Fact] + public void Should_Render_Last_Frame_To_Bitmap() + { + var window = new Window + { + Content = new ContentControl + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + Padding = new Thickness(4), + Content = new PathIcon + { + Data = StreamGeometry.Parse("M0,9 L10,0 20,9 19,10 10,2 1,10 z") + } + }, + SizeToContent = SizeToContent.WidthAndHeight + }; + window.Show(); + + Dispatcher.UIThread.RunJobs(); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + + var frame = ((IHeadlessWindow)window.PlatformImpl!).GetLastRenderedFrame(); + Assert.NotNull(frame); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs new file mode 100644 index 0000000000..0b2927fa29 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -0,0 +1,24 @@ +using Avalonia.Headless.XUnit; +using Avalonia.Headless.XUnit.Tests; +using Avalonia.Themes.Simple; +using Xunit; + +[assembly: AvaloniaTestFramework(typeof(TestApplication))] +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Avalonia.Headless.XUnit.Tests; + +public class TestApplication : Application +{ + public TestApplication() + { + Styles.Add(new SimpleTheme()); + } + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseSkia() + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseHeadlessDrawing = false + }); +} diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs new file mode 100644 index 0000000000..efcd2d9081 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class ThreadingTests +{ + [Fact] + public void Should_Be_On_Dispatcher_Thread() + { + Dispatcher.UIThread.VerifyAccess(); + } + + [Fact] + public async Task DispatcherTimer_Works_On_The_Same_Thread() + { + var currentThread = Thread.CurrentThread; + var tcs = new TaskCompletionSource(); + + DispatcherTimer.RunOnce(() => + { + Assert.Equal(currentThread, Thread.CurrentThread); + + tcs.SetResult(); + }, TimeSpan.FromTicks(1)); + + await tcs.Task; + } +} From 6b81b5a93543bb8dcf679b6fca4e2e6c6b2a4261 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:51:42 -0500 Subject: [PATCH 06/54] Make headless easier to use with HeadlessWindowExtensions --- .../HeadlessWindowExtensions.cs | 50 +++++++++++++++++++ .../Avalonia.Headless/HeadlessWindowImpl.cs | 28 ++++++++--- .../Avalonia.Headless/IHeadlessWindow.cs | 8 +-- .../Avalonia.Headless.UnitTests/InputTests.cs | 9 ++-- .../RenderingTests.cs | 10 ++-- .../TestApplication.cs | 6 +-- .../ThreadingTests.cs | 2 +- 7 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs new file mode 100644 index 0000000000..ad5b7e680c --- /dev/null +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -0,0 +1,50 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media.Imaging; +using Avalonia.Threading; + +namespace Avalonia.Headless; + +public static class HeadlessWindowExtensions +{ + public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) + { + var impl = GetImpl(topLevel); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + return impl.GetLastRenderedFrame(); + } + + public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) => + GetImpl(topLevel).GetLastRenderedFrame(); + + public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + GetImpl(topLevel).KeyPress(key, modifiers); + + public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + GetImpl(topLevel).KeyRelease(key, modifiers); + + public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + + public static void MouseMove(this TopLevel topLevel, Point point, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + + public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + + public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + + public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, + DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => + GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + + private static IHeadlessWindow GetImpl(this TopLevel topLevel) + { + Dispatcher.UIThread.RunJobs(); + return topLevel.PlatformImpl as IHeadlessWindow ?? + throw new InvalidOperationException("TopLevel must be a headless window."); + } +} diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index e1c09107c8..35f2774965 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -271,12 +271,18 @@ namespace Avalonia.Headless Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); } - void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseDown(Point point, MouseButton button, RawInputModifiers modifiers) { Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, - button == 0 ? RawPointerEventType.LeftButtonDown : - button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, - point, modifiers)); + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonDown, + MouseButton.Right => RawPointerEventType.RightButtonDown, + MouseButton.Middle => RawPointerEventType.MiddleButtonDown, + MouseButton.XButton1 => RawPointerEventType.XButton1Down, + MouseButton.XButton2 => RawPointerEventType.XButton2Down, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) @@ -285,12 +291,18 @@ namespace Avalonia.Headless RawPointerEventType.Move, point, modifiers)); } - void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseUp(Point point, MouseButton button, RawInputModifiers modifiers) { Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, - button == 0 ? RawPointerEventType.LeftButtonUp : - button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, - point, modifiers)); + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonUp, + MouseButton.Right => RawPointerEventType.RightButtonUp, + MouseButton.Middle => RawPointerEventType.MiddleButtonUp, + MouseButton.XButton1 => RawPointerEventType.XButton1Up, + MouseButton.XButton2 => RawPointerEventType.XButton2Up, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index 3391a103d1..f3da2335bc 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -6,15 +6,15 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - public interface IHeadlessWindow + internal interface IHeadlessWindow { Bitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); - void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseDown(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); - void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseUp(Point point, MouseButton 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); + void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None); } } diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs index c4d9f1a517..3c0ecbfdb7 100644 --- a/tests/Avalonia.Headless.UnitTests/InputTests.cs +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -1,9 +1,10 @@ using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class InputTests { @@ -27,10 +28,8 @@ public class InputTests }; window.Show(); - Dispatcher.UIThread.RunJobs(); - - ((IHeadlessWindow)window.PlatformImpl!).MouseDown(new Point(50, 50), 0); - ((IHeadlessWindow)window.PlatformImpl!).MouseUp(new Point(50, 50), 0); + window.MouseDown(new Point(50, 50), MouseButton.Left); + window.MouseUp(new Point(50, 50), MouseButton.Left); Assert.True(buttonClicked); } diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index 67b99541d6..33f15bce1a 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -1,13 +1,10 @@ -using System.IO; -using System.Linq; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; -using Avalonia.Media.Imaging; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class RenderingTests { @@ -32,8 +29,7 @@ public class RenderingTests Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - - var frame = ((IHeadlessWindow)window.PlatformImpl!).GetLastRenderedFrame(); + var frame = window.CaptureRenderedFrame(); Assert.NotNull(frame); } } diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs index 0b2927fa29..7bfa0144f3 100644 --- a/tests/Avalonia.Headless.UnitTests/TestApplication.cs +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -1,12 +1,12 @@ -using Avalonia.Headless.XUnit; -using Avalonia.Headless.XUnit.Tests; +using Avalonia.Headless.UnitTests; +using Avalonia.Headless.XUnit; using Avalonia.Themes.Simple; using Xunit; [assembly: AvaloniaTestFramework(typeof(TestApplication))] [assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class TestApplication : Application { diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs index efcd2d9081..419ee5519e 100644 --- a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class ThreadingTests { From 2450cf05077af78ba09ae86b41f085a53d81f0a3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:16:31 -0500 Subject: [PATCH 07/54] Update VNC project --- .../Avalonia.Headless.Vnc.csproj | 1 + .../HeadlessVncFramebufferSource.cs | 35 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 3812fb196d..1f06f28687 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -2,6 +2,7 @@ net6.0;netstandard2.0 + true diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index be0f3579a5..7389a9ba15 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -10,22 +10,28 @@ namespace Avalonia.Headless.Vnc { public class HeadlessVncFramebufferSource : IVncFramebufferSource { - public IHeadlessWindow Window { get; set; } + public Window Window { get; set; } private object _lock = new object(); public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32); private VncButton _previousButtons; public HeadlessVncFramebufferSource(VncServerSession session, Window window) { - Window = window.PlatformImpl as IHeadlessWindow ?? throw new InvalidOperationException("Invalid window parameter"); + Window = window; session.PointerChanged += (_, args) => { var pt = new Point(args.X, args.Y); var buttons = (VncButton)args.PressedButtons; - int TranslateButton(VncButton vncButton) => - vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2; + MouseButton TranslateButton(VncButton vncButton) => + vncButton switch + { + VncButton.Left => MouseButton.Left, + VncButton.Middle => MouseButton.Middle, + VncButton.Right => MouseButton.Right, + _ => MouseButton.None + }; var modifiers = (RawInputModifiers)(((int)buttons & 7) << 4); @@ -58,34 +64,25 @@ namespace Avalonia.Headless.Vnc private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; - public VncFramebuffer Capture() + public unsafe VncFramebuffer Capture() { lock (_lock) { using (var bmpRef = Window.GetLastRenderedFrame()) { - if (bmpRef?.Item == null) + if (bmpRef == null) return _framebuffer; - var bmp = bmpRef.Item; + var bmp = bmpRef; if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height) { _framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height, VncPixelFormat.RGB32); } - using (var fb = bmp.Lock()) + var buffer = _framebuffer.GetBuffer(); + fixed (byte* bufferPtr = buffer) { - var buf = _framebuffer.GetBuffer(); - if (_framebuffer.Stride == fb.RowBytes) - Marshal.Copy(fb.Address, buf, 0, buf.Length); - else - for (var y = 0; y < fb.Size.Height; y++) - { - var sourceStart = fb.RowBytes * y; - var dstStart = _framebuffer.Stride * y; - var row = fb.Size.Width * 4; - Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row); - } + bmp.CopyPixels(new PixelRect(default, bmp.PixelSize), (IntPtr)bufferPtr, buffer.Length, _framebuffer.Stride); } } } From f61f8f2bb65aad48761f47c825d490a41def4bbb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:25:24 -0500 Subject: [PATCH 08/54] Minor changes, missing documentation --- .../HeadlessVncFramebufferSource.cs | 3 +- .../AvaloniaTestFrameworkAttribute.cs | 2 +- .../Avalonia.Headless.csproj | 4 ++ .../AvaloniaHeadlessPlatform.cs | 6 ++- .../HeadlessWindowExtensions.cs | 46 +++++++++++++++++-- .../RenderingTests.cs | 2 - 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 7389a9ba15..85caeb1ac5 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Threading; using RemoteViewing.Vnc; using RemoteViewing.Vnc.Server; @@ -68,7 +69,7 @@ namespace Avalonia.Headless.Vnc { lock (_lock) { - using (var bmpRef = Window.GetLastRenderedFrame()) + using (var bmpRef = Window.CaptureRenderedFrame()) { if (bmpRef == null) return _framebuffer; diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs index d0249a5b25..3eace30805 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -5,7 +5,7 @@ using Xunit.Sdk; namespace Avalonia.Headless.XUnit; /// -/// +/// Sets up global avalonia test framework using avalonia application builder passed as a parameter. /// [TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index fbfa89f5a4..b626eaeb68 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 6619b533f6..882f8b842d 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -72,7 +72,11 @@ namespace Avalonia.Headless Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } - + /// + /// Forces renderer to process a rendering timer tick. + /// Use this method before calling . + /// + /// Count of frames to be ticked on the timer. public static void ForceRenderTimerTick(int count = 1) { var timer = AvaloniaLocator.Current.GetService() as RenderTimer; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index ad5b7e680c..5487a7943a 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -3,40 +3,78 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media.Imaging; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Headless; public static class HeadlessWindowExtensions { + /// + /// Triggers a renderer timer tick and captures last rendered frame. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) { - var impl = GetImpl(topLevel); + Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - return impl.GetLastRenderedFrame(); + return topLevel.GetLastRenderedFrame(); } - public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) => - GetImpl(topLevel).GetLastRenderedFrame(); + /// + /// Reads last rendered frame. + /// Note, in order to trigger rendering timer, call method. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. + public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) + { + if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) + { + throw new NotSupportedException("To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); + } + + return GetImpl(topLevel).GetLastRenderedFrame(); + } + /// + /// Simulates keyboard press on the headless window/toplevel. + /// public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => GetImpl(topLevel).KeyPress(key, modifiers); + /// + /// Simulates keyboard release on the headless window/toplevel. + /// public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => GetImpl(topLevel).KeyRelease(key, modifiers); + /// + /// Simulates mouse down on the headless window/toplevel. + /// public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + /// + /// Simulates mouse move on the headless window/toplevel. + /// public static void MouseMove(this TopLevel topLevel, Point point, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + /// + /// Simulates mouse up on the headless window/toplevel. + /// public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + /// + /// Simulates mouse wheel on the headless window/toplevel. + /// public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + /// + /// Simulates drag'n'drop target on the headless window/toplevel. + /// public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index 33f15bce1a..bc50686235 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -27,8 +27,6 @@ public class RenderingTests }; window.Show(); - Dispatcher.UIThread.RunJobs(); - AvaloniaHeadlessPlatform.ForceRenderTimerTick(); var frame = window.CaptureRenderedFrame(); Assert.NotNull(frame); } From c691972f44753cd9b13c0d6f4677a0189c6947cc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:35:29 -0500 Subject: [PATCH 09/54] Add missing InternalsVisibleTo as ScreenHelper was moved --- src/Avalonia.Controls/Avalonia.Controls.csproj | 4 ++++ src/Avalonia.Native/Avalonia.Native.csproj | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index b9e2d3d259..9c4bacbedf 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -18,5 +18,9 @@ + + + + diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 095662a538..e69c39a41e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -26,8 +26,4 @@ - - - - From 46c5db37258d0b1f4cdfc29f18efbf391553ae50 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:53:49 -0500 Subject: [PATCH 10/54] Fixes after VNC check --- .../Properties/launchSettings.json | 6 +++- .../HeadlessVncFramebufferSource.cs | 2 +- .../HeadlessWindowExtensions.cs | 35 +++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Properties/launchSettings.json b/samples/ControlCatalog.NetCore/Properties/launchSettings.json index 5964ca320e..11feb94bb3 100644 --- a/samples/ControlCatalog.NetCore/Properties/launchSettings.json +++ b/samples/ControlCatalog.NetCore/Properties/launchSettings.json @@ -6,6 +6,10 @@ "Dxgi": { "commandName": "Project", "commandLineArgs": "--dxgi" + }, + "VNC": { + "commandName": "Project", + "commandLineArgs": "--vnc" } } -} \ No newline at end of file +} diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 85caeb1ac5..24703003da 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -69,7 +69,7 @@ namespace Avalonia.Headless.Vnc { lock (_lock) { - using (var bmpRef = Window.CaptureRenderedFrame()) + using (var bmpRef = Window.GetLastRenderedFrame()) { if (bmpRef == null) return _framebuffer; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index 5487a7943a..8fbc5ec6ef 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -8,6 +8,9 @@ using Avalonia.Threading; namespace Avalonia.Headless; +/// +/// Set of extension methods to simplify usage of Avalonia.Headless platform. +/// public static class HeadlessWindowExtensions { /// @@ -20,7 +23,7 @@ public static class HeadlessWindowExtensions AvaloniaHeadlessPlatform.ForceRenderTimerTick(); return topLevel.GetLastRenderedFrame(); } - + /// /// Reads last rendered frame. /// Note, in order to trigger rendering timer, call method. @@ -30,7 +33,8 @@ public static class HeadlessWindowExtensions { if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) { - throw new NotSupportedException("To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); + throw new NotSupportedException( + "To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); } return GetImpl(topLevel).GetLastRenderedFrame(); @@ -40,49 +44,58 @@ public static class HeadlessWindowExtensions /// Simulates keyboard press on the headless window/toplevel. /// public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - GetImpl(topLevel).KeyPress(key, modifiers); + RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers); /// /// Simulates keyboard release on the headless window/toplevel. /// public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - GetImpl(topLevel).KeyRelease(key, modifiers); + RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers); /// /// Simulates mouse down on the headless window/toplevel. /// public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers); /// /// Simulates mouse move on the headless window/toplevel. /// public static void MouseMove(this TopLevel topLevel, Point point, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers); /// /// Simulates mouse up on the headless window/toplevel. /// public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers); /// /// Simulates mouse wheel on the headless window/toplevel. /// public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers); /// /// Simulates drag'n'drop target on the headless window/toplevel. /// public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => - GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); - private static IHeadlessWindow GetImpl(this TopLevel topLevel) + private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel) { Dispatcher.UIThread.RunJobs(); + return GetImpl(topLevel); + } + + private static IHeadlessWindow GetImpl(this TopLevel topLevel) + { return topLevel.PlatformImpl as IHeadlessWindow ?? - throw new InvalidOperationException("TopLevel must be a headless window."); + throw new InvalidOperationException("TopLevel must be a headless window."); } } From 4d415d544912fb5e1f7e61b801e2588f9956bac0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 02:23:03 -0500 Subject: [PATCH 11/54] Use Cancel instead of Dispose in CTS --- src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs index 579232521d..42604adf46 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs @@ -24,7 +24,7 @@ internal class AvaloniaTestRunner : XunitTestAssemblyRunner public override void Dispose() { - _cancellationTokenSource?.Dispose(); + _cancellationTokenSource?.Cancel(); base.Dispose(); } From bc6773f93052300d30cbd1badba09d8b4e056db4 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 14 Apr 2023 20:47:13 +0200 Subject: [PATCH 12/54] Fix InputContext event handling --- native/Avalonia.Native/src/OSX/AvnView.mm | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index fdc144e3a5..1c950f01a8 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -521,9 +521,17 @@ - (void)keyDown:(NSEvent *)event { - [self keyboardEvent:event withType:KeyDown]; - _lastKeyHandled = [[self inputContext] handleEvent:event]; - [super keyDown:event]; + _lastKeyHandled = false; + + [[self inputContext] handleEvent:event]; + + if(!_lastKeyHandled){ + [self keyboardEvent:event withType:KeyDown]; + } +} + +- (void) doCommandBySelector:(SEL)selector{ + } - (void)keyUp:(NSEvent *)event @@ -578,6 +586,8 @@ - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { + _lastKeyHandled = true; + if([string isKindOfClass:[NSAttributedString class]]) { _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; @@ -619,11 +629,15 @@ - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { + _lastKeyHandled = true; + [self unmarkText]; if(_parent != nullptr) { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + uint32_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); + + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [string UTF8String]); } [[self inputContext] invalidateCharacterCoordinates]; @@ -746,9 +760,9 @@ } - (void) setText:(NSString *)text{ - [_text setString:text]; + [self unmarkText]; - [[self inputContext] discardMarkedText]; + [_text setString:text]; } - (void) setSelection:(int)start :(int)end{ From 25ff3e2004a70823cfd2b400bc9375bf4ccfda60 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 17 Apr 2023 08:50:39 +0200 Subject: [PATCH 13/54] Only initialize imm32 if a text input client is present --- src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index db650db4b0..aabf361844 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -85,16 +85,18 @@ namespace Avalonia.Win32.Input _parent = parent; - var langId= PRIMARYLANGID(LGID(HKL)); + var langId = PRIMARYLANGID(LGID(HKL)); - if(langId != _langId) + if (IsActive) { - DisableImm(); + if (langId != _langId) + { + DisableImm(); + EnableImm(); + } } _langId = langId; - - EnableImm(); } public void ClearLanguageAndWindow() From a6bc0f9f738ad8431b80fa35c6d04a1e6d2a61d0 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 17 Apr 2023 08:52:20 +0200 Subject: [PATCH 14/54] [TextLayout] Do not call Draw with current TextLine.Start offset --- src/Avalonia.Base/Media/TextFormatting/TextLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index a382416b8a..f373e0178a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -174,7 +174,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in _textLines) { - textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); + textLine.Draw(context, new Point(currentX, currentY)); currentY += textLine.Height; } From be40f919595cb5071a7da935365e72c8c5dfe14c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 17 Apr 2023 12:28:49 +0200 Subject: [PATCH 15/54] Minor tweaks --- native/Avalonia.Native/src/OSX/AvnView.mm | 34 +++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 1c950f01a8..f29508c851 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -22,7 +22,7 @@ AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; NSRect _cursorRect; - NSMutableString* _text; + NSMutableAttributedString* _text; NSRange _selection; } @@ -59,6 +59,11 @@ [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; _modifierState = AvnInputModifiersNone; + + _text = [[NSMutableAttributedString alloc] initWithString:@""]; + _markedText = [[NSMutableAttributedString alloc] initWithString:@""]; + _selection = NSMakeRange(NSNotFound, 0); + return self; } @@ -530,10 +535,6 @@ } } -- (void) doCommandBySelector:(SEL)selector{ - -} - - (void)keyUp:(NSEvent *)event { [self keyboardEvent:event withType:KeyUp]; @@ -575,7 +576,7 @@ - (NSRange)markedRange { if([_markedText length] > 0) - return NSMakeRange(0, [_markedText length] - 1); + return NSMakeRange(_selection.location, [_markedText length]); return NSMakeRange(NSNotFound, 0); } @@ -608,8 +609,11 @@ { [[_markedText mutableString] setString:@""]; - [[self inputContext] discardMarkedText]; - + if([self inputContext]) { + [[self inputContext] discardMarkedText]; + [[self inputContext] invalidateCharacterCoordinates]; + } + if(!_parent->InputMethod->IsActive()){ return; } @@ -631,8 +635,6 @@ { _lastKeyHandled = true; - [self unmarkText]; - if(_parent != nullptr) { uint32_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); @@ -640,7 +642,7 @@ _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [string UTF8String]); } - [[self inputContext] invalidateCharacterCoordinates]; + //[self unmarkText]; } - (NSUInteger)characterIndexForPoint:(NSPoint)point @@ -762,13 +764,15 @@ - (void) setText:(NSString *)text{ [self unmarkText]; - [_text setString:text]; + [[_text mutableString] setString:text]; } - (void) setSelection:(int)start :(int)end{ _selection = NSMakeRange(start, end - start); - [[self inputContext] invalidateCharacterCoordinates]; + if([self inputContext]) { + [[self inputContext] invalidateCharacterCoordinates]; + } } - (void) setCursorRect:(AvnRect)rect{ @@ -780,7 +784,9 @@ _cursorRect = windowRectOnScreen; - [[self inputContext] invalidateCharacterCoordinates]; + if([self inputContext]) { + [[self inputContext] invalidateCharacterCoordinates]; + } } @end From 5926c994c52c9bbb1260d993701203c70e17cd9c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 14 Apr 2023 16:51:35 +0200 Subject: [PATCH 16/54] Added ThumbAutomationPeer. To make slider thumb visible to automation. --- .../Automation/Peers/ThumbAutomationPeer.cs | 12 ++++++++++++ src/Avalonia.Controls/Primitives/Thumb.cs | 4 ++++ 2 files changed, 16 insertions(+) create mode 100644 src/Avalonia.Controls/Automation/Peers/ThumbAutomationPeer.cs diff --git a/src/Avalonia.Controls/Automation/Peers/ThumbAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ThumbAutomationPeer.cs new file mode 100644 index 0000000000..1566370df0 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ThumbAutomationPeer.cs @@ -0,0 +1,12 @@ +using Avalonia.Automation.Peers; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Automation.Peers +{ + public class ThumbAutomationPeer : ControlAutomationPeer + { + public ThumbAutomationPeer(Thumb owner) : base(owner) { } + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Thumb; + protected override bool IsContentElementCore() => false; + } +} diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index c205830bc2..993d054f87 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; @@ -45,6 +47,8 @@ namespace Avalonia.Controls.Primitives remove { RemoveHandler(DragCompletedEvent, value); } } + protected override AutomationPeer OnCreateAutomationPeer() => new ThumbAutomationPeer(this); + protected virtual void OnDragStarted(VectorEventArgs e) { } From 576014db05843a6104ae8722a60ae9d56a00995b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 18 Apr 2023 10:06:56 +0200 Subject: [PATCH 17/54] Improve slider integration tests. They are now failing due to #11015. --- samples/IntegrationTestApp/MainWindow.axaml | 17 +++- .../IntegrationTestApp/MainWindow.axaml.cs | 2 + .../ScrollBarTests.cs | 2 +- .../SliderTests.cs | 89 +++++++++++++++++-- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 95378ed717..d8d9678a2d 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -170,10 +170,21 @@ - - + + + + + + + + + - + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index c9c7939c1c..39497f1811 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -270,6 +270,8 @@ namespace IntegrationTestApp this.Get("BasicListBox").SelectedIndex = -1; if (source?.Name == "MenuClickedMenuItemReset") this.Get("ClickedMenuItem").Text = "None"; + if (source?.Name == "ResetSliders") + this.Get("HorizontalSlider").Value = 50; if (source?.Name == "ShowTransparentWindow") ShowTransparentWindow(); if (source?.Name == "ShowTransparentPopup") diff --git a/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs b/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs index e9d0a5d3a4..9d5df2fb46 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ScrollBarTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.IntegrationTests.Appium _session = fixture.Session; var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("ScrollBarTab"); + var tab = tabs.FindElementByName("ScrollBar"); tab.Click(); } diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index 9371a49ade..8b919d996e 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; using Xunit; @@ -15,21 +16,95 @@ namespace Avalonia.IntegrationTests.Appium _session = fixture.Session; var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("SliderTab"); + var tab = tabs.FindElementByName("Slider"); tab.Click(); + + var reset = _session.FindElementByAccessibilityId("ResetSliders"); + reset.Click(); + } + + [Fact] + public void Horizontal_Changes_Value_Dragging_Thumb_Right() + { + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).ClickAndHold(thumb).MoveByOffset(100, 0).Release().Perform(); + + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); + + Assert.True(value > 50); + Assert.Equal(value, boundValue); + + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left > initialThumbRect.Left); + } + + [Fact] + public void Horizontal_Changes_Value_Dragging_Thumb_Left() + { + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).ClickAndHold(thumb).MoveByOffset(-100, 0).Release().Perform(); + + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); + + Assert.True(value < 50); + Assert.Equal(value, boundValue); + + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left < initialThumbRect.Left); } [Fact] - public void Changes_Value_When_Clicking_Increase_Button() + public void Horizontal_Changes_Value_When_Clicking_Increase_Button() { - var slider = _session.FindElementByAccessibilityId("Slider"); + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).MoveToElement(slider).MoveByOffset(100, 0).Click().Perform(); + + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); + + Assert.True(value > 50); + Assert.Equal(value, boundValue); + + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left > initialThumbRect.Left); + } + + [Fact] + public void Horizontal_Changes_Value_When_Clicking_Decrease_Button() + { + var slider = _session.FindElementByAccessibilityId("HorizontalSlider"); + var thumb = slider.FindElementByAccessibilityId("thumb"); + var initialThumbRect = thumb.Rect; + + new Actions(_session).MoveToElement(slider).MoveByOffset(-100, 0).Click().Perform(); - // slider.Text gets the Slider value - Assert.True(double.Parse(slider.Text) == 30); + var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); + var boundValue = double.Parse( + _session.FindElementByAccessibilityId("HorizontalSliderValue").Text, + CultureInfo.InvariantCulture); - new Actions(_session).Click(slider).Perform(); + Assert.True(value < 50); + Assert.Equal(value, boundValue); - Assert.Equal(50, Math.Round(double.Parse(slider.Text))); + var currentThumbRect = thumb.Rect; + Assert.True(currentThumbRect.Left < initialThumbRect.Left); } } } From fe8f349047e6ff7a3b2d44436ab4de9e44c1a3e9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 18 Apr 2023 10:08:31 +0200 Subject: [PATCH 18/54] Added failing test for #11015. Problem is in layout manager. --- .../Layout/LayoutManagerTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index 98d2807db5..9f13520086 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -457,5 +457,39 @@ namespace Avalonia.Base.UnitTests.Layout Assert.Equal(1, layoutCount); } + + [Fact] + public void Child_Can_Invalidate_Parent_Measure_During_Arrange() + { + // Issue #11015. + // + // - Child invalidates parent measure in arrange pass + // - Parent is added to measure & arrange queues + // - Arrange pass dequeues parent + // - Measure is not valid so parent is not arranged + // - Parent is measured + // - Parent has been dequeued from arrange queue so no arrange is performed + var child = new LayoutTestControl(); + var parent = new LayoutTestControl { Child = child }; + var root = new LayoutTestRoot { Child = parent }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + child.DoArrangeOverride = (_, s) => + { + parent.InvalidateMeasure(); + return s; + }; + + child.InvalidateMeasure(); + parent.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(child.IsMeasureValid); + Assert.True(child.IsArrangeValid); + Assert.True(parent.IsMeasureValid); + Assert.True(parent.IsArrangeValid); + } } } From 00315ef37d043e8907f07624465e481086597914 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 18 Apr 2023 10:55:59 +0200 Subject: [PATCH 19/54] Enqueue for arrange after measure. Fixes the problem described in `Child_Can_Invalidate_Parent_Measure_During_Arrange`. To do this, add controls to the arrange queue after they've been measured, not on measure invalidation. Fixes #11015 --- src/Avalonia.Base/Layout/LayoutManager.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index e16be3fa85..94955a18ae 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Avalonia.Logging; using Avalonia.Rendering; using Avalonia.Threading; @@ -64,7 +65,6 @@ namespace Avalonia.Layout } _toMeasure.Enqueue(control); - _toArrange.Enqueue(control); QueueLayoutPass(); } @@ -297,6 +297,8 @@ namespace Avalonia.Layout { control.Measure(control.PreviousMeasure.Value); } + + _toArrange.Enqueue(control); } return true; @@ -313,7 +315,10 @@ namespace Avalonia.Layout return false; } - if (control.IsMeasureValid && !control.IsArrangeValid) + if (!control.IsMeasureValid) + return false; + + if (!control.IsArrangeValid) { if (control is IEmbeddedLayoutRoot embeddedRoot) control.Arrange(new Rect(embeddedRoot.AllocatedSize)); From 3882bc021739e2623953a65a7fa33962181ee62b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 18 Apr 2023 11:27:21 +0200 Subject: [PATCH 20/54] Remove ControlTemplateResult. Use `TempateResult` instead as described in #6666. Fixes #10525. --- .../Templates/FuncControlTemplate.cs | 4 ++-- .../Templates/IControlTemplate.cs | 18 +----------------- .../Templates/ControlTemplate.cs | 3 ++- .../Templates/DataTemplate.cs | 2 +- .../Templates/ItemsPanelTemplate.cs | 2 +- .../Avalonia.Markup.Xaml/Templates/Template.cs | 2 +- .../Templates/TemplateContent.cs | 5 +++-- .../Templates/TreeDataTemplate.cs | 2 +- .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 2 +- .../CompiledBindingExtensionTests.cs | 2 +- .../Xaml/BasicTests.cs | 2 +- .../Xaml/ControlTemplateTests.cs | 6 +++--- 12 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs index 64a883e88c..895ce53907 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs @@ -18,10 +18,10 @@ namespace Avalonia.Controls.Templates { } - public new ControlTemplateResult Build(TemplatedControl param) + public new TemplateResult Build(TemplatedControl param) { var (control, scope) = BuildWithNameScope(param); - return new ControlTemplateResult(control, scope); + return new(control, scope); } } } diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 38ad6561ab..c3f9c9e8aa 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -5,23 +5,7 @@ namespace Avalonia.Controls.Templates /// /// Interface representing a template used to build a . /// - public interface IControlTemplate : ITemplate + public interface IControlTemplate : ITemplate?> { } - - public class ControlTemplateResult : TemplateResult - { - public Control Control { get; } - - public ControlTemplateResult(Control control, INameScope nameScope) : base(control, nameScope) - { - Control = control; - } - - public new void Deconstruct(out Control control, out INameScope scope) - { - control = Control; - scope = NameScope; - } - } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs index 4bbdda31d8..b94eccf7c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Metadata; @@ -13,6 +14,6 @@ namespace Avalonia.Markup.Xaml.Templates public Type? TargetType { get; set; } - public ControlTemplateResult? Build(TemplatedControl control) => TemplateContent.Load(Content); + public TemplateResult? Build(TemplatedControl control) => TemplateContent.Load(Content); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 89b0468c6e..b45898d8bd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates public Control? Build(object? data, Control? existing) { - return existing ?? TemplateContent.Load(Content)?.Control; + return existing ?? TemplateContent.Load(Content)?.Result; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index c228a58990..f31a693e72 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object? Content { get; set; } - public Panel? Build() => (Panel?)TemplateContent.Load(Content)?.Control; + public Panel? Build() => (Panel?)TemplateContent.Load(Content)?.Result; object? ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index 62febebc8c..5999a8021e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object? Content { get; set; } - public Control? Build() => TemplateContent.Load(Content)?.Control; + public Control? Build() => TemplateContent.Load(Content)?.Result; object? ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 08e897c514..504478f9b3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -1,15 +1,16 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates { public static class TemplateContent { - public static ControlTemplateResult? Load(object? templateContent) + public static TemplateResult? Load(object? templateContent) { if (templateContent is Func direct) { - return (ControlTemplateResult?)direct(null); + return (TemplateResult?)direct(null); } if (templateContent is null) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index a5b308523f..98c3b61c9f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -54,7 +54,7 @@ namespace Avalonia.Markup.Xaml.Templates public Control? Build(object? data) { - var visualTreeForItem = TemplateContent.Load(Content)?.Control; + var visualTreeForItem = TemplateContent.Load(Content)?.Result; if (visualTreeForItem != null) { visualTreeForItem.DataContext = data; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index ba96ac15b3..0cc7cc5468 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime scope.Complete(); if(typeof(T) == typeof(Control)) - return new ControlTemplateResult((Control)obj, scope); + return new TemplateResult((Control)obj, scope); return new TemplateResult((T)obj, scope); }; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 57d6a8902a..9f0b84733d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1978,7 +1978,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true; - public Control Build(object data) => TemplateContent.Load(Content)?.Control; + public Control Build(object data) => TemplateContent.Load(Content)?.Result; } public class CustomDataTemplateInherit : CustomDataTemplate { } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 5e30198d00..421ed2c979 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -605,7 +605,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var control = new ContentControl(); - var result = (ContentPresenter)template.Build(control).Control; + var result = (ContentPresenter)template.Build(control).Result; Assert.NotNull(result); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs index 0a45814efe..4404564733 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlTemplateTests.cs @@ -258,7 +258,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml "; var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - var parent = (ContentControl)template.Build(new ContentControl()).Control; + var parent = (ContentControl)template.Build(new ContentControl()).Result; Assert.Equal("parent", parent.Name); @@ -283,7 +283,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(typeof(ContentControl), template.TargetType); - Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Control); + Assert.IsType(typeof(ContentPresenter), template.Build(new ContentControl()).Result); } [Fact] @@ -299,7 +299,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml "; var template = AvaloniaRuntimeXamlLoader.Parse(xaml); - var panel = (Panel)template.Build(new ContentControl()).Control; + var panel = (Panel)template.Build(new ContentControl()).Result; Assert.Equal(2, panel.Children.Count); From 6d880d483bd948b9db05199f64ff87de4be47fd1 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 18 Apr 2023 16:18:47 +0200 Subject: [PATCH 21/54] fix Watermark not shown when having a custom watermark set --- src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index c091d07632..90153d3293 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -875,10 +875,11 @@ namespace Avalonia.Controls { if (_textBox != null) { + SetCurrentValue(TextProperty, String.Empty); + if (string.IsNullOrEmpty(Watermark) && !UseFloatingWatermark) { DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat(); - SetCurrentValue(TextProperty, string.Empty); _defaultText = string.Empty; var watermarkFormat = "<{0}>"; string watermarkText; From 6da9f884de1fc47d4ae638eba438c60aba962cd3 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 19 Apr 2023 06:54:09 +0200 Subject: [PATCH 22/54] More fixes --- native/Avalonia.Native/src/OSX/AvnView.mm | 86 +++++++++++++---------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index f29508c851..6d1ff7cf12 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -12,7 +12,6 @@ { ComPtr _parent; NSTrackingArea* _area; - NSMutableAttributedString* _markedText; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; @@ -23,7 +22,8 @@ AvnAccessibilityElement* _accessibilityChild; NSRect _cursorRect; NSMutableAttributedString* _text; - NSRange _selection; + NSRange _selectedRange; + NSRange _markedRange; } - (void)onClosed @@ -61,8 +61,8 @@ _modifierState = AvnInputModifiersNone; _text = [[NSMutableAttributedString alloc] initWithString:@""]; - _markedText = [[NSMutableAttributedString alloc] initWithString:@""]; - _selection = NSMakeRange(NSNotFound, 0); + _markedRange = NSMakeRange(0, 0); + _selectedRange = NSMakeRange(0, 0); return self; } @@ -541,6 +541,10 @@ [super keyUp:event]; } +- (void) doCommandBySelector:(SEL)selector{ + +} + - (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod { unsigned int rv = 0; @@ -570,55 +574,52 @@ - (BOOL)hasMarkedText { - return [_markedText length] > 0; + return _markedRange.length > 0; } - (NSRange)markedRange { - if([_markedText length] > 0) - return NSMakeRange(_selection.location, [_markedText length]); - return NSMakeRange(NSNotFound, 0); + return _markedRange; } - (NSRange)selectedRange { - return _selection; + return _selectedRange; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { _lastKeyHandled = true; + NSString* markedText; + if([string isKindOfClass:[NSAttributedString class]]) { - _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + markedText = [string string]; } else { - _markedText = [[NSMutableAttributedString alloc] initWithString:string]; + markedText = (NSString*) string; } - if(!_parent->InputMethod->IsActive()){ - return; + _markedRange = NSMakeRange(_selectedRange.location, [markedText length]); + + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText((char*)[markedText UTF8String]); } - - _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]); } - (void)unmarkText { - [[_markedText mutableString] setString:@""]; + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText(nullptr); + } + + _markedRange = NSMakeRange(_selectedRange.location, 0); if([self inputContext]) { [[self inputContext] discardMarkedText]; - [[self inputContext] invalidateCharacterCoordinates]; - } - - if(!_parent->InputMethod->IsActive()){ - return; } - - _parent->InputMethod->Client->SetPreeditText(nullptr); } - (NSArray *)validAttributesForMarkedText @@ -628,21 +629,38 @@ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - return nullptr; + if(actualRange){ + range = *actualRange; + } + + NSAttributedString* subString = [_text attributedSubstringFromRange:range]; + + return subString; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - _lastKeyHandled = true; + if(_parent == nullptr){ + return; + } - if(_parent != nullptr) - { - uint32_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); + NSString* text; - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [string UTF8String]); + if([string isKindOfClass:[NSAttributedString class]]) + { + text = [string string]; + } + else + { + text = (NSString*) string; } - //[self unmarkText]; + [self unmarkText]; + + uint32_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); + + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); + } - (NSUInteger)characterIndexForPoint:(NSPoint)point @@ -762,17 +780,11 @@ } - (void) setText:(NSString *)text{ - [self unmarkText]; - [[_text mutableString] setString:text]; } - (void) setSelection:(int)start :(int)end{ - _selection = NSMakeRange(start, end - start); - - if([self inputContext]) { - [[self inputContext] invalidateCharacterCoordinates]; - } + _selectedRange = NSMakeRange(start, end - start); } - (void) setCursorRect:(AvnRect)rect{ From afc55cb98527a42fa8df88c6fac708508cfffeef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 19 Apr 2023 10:45:34 +0200 Subject: [PATCH 23/54] Fix tests on macOS. `MoveByOffset` caused Appium to hang, but `MoveToElement` with an offset seems to work fine. --- tests/Avalonia.IntegrationTests.Appium/SliderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index 8b919d996e..fa83ee199c 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -72,7 +72,7 @@ namespace Avalonia.IntegrationTests.Appium var thumb = slider.FindElementByAccessibilityId("thumb"); var initialThumbRect = thumb.Rect; - new Actions(_session).MoveToElement(slider).MoveByOffset(100, 0).Click().Perform(); + new Actions(_session).MoveToElement(slider, 100, 0, MoveToElementOffsetOrigin.Center).Click().Perform(); var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); var boundValue = double.Parse( @@ -93,7 +93,7 @@ namespace Avalonia.IntegrationTests.Appium var thumb = slider.FindElementByAccessibilityId("thumb"); var initialThumbRect = thumb.Rect; - new Actions(_session).MoveToElement(slider).MoveByOffset(-100, 0).Click().Perform(); + new Actions(_session).MoveToElement(slider, -100, 0, MoveToElementOffsetOrigin.Center).Click().Perform(); var value = Math.Round(double.Parse(slider.Text, CultureInfo.InvariantCulture)); var boundValue = double.Parse( From a24e0185fc46209dd308fb06d2fe40d3f00f61f2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 19 Apr 2023 18:09:18 +0600 Subject: [PATCH 24/54] Generate fake ref assemblies with patched *Impl and [NotClientImplementable] interfaces --- nukebuild/Build.cs | 2 + nukebuild/Helpers.cs | 24 ++++++++ nukebuild/RefAssemblyGenerator.cs | 99 +++++++++++++++++++++++++++++++ nukebuild/_build.csproj | 14 ++--- 4 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 nukebuild/Helpers.cs create mode 100644 nukebuild/RefAssemblyGenerator.cs diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 40232947d9..630c532686 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -273,6 +273,8 @@ partial class Build : NukeBuild if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, new NumergeNukeLogger())) throw new Exception("Package merge failed"); + RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." + + Parameters.Version + ".nupkg"); }); Target RunTests => _ => _ diff --git a/nukebuild/Helpers.cs b/nukebuild/Helpers.cs new file mode 100644 index 0000000000..d8d06559bf --- /dev/null +++ b/nukebuild/Helpers.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using Nuke.Common.Utilities; + +class Helpers +{ + public static IDisposable UseTempDir(out string dir) + { + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(path); + dir = path; + return DelegateDisposable.CreateBracket(null, () => + { + try + { + Directory.Delete(path, true); + } + catch + { + // ignore + } + }); + } +} diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs new file mode 100644 index 0000000000..912f74cdf9 --- /dev/null +++ b/nukebuild/RefAssemblyGenerator.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using dnlib.DotNet.Writer; + +public class RefAssemblyGenerator +{ + static void PatchRefAssembly(string file) + { + + var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); + var snk = new byte[reader.Length]; + reader.Read(snk, 0, snk.Length); + + var def = AssemblyDef.Load(new MemoryStream(File.ReadAllBytes(file))); + + foreach(var t in def.ManifestModule.Types) + ProcessType(t); + def.Write(file, new ModuleWriterOptions(def.ManifestModule) + { + StrongNameKey = new StrongNameKey(snk), + }); + } + + static void ProcessType(TypeDef type) + { + foreach (var nested in type.NestedTypes) + ProcessType(nested); + if (type.IsInterface) + { + var hideMethods = type.Name.EndsWith("Impl"); + var injectMethod = hideMethods + || type.CustomAttributes.Any(a => + a.AttributeType.FullName.EndsWith("NotClientImplementableAttribute")); + + if (hideMethods) + { + foreach (var m in type.Methods) + { + m.Attributes |= MethodAttributes.Public | MethodAttributes.Assembly; + m.Attributes ^= MethodAttributes.Public; + } + } + + if(injectMethod) + { + type.Methods.Add(new MethodDefUser("NotClientImplementable", + new MethodSig(CallingConvention.Default, 0, type.Module.CorLibTypes.Void), + MethodAttributes.Assembly + | MethodAttributes.Abstract + | MethodAttributes.NewSlot + | MethodAttributes.HideBySig)); + } + } + } + + public static void GenerateRefAsmsInPackage(string packagePath) + { + using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), + ZipArchiveMode.Update)) + { + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("ref/")) + entry.Delete(); + } + + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("lib/")) + { + if (entry.Name.EndsWith(".dll")) + { + using (Helpers.UseTempDir(out var temp)) + { + var file = Path.Combine(temp, entry.Name); + entry.ExtractToFile(file); + PatchRefAssembly(file); + archive.CreateEntryFromFile(file, "ref/" + entry.FullName.Substring(4)); + + } + } + else if (entry.Name.EndsWith(".xml")) + { + var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), + CompressionLevel.Optimal); + using (var src = entry.Open()) + using (var dst = newEntry.Open()) + src.CopyTo(dst); + } + } + } + } + } +} \ No newline at end of file diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 13bac4b7db..cc3ce9f0b0 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -18,6 +18,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -31,18 +32,11 @@ - - - - - - - + + - - - + From f9955f0c79aaed922761646b2c58e9214e9a8b11 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 19 Apr 2023 18:57:45 +0600 Subject: [PATCH 25/54] More patches --- nukebuild/RefAssemblyGenerator.cs | 49 +++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index 912f74cdf9..5c5324ac8f 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -18,31 +18,39 @@ public class RefAssemblyGenerator var def = AssemblyDef.Load(new MemoryStream(File.ReadAllBytes(file))); + var obsoleteAttribute = new TypeRefUser(def.ManifestModule, "System", "ObsoleteAttribute", def.ManifestModule.CorLibTypes.AssemblyRef); + var obsoleteCtor = def.ManifestModule.Import(new MemberRefUser(def.ManifestModule, ".ctor", + new MethodSig(CallingConvention.Default, 0, def.ManifestModule.CorLibTypes.Void, new TypeSig[] + { + def.ManifestModule.CorLibTypes.String + }), obsoleteAttribute)); + foreach(var t in def.ManifestModule.Types) - ProcessType(t); + ProcessType(t, obsoleteCtor); def.Write(file, new ModuleWriterOptions(def.ManifestModule) { StrongNameKey = new StrongNameKey(snk), }); } - static void ProcessType(TypeDef type) + static void ProcessType(TypeDef type, MemberRef obsoleteCtor) { foreach (var nested in type.NestedTypes) - ProcessType(nested); + ProcessType(nested, obsoleteCtor); if (type.IsInterface) { var hideMethods = type.Name.EndsWith("Impl"); var injectMethod = hideMethods || type.CustomAttributes.Any(a => a.AttributeType.FullName.EndsWith("NotClientImplementableAttribute")); - + if (hideMethods) { foreach (var m in type.Methods) { - m.Attributes |= MethodAttributes.Public | MethodAttributes.Assembly; - m.Attributes ^= MethodAttributes.Public; + var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | + MethodAttributes.FamANDAssem | MethodAttributes.Assembly; + m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; } } @@ -55,8 +63,37 @@ public class RefAssemblyGenerator | MethodAttributes.NewSlot | MethodAttributes.HideBySig)); } + + var forceUnstable = type.CustomAttributes.Any(a => + a.AttributeType.FullName.EndsWith("UnstableAttribute")); + + foreach (var m in type.Methods) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Properties) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Events) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + } } + + static void MarkAsUnstable(IMemberDef def, MemberRef obsoleteCtor, bool force) + { + if (!force + || def.HasCustomAttributes == false + || !def.CustomAttributes.Any(a => + a.AttributeType.FullName.EndsWith("UnstableAttribute"))) + return; + + if (def.CustomAttributes.Any(a => a.TypeFullName.EndsWith("ObsoleteAttribute"))) + return; + + def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor, new CAArgument[] + { + new(def.Module.CorLibTypes.String, + "This is a part of unstable API and can be changed in minor releases. You have been warned") + })); + } public static void GenerateRefAsmsInPackage(string packagePath) { From 30064443b14175ccbe901c0d331cd42284a9f119 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 19 Apr 2023 20:16:34 +0600 Subject: [PATCH 26/54] Added [PrivateApi] --- nukebuild/RefAssemblyGenerator.cs | 16 +++++++++------- .../Metadata/PrivateApiAttribute.cs | 9 +++++++++ src/Avalonia.Base/Platform/ICursorFactory.cs | 2 ++ .../Platform/IPlatformRenderInterface.cs | 4 ++-- .../Platform/IPlatformIconLoader.cs | 2 +- .../Platform/IWindowingPlatform.cs | 2 +- 6 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 src/Avalonia.Base/Metadata/PrivateApiAttribute.cs diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index 5c5324ac8f..61cb04c438 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -39,10 +39,13 @@ public class RefAssemblyGenerator ProcessType(nested, obsoleteCtor); if (type.IsInterface) { - var hideMethods = type.Name.EndsWith("Impl"); + var hideMethods = type.Name.EndsWith("Impl") + || (type.HasCustomAttributes && type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute")); + var injectMethod = hideMethods || type.CustomAttributes.Any(a => - a.AttributeType.FullName.EndsWith("NotClientImplementableAttribute")); + a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute"); if (hideMethods) { @@ -65,7 +68,7 @@ public class RefAssemblyGenerator } var forceUnstable = type.CustomAttributes.Any(a => - a.AttributeType.FullName.EndsWith("UnstableAttribute")); + a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); foreach (var m in type.Methods) MarkAsUnstable(m, obsoleteCtor, forceUnstable); @@ -81,11 +84,10 @@ public class RefAssemblyGenerator { if (!force || def.HasCustomAttributes == false - || !def.CustomAttributes.Any(a => - a.AttributeType.FullName.EndsWith("UnstableAttribute"))) + || def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute")) return; - - if (def.CustomAttributes.Any(a => a.TypeFullName.EndsWith("ObsoleteAttribute"))) + + if (def.CustomAttributes.Any(a => a.TypeFullName == "System.ObsoleteAttribute")) return; def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor, new CAArgument[] diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs new file mode 100644 index 0000000000..3f60940c5e --- /dev/null +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Metadata; + +[AttributeUsage(AttributeTargets.Interface)] +public sealed class PrivateApiAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/ICursorFactory.cs b/src/Avalonia.Base/Platform/ICursorFactory.cs index fff1f92d53..99a9a9d7fa 100644 --- a/src/Avalonia.Base/Platform/ICursorFactory.cs +++ b/src/Avalonia.Base/Platform/ICursorFactory.cs @@ -1,9 +1,11 @@ using Avalonia.Input; +using Avalonia.Metadata; #nullable enable namespace Avalonia.Platform { + [PrivateApi] public interface ICursorFactory { ICursorImpl GetCursor(StandardCursorType cursorType); diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 81fe2c046f..6f62c3be1d 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -11,7 +11,7 @@ namespace Avalonia.Platform /// /// Defines the main platform-specific interface for the rendering subsystem. /// - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterface { /// @@ -201,7 +201,7 @@ namespace Avalonia.Platform bool IsSupportedBitmapPixelFormat(PixelFormat format); } - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable { /// diff --git a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs index 4c844ce30f..2ff74cc582 100644 --- a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs +++ b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs @@ -3,7 +3,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + [Unstable, PrivateApi] public interface IPlatformIconLoader { IWindowIconImpl LoadIcon(string fileName); diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5acc5adccd..f6cf8c604e 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -2,7 +2,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + [Unstable, PrivateApi] public interface IWindowingPlatform { IWindowImpl CreateWindow(); From 0f7fba7f7f5e5c99708e68c887d9c39e91ada894 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 19 Apr 2023 20:53:24 +0600 Subject: [PATCH 27/54] SourceLink? --- nukebuild/RefAssemblyGenerator.cs | 131 +++++++++++++++++++----------- nukebuild/_build.csproj | 1 - 2 files changed, 82 insertions(+), 50 deletions(-) diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index 61cb04c438..2c5724e3ab 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -1,39 +1,69 @@ -using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; -using dnlib.DotNet; -using dnlib.DotNet.Emit; -using dnlib.DotNet.Writer; +using ILRepacking; +using Mono.Cecil; +using Mono.Cecil.Cil; public class RefAssemblyGenerator { - static void PatchRefAssembly(string file) + class Resolver : DefaultAssemblyResolver, IAssemblyResolver + { + private readonly string _dir; + Dictionary _cache = new(); + + public Resolver(string dir) + { + _dir = dir; + } + + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + if (_cache.TryGetValue(name.Name, out var asm)) + return asm; + var path = Path.Combine(_dir, name.Name + ".dll"); + if (File.Exists(path)) + return _cache[name.Name] = AssemblyDefinition.ReadAssembly(path, parameters); + return base.Resolve(name, parameters); + } + } + + public static void PatchRefAssembly(string file) { - var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); var snk = new byte[reader.Length]; reader.Read(snk, 0, snk.Length); - - var def = AssemblyDef.Load(new MemoryStream(File.ReadAllBytes(file))); - - var obsoleteAttribute = new TypeRefUser(def.ManifestModule, "System", "ObsoleteAttribute", def.ManifestModule.CorLibTypes.AssemblyRef); - var obsoleteCtor = def.ManifestModule.Import(new MemberRefUser(def.ManifestModule, ".ctor", - new MethodSig(CallingConvention.Default, 0, def.ManifestModule.CorLibTypes.Void, new TypeSig[] - { - def.ManifestModule.CorLibTypes.String - }), obsoleteAttribute)); - - foreach(var t in def.ManifestModule.Types) + + var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) + }); + + var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, + def.MainModule.TypeSystem.CoreLibrary)); + var obsoleteCtor = def.MainModule.ImportReference(new MethodReference(".ctor", + def.MainModule.TypeSystem.Void, obsoleteAttribute) + { + Parameters = { new ParameterDefinition(def.MainModule.TypeSystem.String) } + }); + + foreach(var t in def.MainModule.Types) ProcessType(t, obsoleteCtor); - def.Write(file, new ModuleWriterOptions(def.ManifestModule) + def.Write(file, new WriterParameters() { - StrongNameKey = new StrongNameKey(snk), + StrongNameKeyBlob = snk, + WriteSymbols = def.MainModule.HasSymbols, + SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), + DeterministicMvid = def.MainModule.HasSymbols }); } - static void ProcessType(TypeDef type, MemberRef obsoleteCtor) + static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) { foreach (var nested in type.NestedTypes) ProcessType(nested, obsoleteCtor); @@ -59,12 +89,11 @@ public class RefAssemblyGenerator if(injectMethod) { - type.Methods.Add(new MethodDefUser("NotClientImplementable", - new MethodSig(CallingConvention.Default, 0, type.Module.CorLibTypes.Void), + type.Methods.Add(new MethodDefinition("NotClientImplementable", MethodAttributes.Assembly | MethodAttributes.Abstract | MethodAttributes.NewSlot - | MethodAttributes.HideBySig)); + | MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); } var forceUnstable = type.CustomAttributes.Any(a => @@ -80,21 +109,24 @@ public class RefAssemblyGenerator } } - static void MarkAsUnstable(IMemberDef def, MemberRef obsoleteCtor, bool force) + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, bool force) { if (!force || def.HasCustomAttributes == false || def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute")) return; - if (def.CustomAttributes.Any(a => a.TypeFullName == "System.ObsoleteAttribute")) + if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) return; - def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor, new CAArgument[] + def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor) { - new(def.Module.CorLibTypes.String, - "This is a part of unstable API and can be changed in minor releases. You have been warned") - })); + ConstructorArguments = + { + new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, + "This is a part of unstable API and can be changed in minor releases. You have been warned") + } + }); } public static void GenerateRefAsmsInPackage(string packagePath) @@ -110,29 +142,30 @@ public class RefAssemblyGenerator foreach (var entry in archive.Entries.ToList()) { - if (entry.FullName.StartsWith("lib/")) + if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) { - if (entry.Name.EndsWith(".dll")) - { - using (Helpers.UseTempDir(out var temp)) - { - var file = Path.Combine(temp, entry.Name); - entry.ExtractToFile(file); - PatchRefAssembly(file); - archive.CreateEntryFromFile(file, "ref/" + entry.FullName.Substring(4)); - - } - } - else if (entry.Name.EndsWith(".xml")) - { - var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), - CompressionLevel.Optimal); - using (var src = entry.Open()) - using (var dst = newEntry.Open()) - src.CopyTo(dst); - } + var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), + CompressionLevel.Optimal); + using (var src = entry.Open()) + using (var dst = newEntry.Open()) + src.CopyTo(dst); } } + + var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) + .Select((e => new { s = e.FullName.Split('/'), e = e })) + .Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) + .GroupBy(x => x.Tfm); + foreach(var tfm in libs) + using (Helpers.UseTempDir(out var temp)) + { + foreach (var l in tfm) + l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + PatchRefAssembly(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); + } } } } \ No newline at end of file diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index cc3ce9f0b0..d03746766e 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -18,7 +18,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive From c5ae8bb762589850c1620ca2b07b7169a72515f9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 19 Apr 2023 21:40:26 +0600 Subject: [PATCH 28/54] Fixes --- nukebuild/RefAssemblyGenerator.cs | 6 +++--- src/Avalonia.Base/Threading/IDispatcherImpl.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index 2c5724e3ab..cbe5236bca 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -111,9 +111,9 @@ public class RefAssemblyGenerator static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, bool force) { - if (!force - || def.HasCustomAttributes == false - || def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute")) + if (!force && ( + def.HasCustomAttributes == false + || def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute"))) return; if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 4c30e2eb2c..ccbe3baf9a 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Threading; -[Unstable] +[PrivateApi] public interface IDispatcherImpl { bool CurrentThreadIsLoopThread { get; } @@ -19,7 +19,7 @@ public interface IDispatcherImpl void UpdateTimer(long? dueTimeInMs); } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can @@ -28,14 +28,14 @@ public interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl { event Action ReadyForBackgroundProcessing; void RequestBackgroundProcessing(); } -[Unstable] +[PrivateApi] public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop From e9519e27192f87ac57e5df882870d1b24a10a312 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 19 Apr 2023 19:58:34 -0700 Subject: [PATCH 29/54] Fix NET8 build --- Directory.Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 73954c7f4d..e8d4baba11 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - + $(DefineConstants);NET7SDK From 38c1cc95c6596d3638bac4244d0bf87e98733eba Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 8 Mar 2023 22:41:16 +0900 Subject: [PATCH 30/54] Init AvaloniaListAttribute --- .../Metadata/AvaloniaListAttribute.cs | 12 ++ src/Avalonia.Controls/Shapes/Polygon.cs | 7 +- src/Avalonia.Controls/Shapes/Polyline.cs | 6 +- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 118 ++++++++++++------ .../AvaloniaXamlIlWellKnownTypes.cs | 4 + 5 files changed, 99 insertions(+), 48 deletions(-) create mode 100644 src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs diff --git a/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs new file mode 100644 index 0000000000..f06e6f1ca9 --- /dev/null +++ b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Metadata; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public sealed class AvaloniaListAttribute : Attribute +{ + public string[]? Separators { get; init; } + + // StringSplitOptions.TrimEntries = 2, but only on net6 target. + public StringSplitOptions SplitOptions { get; init; } = StringSplitOptions.RemoveEmptyEntries | (StringSplitOptions)2; +} diff --git a/src/Avalonia.Controls/Shapes/Polygon.cs b/src/Avalonia.Controls/Shapes/Polygon.cs index 70a45f3516..3ac5af2d33 100644 --- a/src/Avalonia.Controls/Shapes/Polygon.cs +++ b/src/Avalonia.Controls/Shapes/Polygon.cs @@ -1,19 +1,18 @@ -using System.Collections.Generic; using Avalonia.Media; namespace Avalonia.Controls.Shapes { public class Polygon : Shape { - public static readonly StyledProperty> PointsProperty = - AvaloniaProperty.Register>("Points"); + public static readonly StyledProperty PointsProperty = + AvaloniaProperty.Register("Points"); static Polygon() { AffectsGeometry(PointsProperty); } - public IList Points + public Points Points { get { return GetValue(PointsProperty); } set { SetValue(PointsProperty, value); } diff --git a/src/Avalonia.Controls/Shapes/Polyline.cs b/src/Avalonia.Controls/Shapes/Polyline.cs index 4b4bb3ffd0..e6edd7a599 100644 --- a/src/Avalonia.Controls/Shapes/Polyline.cs +++ b/src/Avalonia.Controls/Shapes/Polyline.cs @@ -5,8 +5,8 @@ namespace Avalonia.Controls.Shapes { public class Polyline: Shape { - public static readonly StyledProperty> PointsProperty = - AvaloniaProperty.Register>("Points"); + public static readonly StyledProperty PointsProperty = + AvaloniaProperty.Register("Points"); static Polyline() { @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Shapes AffectsGeometry(PointsProperty); } - public IList Points + public Points Points { get { return GetValue(PointsProperty); } set { SetValue(PointsProperty, value); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index d8524cfd88..65fa6f3e8b 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -198,6 +198,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node); } } + + if (type.Equals(types.ColumnDefinition) || type.Equals(types.RowDefinition)) + { + try + { + var gridLength = GridLength.Parse(text); + + result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength); + + var definitionConstructorGridLength = type.GetConstructor(new List {types.GridLength}); + var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength); + var definitionTypeRef = new XamlAstClrTypeReference(node, type, false); + + result = new XamlAstNewClrObjectNode(node, definitionTypeRef, + definitionConstructorGridLength, new List {lengthNode}); + + return true; + } + catch + { + throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node); + } + } if (type.Equals(types.Cursor)) { @@ -211,16 +234,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - if (type.Equals(types.ColumnDefinitions)) - { - return ConvertDefinitionList(node, text, types, types.ColumnDefinitions, types.ColumnDefinition, "column definitions", out result); - } - - if (type.Equals(types.RowDefinitions)) - { - return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result); - } - if (types.IBrush.IsAssignableFrom(type)) { if (Color.TryParse(text, out Color color)) @@ -295,46 +308,69 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - result = null; - return false; - } - - private static bool ConvertDefinitionList( - IXamlAstValueNode node, - string text, - AvaloniaXamlIlWellKnownTypes types, - IXamlType listType, - IXamlType elementType, - string errorDisplayName, - out IXamlAstValueNode result) - { - try + // Keep it in the end, so more specific parsers can be applied. + var itemType = GetElementType(type, context.Configuration.WellKnownTypes); + if (itemType is not null + && types.AvaloniaList.MakeGenericType(itemType).IsAssignableFrom(type)) { - var lengths = GridLength.ParseLengths(text); - - var definitionTypeRef = new XamlAstClrTypeReference(node, elementType, false); - - var definitionConstructorGridLength = elementType.GetConstructor(new List {types.GridLength}); + const StringSplitOptions trimOption = (StringSplitOptions)2; // StringSplitOptions.TrimEntries + var separators = new[] { "," }; + var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption; - IXamlAstValueNode CreateDefinitionNode(GridLength length) + var attribute = type.CustomAttributes.FirstOrDefault(a => a.Type == types.AvaloniaListAttribute); + if (attribute is not null) { - var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, length); - - return new XamlAstNewClrObjectNode(node, definitionTypeRef, - definitionConstructorGridLength, new List {lengthNode}); + if (attribute.Properties.TryGetValue("Separators", out var separatorsArray)) + { + separators = ((Array)separatorsArray)?.OfType().ToArray(); + } + if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj)) + { + splitOptions = (StringSplitOptions)splitOptionsObj; + } + } + + var items = text.Split(separators, splitOptions ^ trimOption); + // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. + if (splitOptions.HasFlag(trimOption)) + { + items = items.Select(i => i.Trim()).ToArray(); } - var definitionNodes = - new List(lengths.Select(CreateDefinitionNode)); + if (itemType is null) + { + throw new XamlX.XamlLoadException($"Type '{type.Name}' is not a collection type.", node); + } - result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, listType, elementType, definitionNodes); + var nodes = new IXamlAstValueNode[items.Length]; + for (var index = 0; index < items.Length; index++) + { + var success = XamlTransformHelpers.TryGetCorrectlyTypedValue( + context, + new XamlAstTextNode(node, items[index], true, context.Configuration.WellKnownTypes.String), + itemType, out var itemNode); + if (!success) + { + result = null; + return false; + } + nodes[index] = itemNode; + } + + result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, type, itemType, nodes); return true; } - catch - { - throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a {errorDisplayName}", node); - } + + result = null; + return false; + } + + private static IXamlType GetElementType(IXamlType type, XamlTypeWellKnownTypes types) + { + return type.GetAllInterfaces().FirstOrDefault(i => + i.FullName.StartsWith(types.IEnumerableT.FullName))? + .GenericArguments[0]; } } } 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 60a7d953ab..62ba2eb5a2 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -33,6 +33,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType InheritDataTypeFromItemsAttribute { get; } public IXamlType MarkupExtensionOptionAttribute { get; } public IXamlType MarkupExtensionDefaultOptionAttribute { get; } + public IXamlType AvaloniaListAttribute { get; } + public IXamlType AvaloniaList { get; } public IXamlType OnExtensionType { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } @@ -141,6 +143,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute"); MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); + AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute"); + AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1"); OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject, AvaloniaProperty, From 862d07fcaf8a50342f8ee96174482b6c7984b8f3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 20 Apr 2023 01:34:25 -0400 Subject: [PATCH 31/54] Support special case for points collection --- .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 56 ++++++++++++------- .../Shapes/PolygonTests.cs | 4 +- .../Shapes/PolylineTests.cs | 4 +- 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 65fa6f3e8b..e88199cdad 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -313,33 +313,51 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions if (itemType is not null && types.AvaloniaList.MakeGenericType(itemType).IsAssignableFrom(type)) { - const StringSplitOptions trimOption = (StringSplitOptions)2; // StringSplitOptions.TrimEntries - var separators = new[] { "," }; - var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption; - - var attribute = type.CustomAttributes.FirstOrDefault(a => a.Type == types.AvaloniaListAttribute); - if (attribute is not null) + string[] items; + // Normalize special case of Points collection. + if (itemType == types.Point) { - if (attribute.Properties.TryGetValue("Separators", out var separatorsArray)) + var pointParts = text.Split(new[] { ",", " " }, StringSplitOptions.RemoveEmptyEntries); + if (pointParts.Length % 2 == 0) { - separators = ((Array)separatorsArray)?.OfType().ToArray(); + items = new string[pointParts.Length / 2]; + for (int i = 0; i < pointParts.Length; i += 2) + { + items[i / 2] = string.Format(CultureInfo.InvariantCulture, "{0} {1}", pointParts[i], + pointParts[i + 1]); + } } - if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj)) + else { - splitOptions = (StringSplitOptions)splitOptionsObj; + throw new XamlX.XamlLoadException($"Invalid PointsList.", node); } } - - var items = text.Split(separators, splitOptions ^ trimOption); - // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. - if (splitOptions.HasFlag(trimOption)) + else { - items = items.Select(i => i.Trim()).ToArray(); - } + const StringSplitOptions trimOption = (StringSplitOptions)2; // StringSplitOptions.TrimEntries + var separators = new[] { "," }; + var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption; - if (itemType is null) - { - throw new XamlX.XamlLoadException($"Type '{type.Name}' is not a collection type.", node); + var attribute = type.CustomAttributes.FirstOrDefault(a => a.Type == types.AvaloniaListAttribute); + if (attribute is not null) + { + if (attribute.Properties.TryGetValue("Separators", out var separatorsArray)) + { + separators = ((Array)separatorsArray)?.OfType().ToArray(); + } + + if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj)) + { + splitOptions = (StringSplitOptions)splitOptionsObj; + } + } + + items = text.Split(separators, splitOptions ^ trimOption); + // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. + if (splitOptions.HasFlag(trimOption)) + { + items = items.Select(i => i.Trim()).ToArray(); + } } var nodes = new IXamlAstValueNode[items.Length]; diff --git a/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs b/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs index 3ac884df7d..b918f7180a 100644 --- a/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Stroke = Brushes.DarkBlue, Stretch = Stretch.Uniform, Fill = Brushes.Violet, - Points = new [] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, + Points = new Points { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, StrokeThickness = 1 } }; @@ -52,7 +52,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Stroke = Brushes.DarkBlue, Stretch = Stretch.Fill, Fill = Brushes.Violet, - Points = new[] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, + Points = new Points { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, StrokeThickness = 5, } }; diff --git a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs index d02d494ff2..12420b524a 100644 --- a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes [Fact] public async Task Polyline_1px_Stroke() { - var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), + var polylinePoints = new Points { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), new Point(9, 1), new Point(10, 0), new Point(15, 0) }; Decorator target = new Decorator @@ -44,7 +44,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes [Fact] public async Task Polyline_10px_Stroke_PenLineJoin() { - var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), + var polylinePoints = new Points { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), new Point(9, 1), new Point(10, 0), new Point(15, 0) }; Decorator target = new Decorator From 477abdd2f02d21025af9afe27881083c92be1c33 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 20 Apr 2023 02:19:00 -0400 Subject: [PATCH 32/54] Remove AvaloniaListAttribute --- .../Metadata/AvaloniaListAttribute.cs | 12 ------------ .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 14 -------------- .../Transformers/AvaloniaXamlIlWellKnownTypes.cs | 2 -- 3 files changed, 28 deletions(-) delete mode 100644 src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs diff --git a/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs b/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs deleted file mode 100644 index f06e6f1ca9..0000000000 --- a/src/Avalonia.Base/Metadata/AvaloniaListAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Avalonia.Metadata; - -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public sealed class AvaloniaListAttribute : Attribute -{ - public string[]? Separators { get; init; } - - // StringSplitOptions.TrimEntries = 2, but only on net6 target. - public StringSplitOptions SplitOptions { get; init; } = StringSplitOptions.RemoveEmptyEntries | (StringSplitOptions)2; -} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index e88199cdad..ded1953dff 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -338,20 +338,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var separators = new[] { "," }; var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption; - var attribute = type.CustomAttributes.FirstOrDefault(a => a.Type == types.AvaloniaListAttribute); - if (attribute is not null) - { - if (attribute.Properties.TryGetValue("Separators", out var separatorsArray)) - { - separators = ((Array)separatorsArray)?.OfType().ToArray(); - } - - if (attribute.Properties.TryGetValue("SplitOptions", out var splitOptionsObj)) - { - splitOptions = (StringSplitOptions)splitOptionsObj; - } - } - items = text.Split(separators, splitOptions ^ trimOption); // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. if (splitOptions.HasFlag(trimOption)) 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 62ba2eb5a2..63683da0db 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -33,7 +33,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType InheritDataTypeFromItemsAttribute { get; } public IXamlType MarkupExtensionOptionAttribute { get; } public IXamlType MarkupExtensionDefaultOptionAttribute { get; } - public IXamlType AvaloniaListAttribute { get; } public IXamlType AvaloniaList { get; } public IXamlType OnExtensionType { get; } public IXamlType UnsetValueType { get; } @@ -143,7 +142,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute"); MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); - AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute"); AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1"); OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject, From c2b280348341d22ae63bda55891b7822eac7f32c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 20 Apr 2023 02:26:05 -0400 Subject: [PATCH 33/54] Nullable attributes --- src/Avalonia.Controls/Shapes/Polygon.cs | 6 +++--- src/Avalonia.Controls/Shapes/Polyline.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Polygon.cs b/src/Avalonia.Controls/Shapes/Polygon.cs index 3ac5af2d33..1a9dc42490 100644 --- a/src/Avalonia.Controls/Shapes/Polygon.cs +++ b/src/Avalonia.Controls/Shapes/Polygon.cs @@ -4,15 +4,15 @@ namespace Avalonia.Controls.Shapes { public class Polygon : Shape { - public static readonly StyledProperty PointsProperty = - AvaloniaProperty.Register("Points"); + public static readonly StyledProperty PointsProperty = + AvaloniaProperty.Register("Points"); static Polygon() { AffectsGeometry(PointsProperty); } - public Points Points + public Points? Points { get { return GetValue(PointsProperty); } set { SetValue(PointsProperty, value); } diff --git a/src/Avalonia.Controls/Shapes/Polyline.cs b/src/Avalonia.Controls/Shapes/Polyline.cs index e6edd7a599..95ab880142 100644 --- a/src/Avalonia.Controls/Shapes/Polyline.cs +++ b/src/Avalonia.Controls/Shapes/Polyline.cs @@ -5,8 +5,8 @@ namespace Avalonia.Controls.Shapes { public class Polyline: Shape { - public static readonly StyledProperty PointsProperty = - AvaloniaProperty.Register("Points"); + public static readonly StyledProperty PointsProperty = + AvaloniaProperty.Register("Points"); static Polyline() { @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Shapes AffectsGeometry(PointsProperty); } - public Points Points + public Points? Points { get { return GetValue(PointsProperty); } set { SetValue(PointsProperty, value); } From c63571dcdadb3ae88688184c6050aad3742488aa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 20 Apr 2023 16:19:46 +0600 Subject: [PATCH 34/54] Reuse SpanHelpers in Avalonia.Build.Tasks --- src/Avalonia.Base/Media/Color.cs | 2 +- src/Avalonia.Base/Utilities/SpanHelpers.cs | 5 +++- .../Avalonia.Build.Tasks.csproj | 1 + src/Avalonia.Build.Tasks/SpanCompat.cs | 26 +------------------ 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index f06f272e51..7b29ec640a 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -9,8 +9,8 @@ using System; using System.Globalization; #if !BUILDTASK using Avalonia.Animation.Animators; -using static Avalonia.Utilities.SpanHelpers; #endif +using static Avalonia.Utilities.SpanHelpers; namespace Avalonia.Media { diff --git a/src/Avalonia.Base/Utilities/SpanHelpers.cs b/src/Avalonia.Base/Utilities/SpanHelpers.cs index 9a5dce9798..f80ac7c046 100644 --- a/src/Avalonia.Base/Utilities/SpanHelpers.cs +++ b/src/Avalonia.Base/Utilities/SpanHelpers.cs @@ -4,7 +4,10 @@ using System.Runtime.CompilerServices; namespace Avalonia.Utilities { - public static class SpanHelpers +#if !BUILDTASK + public +#endif + static class SpanHelpers { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseUInt(this ReadOnlySpan span, NumberStyles style, IFormatProvider provider, out uint value) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e44b7290af..b654c66157 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -115,6 +115,7 @@ + diff --git a/src/Avalonia.Build.Tasks/SpanCompat.cs b/src/Avalonia.Build.Tasks/SpanCompat.cs index be59ff8b6c..00892d56e6 100644 --- a/src/Avalonia.Build.Tasks/SpanCompat.cs +++ b/src/Avalonia.Build.Tasks/SpanCompat.cs @@ -85,31 +85,7 @@ namespace System { return TrimStart().TrimEnd(); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseUInt(NumberStyles style, IFormatProvider provider, out uint value) - { - return uint.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseInt(out int value) - { - return int.TryParse(ToString(), out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseDouble(NumberStyles style, IFormatProvider provider, out double value) - { - return double.TryParse(ToString(), style, provider, out value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryParseByte(NumberStyles style, IFormatProvider provider, out byte value) - { - return byte.TryParse(ToString(), style, provider, out value); - } - + public override string ToString() => _length == 0 ? string.Empty : _s.Substring(_start, _length); internal int IndexOf(ReadOnlySpan v, StringComparison ordinal, int start = 0) From e5acebabcbf8163642a633ee0d9fc696eec17fc1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 20 Apr 2023 18:20:49 +0200 Subject: [PATCH 35/54] Make thumb drag delta relative to root. #10892 changed the thumb drag delta to be relative to the parent, but the problem was that is that if the thumb controls the position of the parent then the delta will be incorrect. This was causing a bug in `TreeDataGrid` headers which were jumping around: the `Thumb` is a child of the header and causes the header to move. --- src/Avalonia.Controls/Primitives/Thumb.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 993d054f87..5dd2bd067b 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -85,7 +85,7 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - var point = e.GetPosition(this.GetVisualParent()); + var point = e.GetPosition((Visual?)this.GetVisualRoot()); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, @@ -100,7 +100,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this.GetVisualParent()); + _lastPoint = e.GetPosition((Visual?)this.GetVisualRoot()); var ev = new VectorEventArgs { @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this.GetVisualParent()), + Vector = (Vector)e.GetPosition((Visual?)this.GetVisualRoot()), }; RaiseEvent(ev); From ec19a0876e925a98083a15e8ae5009e22875d789 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 20 Apr 2023 21:22:36 -0400 Subject: [PATCH 36/54] Cleanup Points collection usage, make it use IList --- src/Avalonia.Base/Media/PolyLineSegment.cs | 10 +++++----- src/Avalonia.Base/Media/PolylineGeometry.cs | 18 +++++++++--------- src/Avalonia.Base/Points.cs | 14 +++++++++++++- src/Avalonia.Controls/Shapes/Polygon.cs | 16 +++++++++++----- src/Avalonia.Controls/Shapes/Polyline.cs | 15 ++++++++++----- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 5c48c11e19..d17a621348 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -10,8 +10,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty PointsProperty - = AvaloniaProperty.Register(nameof(Points)); + public static readonly StyledProperty> PointsProperty + = AvaloniaProperty.Register>(nameof(Points)); /// /// Gets or sets the points. @@ -19,7 +19,7 @@ namespace Avalonia.Media /// /// The points. /// - public Points Points + public IList Points { get => GetValue(PointsProperty); set => SetValue(PointsProperty, value); @@ -37,9 +37,9 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The points. - public PolyLineSegment(IEnumerable points) : this() + public PolyLineSegment(IEnumerable points) { - Points.AddRange(points); + Points = new Points(points); } protected internal override void ApplyTo(StreamGeometryContext ctx) diff --git a/src/Avalonia.Base/Media/PolylineGeometry.cs b/src/Avalonia.Base/Media/PolylineGeometry.cs index dd3c298b5b..b0229b6455 100644 --- a/src/Avalonia.Base/Media/PolylineGeometry.cs +++ b/src/Avalonia.Base/Media/PolylineGeometry.cs @@ -14,8 +14,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly DirectProperty PointsProperty = - AvaloniaProperty.RegisterDirect(nameof(Points), g => g.Points, (g, f) => g.Points = f); + public static readonly DirectProperty> PointsProperty = + AvaloniaProperty.RegisterDirect>(nameof(Points), g => g.Points, (g, f) => g.Points = f); /// /// Defines the property. @@ -23,13 +23,13 @@ namespace Avalonia.Media public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); - private Points _points; + private IList _points; private IDisposable? _pointsObserver; static PolylineGeometry() { AffectsGeometry(IsFilledProperty); - PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as Points)); + PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as IList)); } /// @@ -43,9 +43,9 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// - public PolylineGeometry(IEnumerable points, bool isFilled) : this() + public PolylineGeometry(IEnumerable points, bool isFilled) { - Points.AddRange(points); + _points = new Points(points); IsFilled = isFilled; } @@ -56,7 +56,7 @@ namespace Avalonia.Media /// The points. /// [Content] - public Points Points + public IList Points { get => _points; set => SetAndRaise(PointsProperty, ref _points, value); @@ -97,10 +97,10 @@ namespace Avalonia.Media return geometry; } - private void OnPointsChanged(Points? newValue) + private void OnPointsChanged(IList? newValue) { _pointsObserver?.Dispose(); - _pointsObserver = newValue?.ForEachItem( + _pointsObserver = (newValue as IAvaloniaList)?.ForEachItem( _ => InvalidateGeometry(), _ => InvalidateGeometry(), InvalidateGeometry); diff --git a/src/Avalonia.Base/Points.cs b/src/Avalonia.Base/Points.cs index b655dbcb38..2f88ecd80f 100644 --- a/src/Avalonia.Base/Points.cs +++ b/src/Avalonia.Base/Points.cs @@ -1,6 +1,18 @@ +using System.Collections.Generic; using Avalonia.Collections; namespace Avalonia { - public sealed class Points : AvaloniaList { } + public sealed class Points : AvaloniaList + { + public Points() + { + + } + + public Points(IEnumerable points) : base(points) + { + + } + } } diff --git a/src/Avalonia.Controls/Shapes/Polygon.cs b/src/Avalonia.Controls/Shapes/Polygon.cs index 1a9dc42490..78def84448 100644 --- a/src/Avalonia.Controls/Shapes/Polygon.cs +++ b/src/Avalonia.Controls/Shapes/Polygon.cs @@ -1,21 +1,27 @@ +using System.Collections.Generic; using Avalonia.Media; namespace Avalonia.Controls.Shapes { public class Polygon : Shape { - public static readonly StyledProperty PointsProperty = - AvaloniaProperty.Register("Points"); + public static readonly StyledProperty> PointsProperty = + AvaloniaProperty.Register>("Points"); static Polygon() { AffectsGeometry(PointsProperty); } - public Points? Points + public Polygon() { - get { return GetValue(PointsProperty); } - set { SetValue(PointsProperty, value); } + Points = new Points(); + } + + public IList Points + { + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); } protected override Geometry CreateDefiningGeometry() diff --git a/src/Avalonia.Controls/Shapes/Polyline.cs b/src/Avalonia.Controls/Shapes/Polyline.cs index 95ab880142..2533794f89 100644 --- a/src/Avalonia.Controls/Shapes/Polyline.cs +++ b/src/Avalonia.Controls/Shapes/Polyline.cs @@ -5,8 +5,8 @@ namespace Avalonia.Controls.Shapes { public class Polyline: Shape { - public static readonly StyledProperty PointsProperty = - AvaloniaProperty.Register("Points"); + public static readonly StyledProperty> PointsProperty = + AvaloniaProperty.Register>("Points"); static Polyline() { @@ -14,10 +14,15 @@ namespace Avalonia.Controls.Shapes AffectsGeometry(PointsProperty); } - public Points? Points + public Polyline() { - get { return GetValue(PointsProperty); } - set { SetValue(PointsProperty, value); } + Points = new Points(); + } + + public IList Points + { + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); } protected override Geometry CreateDefiningGeometry() From 56d87931db92f980080302d0966e622e0df5a225 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 20 Apr 2023 21:23:48 -0400 Subject: [PATCH 37/54] Inject array for IList --- .../AvaloniaXamlIlArrayConstantAstNode.cs | 71 +++++++++++++++++++ .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 32 ++++++--- .../Avalonia.Markup.Xaml.Loader/xamlil.github | 2 +- 3 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs new file mode 100644 index 0000000000..339b720d10 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using Avalonia.Controls; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes +{ + class AvaloniaXamlIlArrayConstantAstNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode + { + private readonly IXamlType _elementType; + private readonly IReadOnlyList _values; + + public AvaloniaXamlIlArrayConstantAstNode(IXamlLineInfo lineInfo, IXamlType arrayType, IXamlType elementType, IReadOnlyList values) : base(lineInfo) + { + _elementType = elementType; + _values = values; + + Type = new XamlAstClrTypeReference(lineInfo, arrayType, false); + + foreach (var element in values) + { + if (!elementType.IsAssignableFrom(element.Type.GetClrType())) + { + throw new XamlParseException("x:Array element is not assignable to the array element type!", lineInfo); + } + } + } + + public IXamlAstTypeReference Type { get; } + + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldc_I4(_values.Count) + .Newarr(_elementType); + + for (var index = 0; index < _values.Count; index++) + { + var value = _values[index]; + + codeGen + .Dup() + .Ldc_I4(index); + + context.Emit(value, codeGen, _elementType); + + if (value.Type.GetClrType() is { IsValueType: true } valTypeInObjArr) + { + if (!_elementType.IsValueType) + { + codeGen.Box(valTypeInObjArr); + } + // It seems like ASM codegen for "stelem valuetype" and "stelem.i4" is identical, + // so we don't need to try to optimize it here. + codeGen.Emit(OpCodes.Stelem, valTypeInObjArr); + } + else + { + codeGen.Stelem_ref(); + } + } + + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index ded1953dff..cd005ce24d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -309,13 +309,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } // Keep it in the end, so more specific parsers can be applied. - var itemType = GetElementType(type, context.Configuration.WellKnownTypes); - if (itemType is not null - && types.AvaloniaList.MakeGenericType(itemType).IsAssignableFrom(type)) + var elementType = GetElementType(type, context.Configuration.WellKnownTypes); + if (elementType is not null) { string[] items; // Normalize special case of Points collection. - if (itemType == types.Point) + if (elementType == types.Point) { var pointParts = text.Split(new[] { ",", " " }, StringSplitOptions.RemoveEmptyEntries); if (pointParts.Length % 2 == 0) @@ -352,7 +351,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var success = XamlTransformHelpers.TryGetCorrectlyTypedValue( context, new XamlAstTextNode(node, items[index], true, context.Configuration.WellKnownTypes.String), - itemType, out var itemNode); + elementType, out var itemNode); if (!success) { result = null; @@ -361,9 +360,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions nodes[index] = itemNode; } - - result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, type, itemType, nodes); - return true; + + if (types.AvaloniaList.MakeGenericType(elementType).IsAssignableFrom(type)) + { + result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, type, elementType, nodes); + return true; + } + else if (type.IsArray) + { + result = new AvaloniaXamlIlArrayConstantAstNode(node, elementType.MakeArrayType(1), elementType, nodes); + return true; + } + else if (type == context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType)) + { + var listType = context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType); + result = new AvaloniaXamlIlArrayConstantAstNode(node, listType, elementType, nodes); + return true; + } + + result = null; + return false; } result = null; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 5dd0b042e1..5d1025f30d 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 5dd0b042e144e677638224c49fec16dab66143e8 +Subproject commit 5d1025f30d0ed6d8f419d82959c148276301f393 From 5262eec4cf1f5134ac873a67a386bde9678913ab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 21 Apr 2023 10:38:00 +0200 Subject: [PATCH 38/54] Passing null gives us the point relative to the root. And update the documentation for `GetPosition` to explain what `null` does (as in `GetCurrentPoint`). --- src/Avalonia.Base/Input/PointerEventArgs.cs | 4 ++-- src/Avalonia.Controls/Primitives/Thumb.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 28a3c3aefb..beb953ce8f 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -77,14 +77,14 @@ namespace Avalonia.Input /// /// Gets the pointer position relative to a control. /// - /// The control. + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// The pointer position in the control's coordinates. public Point GetPosition(Visual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); /// /// Returns the PointerPoint associated with the current event /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// public PointerPoint GetCurrentPoint(Visual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 5dd2bd067b..9854bdbea6 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -85,7 +85,7 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - var point = e.GetPosition((Visual?)this.GetVisualRoot()); + var point = e.GetPosition(null); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, @@ -100,7 +100,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition((Visual?)this.GetVisualRoot()); + _lastPoint = e.GetPosition(null); var ev = new VectorEventArgs { @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition((Visual?)this.GetVisualRoot()), + Vector = (Vector)e.GetPosition(null), }; RaiseEvent(ev); From dd1df9a539e601791867832c1bdb75dfaf8ef3ed Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 21 Apr 2023 13:03:49 +0200 Subject: [PATCH 39/54] Do not execute OnClick when access key is pressed in combination with AltGr --- src/Avalonia.Base/Input/AccessKeyHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 13ca140565..2bd9fce947 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -176,7 +176,7 @@ namespace Avalonia.Input { bool menuIsOpen = MainMenu?.IsOpen == true; - if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || menuIsOpen) + if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) && !e.KeyModifiers.HasAllFlags(KeyModifiers.Control) || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. From 981dfd29d74ba2c64b9cca220d2f0e7ec830e201 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 21 Apr 2023 13:49:44 +0200 Subject: [PATCH 40/54] Added WindowBase.Resized event. Which exposes the resize reason and new client size. Required renaming `PlatformResizeReason` to `WindowResizeReason`. Made `TopLevel.HandleResized` method internal. --- .../Platform/SkiaPlatform/TopLevelImpl.cs | 6 +- .../Offscreen/OffscreenTopLevelImpl.cs | 4 +- .../Platform/ITopLevelImpl.cs | 36 +---------- src/Avalonia.Controls/Platform/IWindowImpl.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Controls/Window.cs | 14 ++--- src/Avalonia.Controls/WindowBase.cs | 15 ++++- .../WindowResizedEventArgs.cs | 61 +++++++++++++++++++ .../Remote/PreviewerWindowImpl.cs | 2 +- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 6 +- src/Avalonia.Headless/HeadlessWindowImpl.cs | 6 +- src/Avalonia.Native/PopupImpl.cs | 2 +- src/Avalonia.Native/WindowImplBase.cs | 8 +-- src/Avalonia.X11/X11Window.cs | 14 ++--- .../Avalonia.Browser/BrowserTopLevelImpl.cs | 4 +- .../FramebufferToplevelImpl.cs | 2 +- .../Wpf/WpfTopLevelImpl.cs | 4 +- src/Windows/Avalonia.Win32/PopupImpl.cs | 3 +- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 2 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 6 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 ++--- src/iOS/Avalonia.iOS/AvaloniaView.cs | 4 +- .../Primitives/PopupTests.cs | 4 +- .../TopLevelTests.cs | 2 +- .../WindowTests.cs | 14 ++--- .../CompositorTestServices.cs | 2 +- .../MockWindowingPlatform.cs | 9 +-- 27 files changed, 145 insertions(+), 103 deletions(-) create mode 100644 src/Avalonia.Controls/WindowResizedEventArgs.cs diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 126c488d59..fae1aacf61 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } @@ -156,12 +156,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform protected virtual void OnResized(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Unspecified); + Resized?.Invoke(size, WindowResizeReason.Unspecified); } internal void Resize(Size size) { - Resized?.Invoke(size, PlatformResizeReason.Layout); + Resized?.Invoke(size, WindowResizeReason.Layout); } class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 3a4ae80cf4..387357dddd 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Embedding.Offscreen set { _clientSize = value; - Resized?.Invoke(value, PlatformResizeReason.Unspecified); + Resized?.Invoke(value, WindowResizeReason.Unspecified); } } @@ -65,7 +65,7 @@ namespace Avalonia.Controls.Embedding.Offscreen public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } public Action? TransparencyLevelChanged { get; set; } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 29156f4030..bb6b2304af 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -9,40 +9,6 @@ using Avalonia.Rendering; namespace Avalonia.Platform { - /// - /// Describes the reason for a message. - /// - public enum PlatformResizeReason - { - /// - /// The resize reason is unknown or unspecified. - /// - Unspecified, - - /// - /// The resize was due to the user resizing the window, for example by dragging the - /// window frame. - /// - User, - - /// - /// The resize was initiated by the application, for example by setting one of the sizing- - /// related properties on such as or - /// . - /// - Application, - - /// - /// The resize was initiated by the layout system. - /// - Layout, - - /// - /// The resize was due to a change in DPI. - /// - DpiChange, - } - /// /// Defines a platform-specific top-level window implementation. /// @@ -93,7 +59,7 @@ namespace Avalonia.Platform /// /// Gets or sets a method called when the toplevel is resized. /// - Action? Resized { get; set; } + Action? Resized { get; set; } /// /// Gets or sets a method called when the toplevel's scaling changes. diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 31b144ce00..5591e68235 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -114,7 +114,7 @@ namespace Avalonia.Platform /// /// The new client size. /// The reason for the resize. - void Resize(Size clientSize, PlatformResizeReason reason = PlatformResizeReason.Application); + void Resize(Size clientSize, WindowResizeReason reason = WindowResizeReason.Application); /// /// Sets the client size of the top level. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 07b1e9b51f..eeb44e7bd8 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -569,7 +569,7 @@ namespace Avalonia.Controls /// /// The new client size. /// The reason for the resize. - protected virtual void HandleResized(Size clientSize, PlatformResizeReason reason) + internal virtual void HandleResized(Size clientSize, WindowResizeReason reason) { ClientSize = clientSize; FrameSize = PlatformImpl!.FrameSize; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index f9593f1c1b..48edb81b16 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -227,7 +227,7 @@ namespace Avalonia.Controls impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; - this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); + this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, WindowResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); } @@ -700,7 +700,7 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); @@ -778,7 +778,7 @@ namespace Avalonia.Controls if (initialSize != ClientSize) { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + PlatformImpl?.Resize(initialSize, WindowResizeReason.Layout); } LayoutManager.ExecuteInitialLayoutPass(); @@ -975,7 +975,7 @@ namespace Avalonia.Controls protected sealed override Size ArrangeSetBounds(Size size) { - PlatformImpl?.Resize(size, PlatformResizeReason.Layout); + PlatformImpl?.Resize(size, WindowResizeReason.Layout); return ClientSize; } @@ -994,7 +994,7 @@ namespace Avalonia.Controls } /// - protected sealed override void HandleResized(Size clientSize, PlatformResizeReason reason) + internal override void HandleResized(Size clientSize, WindowResizeReason reason) { if (ClientSize != clientSize || double.IsNaN(Width) || double.IsNaN(Height)) { @@ -1005,8 +1005,8 @@ namespace Avalonia.Controls // to the requested size. if (sizeToContent != SizeToContent.Manual && CanResize && - reason == PlatformResizeReason.Unspecified || - reason == PlatformResizeReason.User) + reason == WindowResizeReason.Unspecified || + reason == WindowResizeReason.User) { if (clientSize.Width != ClientSize.Width) sizeToContent &= ~SizeToContent.Width; diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 814a9b5960..d42244ba5c 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -80,6 +80,11 @@ namespace Avalonia.Controls /// public event EventHandler? PositionChanged; + /// + /// Occurs when the window is resized. + /// + public event EventHandler? Resized; + public new IWindowBaseImpl? PlatformImpl => (IWindowBaseImpl?) base.PlatformImpl; /// @@ -188,6 +193,12 @@ namespace Avalonia.Controls base.OnOpened(e); } + /// + /// Raises the event. + /// + /// An that contains the event data. + protected virtual void OnResized(WindowResizedEventArgs e) => Resized?.Invoke(this, e); + protected override void HandleClosed() { using (FreezeVisibilityChangeHandling()) @@ -208,7 +219,7 @@ namespace Avalonia.Controls /// /// The new client size. /// The reason for the resize. - protected override void HandleResized(Size clientSize, PlatformResizeReason reason) + internal override void HandleResized(Size clientSize, WindowResizeReason reason) { FrameSize = PlatformImpl?.FrameSize; @@ -218,6 +229,8 @@ namespace Avalonia.Controls LayoutManager.ExecuteLayoutPass(); Renderer.Resized(clientSize); } + + OnResized(new WindowResizedEventArgs(clientSize, reason)); } /// diff --git a/src/Avalonia.Controls/WindowResizedEventArgs.cs b/src/Avalonia.Controls/WindowResizedEventArgs.cs new file mode 100644 index 0000000000..daa8aa0f09 --- /dev/null +++ b/src/Avalonia.Controls/WindowResizedEventArgs.cs @@ -0,0 +1,61 @@ +using System; +using Avalonia.Layout; + +namespace Avalonia.Controls +{ + /// + /// Describes the reason for a event. + /// + public enum WindowResizeReason + { + /// + /// The resize reason is unknown or unspecified. + /// + Unspecified, + + /// + /// The resize was due to the user resizing the window, for example by dragging the + /// window frame. + /// + User, + + /// + /// The resize was initiated by the application, for example by setting one of the sizing- + /// related properties on such as or + /// . + /// + Application, + + /// + /// The resize was initiated by the layout system. + /// + Layout, + + /// + /// The resize was due to a change in DPI. + /// + DpiChange, + } + + /// + /// Provides data for the event. + /// + public class WindowResizedEventArgs : EventArgs + { + internal WindowResizedEventArgs(Size clientSize, WindowResizeReason reason) + { + ClientSize = clientSize; + Reason = reason; + } + + /// + /// Gets the new client size of the window in device-independent pixels. + /// + public Size ClientSize { get; } + + /// + /// Gets the reason for the resize. + /// + public WindowResizeReason Reason { get; } + } +} diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index 2da8f38ea9..e0fcf8e530 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -59,7 +59,7 @@ namespace Avalonia.DesignerSupport.Remote base.OnMessage(transport, obj); } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { _transport.Send(new RequestViewportResizeMessage { diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index ea427e4c92..f6f5c185e9 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -32,7 +32,7 @@ namespace Avalonia.DesignerSupport.Remote public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Func Closing { get; set; } public Action Closed { get; set; } @@ -59,7 +59,7 @@ namespace Avalonia.DesignerSupport.Remote PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, (_, size, __) => { - Resize(size, PlatformResizeReason.Unspecified); + Resize(size, WindowResizeReason.Unspecified); })); } @@ -112,7 +112,7 @@ namespace Avalonia.DesignerSupport.Remote { } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index a801474f21..3405a4d4e7 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Headless public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => @@ -111,7 +111,7 @@ namespace Avalonia.Headless public Action Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { // Emulate X11 behavior here if (IsPopup) @@ -129,7 +129,7 @@ namespace Avalonia.Headless if (ClientSize != clientSize) { ClientSize = clientSize; - Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(clientSize, WindowResizeReason.Unspecified); } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 0953527284..516f8f99f1 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -29,7 +29,7 @@ namespace Avalonia.Native private void MoveResize(PixelPoint position, Size size, double scaling) { Position = position; - Resize(size, PlatformResizeReason.Layout); + Resize(size, ResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 0dff46057e..26c3da9d50 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -95,7 +95,7 @@ namespace Avalonia.Native var monitor = Screen.AllScreens.OrderBy(x => x.Scaling) .FirstOrDefault(m => m.Bounds.Contains(Position)); - Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), PlatformResizeReason.Layout); + Resize(new Size(monitor.WorkingArea.Width * 0.75d, monitor.WorkingArea.Height * 0.7d), WindowResizeReason.Layout); } public IAvnWindowBase Native => _native; @@ -160,7 +160,7 @@ namespace Avalonia.Native public Action LostFocus { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action Closed { get; set; } public IMouseDevice MouseDevice => _mouse; public abstract IPopupImpl CreatePopup(); @@ -211,7 +211,7 @@ namespace Avalonia.Native { var s = new Size(size->Width, size->Height); _parent._savedLogicalSize = s; - _parent.Resized?.Invoke(s, (PlatformResizeReason)reason); + _parent.Resized?.Invoke(s, (WindowResizeReason)reason); } } @@ -360,7 +360,7 @@ namespace Avalonia.Native } } - public void Resize(Size clientSize, PlatformResizeReason reason) + public void Resize(Size clientSize, WindowResizeReason reason) { _native?.Resize(clientSize.Width, clientSize.Height, (AvnPlatformResizeReason)reason); } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 38af1b6d7b..0a535d2f57 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -352,7 +352,7 @@ namespace Avalonia.X11 public IEnumerable Surfaces { get; } public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } //TODO public Action? ScalingChanged { get; set; } public Action? Deactivated { get; set; } @@ -509,7 +509,7 @@ namespace Avalonia.X11 UpdateImePosition(); if (changedSize && !updatedSizeViaScaling && !_popup) - Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); }, DispatcherPriority.Layout); if (_useRenderWindow) @@ -590,7 +590,7 @@ namespace Avalonia.X11 UpdateImePosition(); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) - Resize(oldScaledSize, true, PlatformResizeReason.DpiChange); + Resize(oldScaledSize, true, WindowResizeReason.DpiChange); return true; } @@ -642,7 +642,7 @@ namespace Avalonia.X11 { // Occurs once the window has been mapped, which is the earliest the extents // can be retrieved, so invoke event to force update of TopLevel.FrameSize. - Resized?.Invoke(ClientSize, PlatformResizeReason.Unspecified); + Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); } if (atom == _x11.Atoms._NET_WM_STATE) @@ -959,19 +959,19 @@ namespace Avalonia.X11 } - public void Resize(Size clientSize, PlatformResizeReason reason) => Resize(clientSize, false, reason); + public void Resize(Size clientSize, WindowResizeReason reason) => Resize(clientSize, false, reason); public void Move(PixelPoint point) => Position = point; private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); _scalingOverride = scaling; UpdateScaling(true); - Resize(size, true, PlatformResizeReason.Layout); + Resize(size, true, WindowResizeReason.Layout); } private PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * RenderScaling), (int)(size.Height * RenderScaling)); - private void Resize(Size clientSize, bool force, PlatformResizeReason reason) + private void Resize(Size clientSize, bool force, WindowResizeReason reason) { if (!force && clientSize == ClientSize) return; diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index d33f773bfa..8456dc92d0 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -74,7 +74,7 @@ namespace Avalonia.Browser surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } - Resized?.Invoke(newSize, PlatformResizeReason.User); + Resized?.Invoke(newSize, WindowResizeReason.User); (_insetsManager as BrowserInsetsManager)?.NotifySafeAreaPaddingChanged(); } @@ -241,7 +241,7 @@ namespace Avalonia.Browser public Action? SetCssCursor { get; set; } public Action? Input { get; set; } public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } public Action? TransparencyLevelChanged { get; set; } public Action? Closed { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index af4a70f128..ccc8cab8ae 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -65,7 +65,7 @@ using Avalonia.Rendering.Composition; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 13eae1992c..24fd7e3933 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -108,7 +108,7 @@ namespace Avalonia.Win32.Interop.Wpf if (_finalSize == _previousSize) return finalSize; _previousSize = _finalSize; - _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), PlatformResizeReason.Unspecified); + _ttl.Resized?.Invoke(finalSize.ToAvaloniaSize(), WindowResizeReason.Unspecified); return base.ArrangeOverride(finalSize); } @@ -229,7 +229,7 @@ namespace Avalonia.Win32.Interop.Wpf Action ITopLevelImpl.Input { get; set; } //TODO Action ITopLevelImpl.Paint { get; set; } - Action ITopLevelImpl.Resized { get; set; } + Action ITopLevelImpl.Resized { get; set; } Action ITopLevelImpl.ScalingChanged { get; set; } Action ITopLevelImpl.TransparencyLevelChanged { get; set; } diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index 75c1a2d564..1470435134 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Platform; using Avalonia.Win32.Interop; @@ -135,7 +136,7 @@ namespace Avalonia.Win32 private void MoveResize(PixelPoint position, Size size, double scaling) { Move(position); - Resize(size, PlatformResizeReason.Layout); + Resize(size, WindowResizeReason.Layout); //TODO: We ignore the scaling override for now } diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 05d9faa97b..8f9fc5fa80 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -212,7 +212,7 @@ namespace Avalonia.Win32 if (PlatformImpl is { } platformImpl) { platformImpl.Move(position); - platformImpl.Resize(size, PlatformResizeReason.Layout); + platformImpl.Resize(size, WindowResizeReason.Layout); } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0cb8b09579..2a3255bb70 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -133,7 +133,7 @@ namespace Avalonia.Win32 _scaling = dpi / 96.0; ScalingChanged?.Invoke(_scaling); - using (SetResizeReason(PlatformResizeReason.DpiChange)) + using (SetResizeReason(WindowResizeReason.DpiChange)) { SetWindowPos(hWnd, IntPtr.Zero, @@ -611,7 +611,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_ENTERSIZEMOVE: - _resizeReason = PlatformResizeReason.User; + _resizeReason = WindowResizeReason.User; break; case WindowsMessage.WM_SIZE: @@ -658,7 +658,7 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_EXITSIZEMOVE: - _resizeReason = PlatformResizeReason.Unspecified; + _resizeReason = WindowResizeReason.Unspecified; break; case WindowsMessage.WM_MOVE: diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 545513c732..9217f42952 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -59,7 +59,7 @@ namespace Avalonia.Win32 private double _extendTitleBarHint = -1; private readonly bool _isUsingComposition; private readonly IBlurHost? _blurHost; - private PlatformResizeReason _resizeReason; + private WindowResizeReason _resizeReason; private MOUSEMOVEPOINT _lastWmMousePoint; #if USE_MANAGED_DRAG @@ -200,7 +200,7 @@ namespace Avalonia.Win32 public Action? Paint { get; set; } - public Action? Resized { get; set; } + public Action? Resized { get; set; } public Action? ScalingChanged { get; set; } @@ -588,7 +588,7 @@ namespace Avalonia.Win32 public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); - public void Resize(Size value, PlatformResizeReason reason) + public void Resize(Size value, WindowResizeReason reason) { if (WindowState != WindowState.Normal) return; @@ -1053,7 +1053,7 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); - Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), PlatformResizeReason.Layout); + Resize(new Size(rcWindow.Width / RenderScaling, rcWindow.Height / RenderScaling), WindowResizeReason.Layout); unsafe { @@ -1462,7 +1462,7 @@ namespace Avalonia.Win32 /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); - private ResizeReasonScope SetResizeReason(PlatformResizeReason reason) + private ResizeReasonScope SetResizeReason(WindowResizeReason reason) { var old = _resizeReason; _resizeReason = reason; @@ -1487,9 +1487,9 @@ namespace Avalonia.Win32 private struct ResizeReasonScope : IDisposable { private readonly WindowImpl _owner; - private readonly PlatformResizeReason _restore; + private readonly WindowResizeReason _restore; - public ResizeReasonScope(WindowImpl owner, PlatformResizeReason restore) + public ResizeReasonScope(WindowImpl owner, WindowResizeReason restore) { _owner = owner; _restore = restore; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index ec6ea56d9d..6ca0cf7ace 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -150,7 +150,7 @@ namespace Avalonia.iOS public IEnumerable Surfaces { get; set; } public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } public Action Closed { get; set; } @@ -225,7 +225,7 @@ namespace Avalonia.iOS public override void LayoutSubviews() { - _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, PlatformResizeReason.Layout); + _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout); base.LayoutSubviews(); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 34311949ef..765f2d1c19 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -830,7 +830,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; } - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + window.PlatformImpl?.Resize(new Size(700D, 500D), WindowResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); Assert.True(raised); } @@ -886,7 +886,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } }; } - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + window.PlatformImpl?.Resize(new Size(700D, 500D), WindowResizeReason.Unspecified); Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); Assert.False(raised); } diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 62b5d889a8..0884dd306a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests // The user has resized the window, so we can no longer auto-size. var target = new TestTopLevel(impl.Object); - impl.Object.Resized(new Size(100, 200), PlatformResizeReason.Unspecified); + impl.Object.Resized(new Size(100, 200), WindowResizeReason.Unspecified); Assert.Equal(100, target.Width); Assert.Equal(200, target.Height); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index cada2bfa6f..b59f6e03f7 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -704,8 +704,8 @@ namespace Avalonia.Controls.UnitTests var clientSize = new Size(200, 200); var maxClientSize = new Size(480, 480); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) - .Callback((size, reason) => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((size, reason) => { clientSize = size.Constrain(maxClientSize); windowImpl.Object.Resized?.Invoke(clientSize, reason); @@ -853,7 +853,7 @@ namespace Avalonia.Controls.UnitTests target.PlatformImpl.ScalingChanged(1.5); target.PlatformImpl.Resized( new Size(210.66666666666666, 118.66666666666667), - PlatformResizeReason.DpiChange); + WindowResizeReason.DpiChange); Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } @@ -911,7 +911,7 @@ namespace Avalonia.Controls.UnitTests target.LayoutManager.ExecuteLayoutPass(); var windowImpl = Mock.Get(target.PlatformImpl); - windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application)); + windowImpl.Verify(x => x.Resize(new Size(410, 800), WindowResizeReason.Application)); Assert.Equal(410, target.Width); } } @@ -936,7 +936,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(410, 800), PlatformResizeReason.User); + target.PlatformImpl.Resized(new Size(410, 800), WindowResizeReason.User); Assert.Equal(410, target.Width); Assert.Equal(800, target.Height); @@ -963,7 +963,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(400, 810), PlatformResizeReason.User); + target.PlatformImpl.Resized(new Size(400, 810), WindowResizeReason.User); Assert.Equal(400, target.Width); Assert.Equal(810, target.Height); @@ -991,7 +991,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); - target.PlatformImpl.Resized(new Size(410, 810), PlatformResizeReason.Unspecified); + target.PlatformImpl.Resized(new Size(410, 810), WindowResizeReason.Unspecified); Assert.Equal(400, target.Width); Assert.Equal(800, target.Height); diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs index 5ef09a4d0f..de7cbc873c 100644 --- a/tests/Avalonia.UnitTests/CompositorTestServices.cs +++ b/tests/Avalonia.UnitTests/CompositorTestServices.cs @@ -152,7 +152,7 @@ public class CompositorTestServices : IDisposable public IEnumerable Surfaces { get; } = new[] { new DummyFramebufferSurface() }; public Action Input { get; set; } public Action Paint { get; set; } - public Action Resized { get; set; } + public Action Resized { get; set; } public Action ScalingChanged { get; set; } public Action TransparencyLevelChanged { get; set; } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 142a9cd8ee..ca71a97a6e 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives.PopupPositioning; using Moq; using Avalonia.Platform; using Avalonia.Rendering; +using Avalonia.Controls; namespace Avalonia.UnitTests { @@ -54,8 +55,8 @@ namespace Avalonia.UnitTests windowImpl.Object.PositionChanged?.Invoke(x); }); - windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) - .Callback((x, y) => + windowImpl.Setup(x => x.Resize(It.IsAny(), It.IsAny())) + .Callback((x, y) => { var constrainedSize = x.Constrain(s_screenSize); @@ -68,7 +69,7 @@ namespace Avalonia.UnitTests windowImpl.Setup(x => x.Show(true, It.IsAny())).Callback(() => { - windowImpl.Object.Resized?.Invoke(windowImpl.Object.ClientSize, PlatformResizeReason.Unspecified); + windowImpl.Object.Resized?.Invoke(windowImpl.Object.ClientSize, WindowResizeReason.Unspecified); windowImpl.Object.Activated?.Invoke(); }); @@ -87,7 +88,7 @@ namespace Avalonia.UnitTests { clientSize = size.Constrain(s_screenSize); popupImpl.Object.PositionChanged?.Invoke(pos); - popupImpl.Object.Resized?.Invoke(clientSize, PlatformResizeReason.Unspecified); + popupImpl.Object.Resized?.Invoke(clientSize, WindowResizeReason.Unspecified); }); var positioner = new ManagedPopupPositioner(positionerHelper); From cf52ab43cde367fc12ecad06340ce21b3df31fa7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 21 Apr 2023 14:03:34 +0200 Subject: [PATCH 41/54] Added WindowBase.TryGetPlatformHandle. #11062 made `WindowBase.PlatformImpl` internal so we need to expose a different want to get the window handle. --- src/Avalonia.Controls/WindowBase.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index d42244ba5c..c2523207e4 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -160,6 +160,15 @@ namespace Avalonia.Controls } } + /// + /// Trys to get the platform handle for the window. + /// + /// + /// An describing the window handle, or null if the handle + /// could not be retrieved. + /// + public IPlatformHandle? TryGetPlatformHandle() => PlatformImpl?.Handle; + /// /// Ensures that the window is initialized. /// From 4fbcfac67659e3c7725ed24f82080dba132aae11 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 21 Apr 2023 13:47:02 +0100 Subject: [PATCH 42/54] fix build. --- src/Avalonia.Native/PopupImpl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 516f8f99f1..6b7f7e8883 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Native.Interop; using Avalonia.Platform; @@ -29,7 +30,7 @@ namespace Avalonia.Native private void MoveResize(PixelPoint position, Size size, double scaling) { Position = position; - Resize(size, ResizeReason.Layout); + Resize(size, WindowResizeReason.Layout); //TODO: We ignore the scaling override for now } From 2f5586fad0b096a4e4053257c1ed65e8b73cb5aa Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 18 Apr 2023 08:09:51 +0000 Subject: [PATCH 43/54] make refresh visualizer PullDirection internal --- src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index 8dc19eb1d4..530f28fbb6 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty PullDirectionProperty = + internal static readonly StyledProperty PullDirectionProperty = AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); /// From 1bce8de6867a125cb46d8c1b2f5c24a0240d0477 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 22:09:25 -0400 Subject: [PATCH 44/54] Fix merge conflict --- src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b83de21ac4..cefb6772c9 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using Avalonia.Controls.Platform; using Avalonia.Reactive; using Avalonia.Input; using Avalonia.Input.Platform; From d136d03fd1badabb9161f532b5a64df6bd26c5e4 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:24:17 +0200 Subject: [PATCH 45/54] Fix slnf Headless path --- Avalonia.Desktop.slnf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 76620e8b93..73e38f8cb9 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -23,8 +23,6 @@ "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", - "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", - "src\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Avalonia.MicroCom\\Avalonia.MicroCom.csproj", "src\\Avalonia.Native\\Avalonia.Native.csproj", "src\\Avalonia.OpenGL\\Avalonia.OpenGL.csproj", @@ -33,6 +31,8 @@ "src\\Avalonia.Themes.Fluent\\Avalonia.Themes.Fluent.csproj", "src\\Avalonia.Themes.Simple\\Avalonia.Themes.Simple.csproj", "src\\Avalonia.X11\\Avalonia.X11.csproj", + "src\\Headless\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", + "src\\Headless\\Avalonia.Headless\\Avalonia.Headless.csproj", "src\\Linux\\Avalonia.LinuxFramebuffer\\Avalonia.LinuxFramebuffer.csproj", "src\\Markup\\Avalonia.Markup.Xaml.Loader\\Avalonia.Markup.Xaml.Loader.csproj", "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", @@ -65,4 +65,4 @@ "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj" ] } -} \ No newline at end of file +} From 1117332e4d6e83244f8c3dbf44d4f2f10f4d4005 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:24:56 +0200 Subject: [PATCH 46/54] Add AccentColor support for KDE --- .../DBusPlatformSettings.cs | 78 ++++++++++++++----- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index a25bb68458..2b124499b3 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Avalonia.Logging; +using Avalonia.Media; using Avalonia.Platform; using Tmds.DBus.SourceGenerator; @@ -9,7 +10,10 @@ namespace Avalonia.FreeDesktop internal class DBusPlatformSettings : DefaultPlatformSettings { private readonly OrgFreedesktopPortalSettings? _settings; + private PlatformColorValues? _lastColorValues; + private PlatformThemeVariant? _themeVariant; + private Color? _accentColor; public DBusPlatformSettings() { @@ -21,24 +25,33 @@ namespace Avalonia.FreeDesktop _ = TryGetInitialValueAsync(); } - public override PlatformColorValues GetColorValues() - { - return _lastColorValues ?? base.GetColorValues(); - } + public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues(); private async Task TryGetInitialValueAsync() { try { var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + _themeVariant = ReadAsColorScheme(value); + } + catch (Exception ex) + { + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.freedesktop.appearance.color-scheme value", ex); + } + + try + { + var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor"); + _accentColor = ReadAsAccentColor(value); } catch (Exception ex) { - _lastColorValues = base.GetColorValues(); - Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); + Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.kde.kdeglobals.General.AccentColor value", ex); } + + _lastColorValues = BuildPlatformColorValues(); + if (_lastColorValues is not null) + OnColorValuesChanged(_lastColorValues); } private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple) @@ -46,25 +59,48 @@ namespace Avalonia.FreeDesktop if (exception is not null) return; - if (valueTuple is ("org.freedesktop.appearance", "color-scheme", { } value)) + switch (valueTuple) { - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ - _lastColorValues = GetColorValuesFromSetting(value); - OnColorValuesChanged(_lastColorValues); + case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ + _themeVariant = ReadAsColorScheme(colorScheme); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; + case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor): + _accentColor = ReadAsAccentColor(accentColor); + _lastColorValues = BuildPlatformColorValues(); + OnColorValuesChanged(_lastColorValues!); + break; } } - private static PlatformColorValues GetColorValuesFromSetting(DBusVariantItem value) + private PlatformColorValues? BuildPlatformColorValues() + { + if (_themeVariant is { } themeVariant && _accentColor is { } accentColor) + return new PlatformColorValues { ThemeVariant = themeVariant, AccentColor1 = accentColor }; + if (_themeVariant is { } themeVariant1) + return new PlatformColorValues { ThemeVariant = themeVariant1 }; + if (_accentColor is { } accentColor1) + return new PlatformColorValues { AccentColor1 = accentColor1 }; + return null; + } + + private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) { var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; - return new PlatformColorValues - { - ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light - }; + return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; + } + + private static Color ReadAsAccentColor(DBusVariantItem value) + { + var colorStr = ((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value; + var rgb = colorStr.Split(','); + return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2])); } } } From f77b67db97487825d172401f118e205c6bae0510 Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Sun, 23 Apr 2023 22:32:51 +0200 Subject: [PATCH 47/54] Move comment --- src/Avalonia.FreeDesktop/DBusPlatformSettings.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index 2b124499b3..8b2b38bb82 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -62,11 +62,6 @@ namespace Avalonia.FreeDesktop switch (valueTuple) { case ("org.freedesktop.appearance", "color-scheme", { } colorScheme): - /* - 0: No preference - 1: Prefer dark appearance - 2: Prefer light appearance - */ _themeVariant = ReadAsColorScheme(colorScheme); _lastColorValues = BuildPlatformColorValues(); OnColorValuesChanged(_lastColorValues!); @@ -92,6 +87,11 @@ namespace Avalonia.FreeDesktop private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value) { + /* + 0: No preference + 1: Prefer dark appearance + 2: Prefer light appearance + */ var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1; return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light; } From a507e92b31f24bc5cf88bedea89f47eb93c2ce42 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 24 Apr 2023 02:00:23 -0400 Subject: [PATCH 48/54] Fix theme-dependend markup extensions not knowing current theme context --- .../Controls/IResourceDictionary.cs | 2 +- .../Controls/IThemeVariantProvider.cs | 22 +++ .../Controls/ResourceDictionary.cs | 12 +- .../Controls/ResourceNodeExtensions.cs | 46 +++--- .../Styling/IThemeVariantHost.cs | 1 - .../AvaloniaXamlIlCompiler.cs | 3 +- ...iaXamlIlThemeVariantProviderTransformer.cs | 31 ++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../DynamicResourceExtension.cs | 6 +- .../StaticResourceExtension.cs | 22 ++- .../Styling/ResourceInclude.cs | 4 +- .../Themes/FluentBenchmark.cs | 22 ++- .../Themes/ThemeBenchmark.cs | 4 +- .../Xaml/ThemeDictionariesTests.cs | 136 +++++++++++++++++- 14 files changed, 271 insertions(+), 42 deletions(-) create mode 100644 src/Avalonia.Base/Controls/IThemeVariantProvider.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs diff --git a/src/Avalonia.Base/Controls/IResourceDictionary.cs b/src/Avalonia.Base/Controls/IResourceDictionary.cs index 2bd1f65638..6712498bf4 100644 --- a/src/Avalonia.Base/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/IResourceDictionary.cs @@ -18,6 +18,6 @@ namespace Avalonia.Controls /// /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios. /// - IDictionary ThemeDictionaries { get; } + IDictionary ThemeDictionaries { get; } } } diff --git a/src/Avalonia.Base/Controls/IThemeVariantProvider.cs b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs new file mode 100644 index 0000000000..d1dca2efbf --- /dev/null +++ b/src/Avalonia.Base/Controls/IThemeVariantProvider.cs @@ -0,0 +1,22 @@ +using Avalonia.Metadata; +using Avalonia.Styling; + +namespace Avalonia.Controls; + +/// +/// Resource provider with theme variant awareness. +/// Can be used with . +/// +/// +/// This is a helper interface for the XAML compiler to make Key property accessibly by the markup extensions. +/// Which means, it can only be used with ResourceDictionaries and markup extensions in the XAML code. +/// This API might be removed in the future minor updates. +/// +[Unstable] +public interface IThemeVariantProvider : IResourceProvider +{ + /// + /// Key property set by the compiler. + /// + ThemeVariant? Key { get; set; } +} diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 231a19baab..b928cf0672 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : IResourceDictionary + public class ResourceDictionary : IResourceDictionary, IThemeVariantProvider { private object? lastDeferredItemKey; private Dictionary? _inner; private IResourceHost? _owner; private AvaloniaList? _mergedDictionaries; - private AvaloniaDictionary? _themeDictionary; + private AvaloniaDictionary? _themeDictionary; /// /// Initializes a new instance of the class. @@ -93,13 +93,13 @@ namespace Avalonia.Controls } } - public IDictionary ThemeDictionaries + public IDictionary ThemeDictionaries { get { if (_themeDictionary == null) { - _themeDictionary = new AvaloniaDictionary(2); + _themeDictionary = new AvaloniaDictionary(2); _themeDictionary.ForEachItem( (_, x) => { @@ -120,6 +120,8 @@ namespace Avalonia.Controls return _themeDictionary; } } + + ThemeVariant? IThemeVariantProvider.Key { get; set; } bool IResourceNode.HasResources { @@ -192,7 +194,7 @@ namespace Avalonia.Controls if (_themeDictionary is not null) { - IResourceProvider? themeResourceProvider; + IThemeVariantProvider? themeResourceProvider; if (theme is not null && theme != ThemeVariant.Default) { if (_themeDictionary.TryGetValue(theme, out themeResourceProvider) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 8aed1545a5..382ebac0e3 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -119,7 +119,19 @@ namespace Avalonia.Controls resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); key = key ?? throw new ArgumentNullException(nameof(key)); - return new FloatingResourceObservable(resourceProvider, key, converter); + return new FloatingResourceObservable(resourceProvider, key, null, converter); + } + + public static IObservable GetResourceObservable( + this IResourceProvider resourceProvider, + object key, + ThemeVariant? defaultThemeVariant, + Func? converter = null) + { + resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider)); + key = key ?? throw new ArgumentNullException(nameof(key)); + + return new FloatingResourceObservable(resourceProvider, key, defaultThemeVariant, converter); } private class ResourceObservable : LightweightObservableBase @@ -128,7 +140,10 @@ namespace Avalonia.Controls private readonly object _key; private readonly Func? _converter; - public ResourceObservable(IResourceHost target, object key, Func? converter) + public ResourceObservable( + IResourceHost target, + object key, + Func? converter) { _target = target; _key = key; @@ -170,11 +185,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (_target is not IThemeVariantHost themeVariantHost - || !_target.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = (_target as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } @@ -183,14 +195,20 @@ namespace Avalonia.Controls private class FloatingResourceObservable : LightweightObservableBase { private readonly IResourceProvider _target; + private readonly ThemeVariant? _overrideThemeVariant; private readonly object _key; private readonly Func? _converter; private IResourceHost? _owner; - public FloatingResourceObservable(IResourceProvider target, object key, Func? converter) + public FloatingResourceObservable( + IResourceProvider target, + object key, + ThemeVariant? overrideThemeVariant, + Func? converter) { _target = target; _key = key; + _overrideThemeVariant = overrideThemeVariant; _converter = converter; } @@ -233,7 +251,7 @@ namespace Avalonia.Controls { _owner.ResourcesChanged -= ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost) { themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; } @@ -244,12 +262,11 @@ namespace Avalonia.Controls { _owner.ResourcesChanged += ResourcesChanged; } - if (_owner is IThemeVariantHost themeVariantHost2) + if (_overrideThemeVariant is null && _owner is IThemeVariantHost themeVariantHost2) { themeVariantHost2.ActualThemeVariantChanged -= ActualThemeVariantChanged; } - PublishNext(); } @@ -265,11 +282,8 @@ namespace Avalonia.Controls private object? GetValue() { - if (!(_target.Owner is IThemeVariantHost themeVariantHost) - || !_target.Owner.TryFindResource(_key, themeVariantHost.ActualThemeVariant, out var value)) - { - value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue; - } + var theme = _overrideThemeVariant ?? (_target.Owner as IThemeVariantHost)?.ActualThemeVariant; + var value = _target.Owner?.FindResource(theme, _key) ?? AvaloniaProperty.UnsetValue; return _converter?.Invoke(value) ?? value; } diff --git a/src/Avalonia.Base/Styling/IThemeVariantHost.cs b/src/Avalonia.Base/Styling/IThemeVariantHost.cs index 01583148a8..740887970b 100644 --- a/src/Avalonia.Base/Styling/IThemeVariantHost.cs +++ b/src/Avalonia.Base/Styling/IThemeVariantHost.cs @@ -7,7 +7,6 @@ namespace Avalonia.Styling; /// /// Interface for the host element with a theme variant. /// -[Unstable] public interface IThemeVariantHost : IResourceHost { /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 5ca2b09eba..23c67df810 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -58,7 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(), + new AvaloniaXamlIlThemeVariantProviderTransformer() ); InsertBefore( new AvaloniaXamlIlOptionMarkupExtensionTransformer()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs new file mode 100644 index 0000000000..05df8be1b6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs @@ -0,0 +1,31 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + var type = context.GetAvaloniaTypes().IThemeVariantProvider; + if (!(node is XamlAstObjectNode on + && type.IsAssignableFrom(on.Type.GetClrType()))) + return node; + + var keyDirective = on.Children.FirstOrDefault(n => n is XamlAstXmlDirective d + && d.Namespace == XamlNamespaces.Xaml2006 && + d.Name == "Key") as XamlAstXmlDirective; + if (keyDirective is null) + return node; + + var keyProp = type.Properties.First(p => p.Name == "Key"); + on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective, + new XamlAstClrProperty(keyDirective, keyProp, context.Configuration), + keyDirective.Values, true)); + + return node; + } +} 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 63683da0db..8ab84f4615 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -110,6 +110,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } + public IXamlType IThemeVariantProvider { get; } public IXamlType UriKind { get; } public IXamlConstructor UriConstructor { get; } public IXamlType Style { get; } @@ -250,6 +251,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers cfg.TypeSystem.GetType("System.Func`2").MakeGenericType( cfg.TypeSystem.GetType("System.IServiceProvider"), XamlIlTypes.Object)); + IThemeVariantProvider = cfg.TypeSystem.GetType("Avalonia.Controls.IThemeVariantProvider"); UriKind = cfg.TypeSystem.GetType("System.UriKind"); UriConstructor = Uri.GetConstructor(new List() { cfg.WellKnownTypes.String, UriKind }); Style = cfg.TypeSystem.GetType("Avalonia.Styling.Style"); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index e1b594e331..7f52c872ed 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Xaml.Converters; using Avalonia.Media; +using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions { @@ -10,6 +11,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { private object? _anchor; private BindingPriority _priority; + private ThemeVariant? _currentThemeVariant; public DynamicResourceExtension() { @@ -36,6 +38,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions (object?)serviceProvider.GetFirstParent(); } + _currentThemeVariant = StaticResourceExtension.GetDictionaryVariant(serviceProvider); + return this; } @@ -59,7 +63,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions } else if (_anchor is IResourceProvider resourceProvider) { - var source = resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); + var source = resourceProvider.GetResourceObservable(ResourceKey, _currentThemeVariant, GetConverter(targetProperty)); return InstancedBinding.OneWay(source, _priority); } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index 3de669b1e4..c23c31e24c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -33,7 +33,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var provideTarget = serviceProvider.GetService(); var targetObject = provideTarget?.TargetObject; var targetProperty = provideTarget?.TargetProperty; - var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant; + var themeVariant = (targetObject as IThemeVariantHost)?.ActualThemeVariant + ?? GetDictionaryVariant(serviceProvider); var targetType = targetProperty switch { @@ -78,6 +79,25 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { return ColorToBrushConverter.Convert(control.FindResource(ResourceKey!), targetType); } + + internal static ThemeVariant? GetDictionaryVariant(IServiceProvider serviceProvider) + { + var parents = serviceProvider.GetService()?.Parents; + if (parents is null) + { + return null; + } + + foreach (var parent in parents) + { + if (parent is IThemeVariantProvider { Key: { } setKey }) + { + return setKey; + } + } + + return null; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index fbcfdde565..eee02ea0d8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -13,7 +13,7 @@ namespace Avalonia.Markup.Xaml.Styling /// When used in runtime, this type might be unsafe with trimming and AOT. /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] - public class ResourceInclude : IResourceProvider + public class ResourceInclude : IResourceProvider, IThemeVariantProvider { private readonly IServiceProvider? _serviceProvider; private readonly Uri? _baseUri; @@ -65,6 +65,8 @@ namespace Avalonia.Markup.Xaml.Styling /// public Uri? Source { get; set; } + ThemeVariant? IThemeVariantProvider.Key { get; set; } + bool IResourceNode.HasResources => Loaded.HasResources; public event EventHandler? OwnerChanged diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index e9b82d5381..8eadb3a3f0 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -1,7 +1,9 @@ using System; +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Platform; using Avalonia.Styling; +using Avalonia.Threading; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; using Moq; @@ -30,27 +32,23 @@ namespace Avalonia.Benchmarks.Themes _app.Dispose(); } - [Benchmark] - public void RepeatButton() + [Benchmark()] + [MethodImpl(MethodImplOptions.NoInlining)] + public void CreateButton() { - var button = new RepeatButton(); + var button = new Button(); _root.Child = button; _root.LayoutManager.ExecuteLayoutPass(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded); } private static IDisposable CreateApp() { var services = new TestServices( - assetLoader: new AssetLoader(), - globalClock: new MockGlobalClock(), - platform: new AppBuilder().RuntimePlatform, - renderInterface: new MockPlatformRenderInterface(), - standardCursorFactory: Mock.Of(), - theme: () => LoadFluentTheme(), + renderInterface: new NullRenderingPlatform(), dispatcherImpl: new NullThreadingPlatform(), - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl(), - windowingPlatform: new MockWindowingPlatform()); + standardCursorFactory: new NullCursorFactory(), + theme: () => LoadFluentTheme()); return UnitTestApplication.Start(services); } diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 7c0a3f8bdf..ac174e4bc2 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -1,5 +1,5 @@ using System; - +using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; @@ -29,6 +29,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitFluentTheme() { UnitTestApplication.Current.Styles[0] = new FluentTheme(); @@ -36,6 +37,7 @@ namespace Avalonia.Benchmarks.Themes } [Benchmark] + [MethodImpl(MethodImplOptions.NoInlining)] public bool InitSimpleTheme() { UnitTestApplication.Current.Styles[0] = new SimpleTheme(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs index 3ac4677694..2def84bb18 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ThemeDictionariesTests.cs @@ -1,9 +1,12 @@ -using System.Linq; +using System; +using System.Linq; using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; @@ -140,7 +143,7 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } - [Fact(Skip = "Not implemented")] + [Fact] public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key() { var themeVariantScope = (ThemeVariantScope)AvaloniaRuntimeXamlLoader.Load(@" @@ -183,6 +186,135 @@ public class ThemeDictionariesTests : XamlTestBase Assert.Equal(Colors.Black, ((ISolidColorBrush)border.Background)!.Color); } + + [Fact] + public void StaticResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" + + +"), + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + + + White + + + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary = (ResourceDictionary)parsed[1]!; + + dictionary.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); + var colorResource = Assert.IsType(resource); + Assert.Equal(Colors.White, colorResource); + + dictionary.TryGetResource("InnerKey", ThemeVariant.Light, out resource); + colorResource = Assert.IsType(resource); + Assert.Equal(Colors.Green, colorResource); + } + + [Fact] + public void DynamicResource_Inside_Of_ThemeDictionaries_Should_Use_Same_Theme_Key_From_Inner_File() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Inner.xaml"), @" + + +"), + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + + + White + + + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary1 = (ResourceDictionary)parsed[0]!; + var dictionary2 = (ResourceDictionary)parsed[1]!; + var ownerApp = new Application(); // DynamicResource needs an owner to work + ownerApp.RequestedThemeVariant = new ThemeVariant("FakeOne", null); + ownerApp.Resources.MergedDictionaries.Add(dictionary1); + ownerApp.Resources.MergedDictionaries.Add(dictionary2); + + dictionary2.TryGetResource("InnerKey", ThemeVariant.Dark, out var resource); + var colorResource = Assert.IsAssignableFrom(resource); + Assert.Equal(Colors.White, colorResource.Color); + + dictionary2.TryGetResource("InnerKey", ThemeVariant.Light, out resource); + colorResource = Assert.IsAssignableFrom(resource); + Assert.Equal(Colors.Green, colorResource.Color); + } + + [Fact] + public void DynamicResource_Inside_Control_Inside_Of_ThemeDictionaries_Should_Use_Control_Theme_Variant() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(@" + + + + Green + + + + White + + + +") + }; + + var parsed = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var dictionary = (ResourceDictionary)parsed[0]!; + + dictionary.TryGetResource("Template", ThemeVariant.Dark, out var resource); + var control = Assert.IsType((resource as Template)?.Build()); + control.Resources.MergedDictionaries.Add(dictionary); + Assert.Equal(Colors.Green, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); + control.Resources.MergedDictionaries.Remove(dictionary); + + dictionary.TryGetResource("Template", ThemeVariant.Light, out resource); + control = Assert.IsType((resource as Template)?.Build()); + control.Resources.MergedDictionaries.Add(dictionary); + Assert.Equal(Colors.White, ((ISolidColorBrush)control[TextElement.ForegroundProperty]!).Color); + } [Fact] public void StaticResource_Outside_Of_Dictionaries_Should_Use_Control_ThemeVariant() From a09c182e89245160b64677f369216d56711faeb9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 22:37:16 -0400 Subject: [PATCH 49/54] Implement SystemAccentColors backed by the actual system values --- samples/ControlCatalog/App.xaml | 2 - samples/ControlCatalog/MainView.xaml.cs | 48 ------ .../Accents/AccentColors.xaml | 12 -- .../Accents/SystemAccentColors.cs | 163 ++++++++++++++++++ src/Avalonia.Themes.Fluent/FluentTheme.xaml | 2 +- .../XamlMergeResourceGroupTransformer.cs | 12 -- 6 files changed, 164 insertions(+), 75 deletions(-) delete mode 100644 src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml create mode 100644 src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 3b847adcbb..64bf3e53b3 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -26,8 +26,6 @@ #FFFFFFFF - #FF0078D7 - #FF005A9E diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 9c439c874f..9c511f9eb0 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -19,13 +19,9 @@ namespace ControlCatalog { public class MainView : UserControl { - private readonly IPlatformSettings _platformSettings; - public MainView() { AvaloniaXamlLoader.Load(this); - _platformSettings = AvaloniaLocator.Current.GetRequiredService(); - PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); var sideBar = this.Get("Sidebar"); @@ -141,50 +137,6 @@ namespace ControlCatalog ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true; }; } - - _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; - PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues()); - } - - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - base.OnDetachedFromLogicalTree(e); - - _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; - } - - private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) - { - 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); - - static Color ChangeColorLuminosity(Color color, double luminosityFactor) - { - var red = (double)color.R; - var green = (double)color.G; - var blue = (double)color.B; - - if (luminosityFactor < 0) - { - luminosityFactor = 1 + luminosityFactor; - red *= luminosityFactor; - green *= luminosityFactor; - blue *= luminosityFactor; - } - else if (luminosityFactor >= 0) - { - red = (255 - red) * luminosityFactor + red; - green = (255 - green) * luminosityFactor + green; - blue = (255 - blue) * luminosityFactor + blue; - } - - return new Color(color.A, (byte)red, (byte)green, (byte)blue); - } } } } diff --git a/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml b/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml deleted file mode 100644 index 0fb3ab73c2..0000000000 --- a/src/Avalonia.Themes.Fluent/Accents/AccentColors.xaml +++ /dev/null @@ -1,12 +0,0 @@ - - - - #FF0078D7 - #FF005A9E - #FF004275 - #FF002642 - #FF429CE3 - #FF76B9ED - #FFA6D8FF - diff --git a/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs new file mode 100644 index 0000000000..a4ef15f950 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/SystemAccentColors.cs @@ -0,0 +1,163 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent.Accents; + +internal class SystemAccentColors : IResourceProvider +{ + public const string AccentKey = "SystemAccentColor"; + public const string AccentDark1Key = "SystemAccentColorDark1"; + public const string AccentDark2Key = "SystemAccentColorDark2"; + public const string AccentDark3Key = "SystemAccentColorDark3"; + public const string AccentLight1Key = "SystemAccentColorLight1"; + public const string AccentLight2Key = "SystemAccentColorLight2"; + public const string AccentLight3Key = "SystemAccentColorLight3"; + + private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215); + private readonly IPlatformSettings? _platformSettings; + private bool _invalidateColors = true; + private Color _systemAccentColor; + private Color _systemAccentColorDark1, _systemAccentColorDark2, _systemAccentColorDark3; + private Color _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3; + + public SystemAccentColors() + { + _platformSettings = AvaloniaLocator.Current.GetService(); + } + + public bool HasResources => true; + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + if (key is string strKey) + { + if (strKey.Equals(AccentKey, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColor; + return true; + } + + if (strKey.Equals(AccentDark1Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark1; + return true; + } + + if (strKey.Equals(AccentDark2Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark2; + return true; + } + + if (strKey.Equals(AccentDark3Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorDark3; + return true; + } + + if (strKey.Equals(AccentLight1Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight1; + return true; + } + + if (strKey.Equals(AccentLight2Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight2; + return true; + } + + if (strKey.Equals(AccentLight3Key, StringComparison.InvariantCulture)) + { + EnsureColors(); + value = _systemAccentColorLight3; + return true; + } + } + + value = null; + return false; + } + + public IResourceHost? Owner { get; private set; } + public event EventHandler? OwnerChanged; + public void AddOwner(IResourceHost owner) + { + if (Owner != owner) + { + Owner = owner; + OwnerChanged?.Invoke(this, EventArgs.Empty); + + if (_platformSettings is not null) + { + _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; + } + } + } + + public void RemoveOwner(IResourceHost owner) + { + if (Owner == owner) + { + Owner = null; + OwnerChanged?.Invoke(this, EventArgs.Empty); + + if (_platformSettings is not null) + { + _platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; + } + } + } + + private void EnsureColors() + { + if (_invalidateColors) + { + _invalidateColors = false; + + _systemAccentColor = _platformSettings?.GetColorValues().AccentColor1 ?? s_defaultSystemAccentColor; + (_systemAccentColorDark1,_systemAccentColorDark2, _systemAccentColorDark3, + _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3) = CalculateAccentShades(_systemAccentColor); + } + } + + public static (Color d1, Color d2, Color d3, Color l1, Color l2, Color l3) CalculateAccentShades(Color accentColor) + { + // dark1step = (hslAccent.L - SystemAccentColorDark1.L) * 255 + const double dark1step = 28.5 / 255d; + const double dark2step = 49 / 255d; + const double dark3step = 74.5 / 255d; + // light1step = (SystemAccentColorLight1.L - hslAccent.L) * 255 + const double light1step = 39 / 255d; + const double light2step = 70 / 255d; + const double light3step = 103 / 255d; + + var hslAccent = accentColor.ToHsl(); + + return ( + // Darker shades + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark1step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark2step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark3step).ToRgb(), + + // Lighter shades + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light1step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light2step).ToRgb(), + new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light3step).ToRgb() + ); + } + + private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) + { + _invalidateColors = true; + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } +} diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index e83257fd9f..9154505c28 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -4,9 +4,9 @@ - + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index db8d604154..7d68979c26 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -24,7 +24,6 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; var mergeSourceNodes = new List(); - var hasAnyNonMergedResource = false; foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) { void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) @@ -47,17 +46,6 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer valueNode); } } - else - { - hasAnyNonMergedResource = true; - } - - if (hasAnyNonMergedResource && mergeSourceNodes.Any()) - { - throw new XamlDocumentParseException(context.CurrentDocument, - "Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", - valueNode); - } } } From c9b1ed8f51f41dea62021f7b4b689ec21f149e37 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 22:37:27 -0400 Subject: [PATCH 50/54] Implement ColorPaletteResources as a public API --- .../AvaloniaDictionaryExtensions.cs | 2 +- .../Accents/BaseColorsPalette.xaml | 69 ++++++++ .../Accents/{Base.xaml => BaseResources.xaml} | 154 +++--------------- .../ColorPaletteResources.Properties.cs | 153 +++++++++++++++++ .../ColorPaletteResources.cs | 118 ++++++++++++++ .../ColorPaletteResourcesCollection.cs | 65 ++++++++ src/Avalonia.Themes.Fluent/FluentTheme.xaml | 12 +- .../FluentTheme.xaml.cs | 6 + 8 files changed, 447 insertions(+), 132 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml rename src/Avalonia.Themes.Fluent/Accents/{Base.xaml => BaseResources.xaml} (79%) create mode 100644 src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs create mode 100644 src/Avalonia.Themes.Fluent/ColorPaletteResources.cs create mode 100644 src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs index e350a019d4..8c731c188f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs @@ -35,7 +35,7 @@ namespace Avalonia.Collections /// Indicates if a weak subscription should be used to track changes to the collection. /// /// A disposable used to terminate the subscription. - internal static IDisposable ForEachItem( + public static IDisposable ForEachItem( this IAvaloniaReadOnlyDictionary collection, Action added, Action removed, diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml new file mode 100644 index 0000000000..b8b5fcf1f4 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml @@ -0,0 +1,69 @@ + + + + + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FF171717 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FFCCCCCC + #FF7A7A7A + #FFCCCCCC + #FFF2F2F2 + #FFE6E6E6 + #FFF2F2F2 + #FFFFFFFF + #FF767676 + #19000000 + #33000000 + #C50500 + #FFFFFFFF + #17000000 + #2E000000 + + + #FF000000 + #33000000 + #99000000 + #CC000000 + #66000000 + #FFFFFFFF + #33FFFFFF + #99FFFFFF + #CCFFFFFF + #66FFFFFF + #FFF2F2F2 + #FF000000 + #33000000 + #66000000 + #CC000000 + #FF333333 + #FF858585 + #FF767676 + #FF171717 + #FF1F1F1F + #FF2B2B2B + #FFFFFFFF + #FF767676 + #19FFFFFF + #33FFFFFF + #FFF000 + #FF000000 + #18FFFFFF + #30FFFFFF + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml similarity index 79% rename from src/Avalonia.Themes.Fluent/Accents/Base.xaml rename to src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml index c19a4f5c09..c1f79e45d5 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="using:System" xmlns:converters="using:Avalonia.Controls.Converters"> - fonts:Inter#Inter, $Default 14 @@ -28,39 +27,33 @@ - - - - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FF171717 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FFCCCCCC - #FF7A7A7A - #FFCCCCCC - #FFF2F2F2 - #FFE6E6E6 - #FFF2F2F2 - #FFFFFFFF - #FF767676 - #19000000 - #33000000 - #C50500 + 374 + 0,2,0,2 + 1 + -1,0,-1,0 + 32 + 64 + 456 + 0 + 1 + 0 + + 12,11,12,12 + 96 + 40 + 758 - #17000000 - #2E000000 + + 0 + + 0,4,0,4 + + + 12,0,12,0 + + + - - - - - - - - #FFFFFFFF - - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 - - #FF000000 - #33000000 - #99000000 - #CC000000 - #66000000 - #FFFFFFFF - #33FFFFFF - #99FFFFFF - #CCFFFFFF - #66FFFFFF - #FFF2F2F2 - #FF000000 - #33000000 - #66000000 - #CC000000 - #FF333333 - #FF858585 - #FF767676 - #FF171717 - #FF1F1F1F - #FF2B2B2B - #FFFFFFFF - #FF767676 - #19FFFFFF - #33FFFFFF - #FFF000 - - #18FFFFFF - #30FFFFFF - - - - - - - - #FF000000 - - 374 - 0,2,0,2 - 1 - -1,0,-1,0 - 32 - 64 - 456 - 0 - 1 - 0 - - 12,11,12,12 - 96 - 40 - 758 - - - 0 - - - 0,4,0,4 - - - 12,0,12,0 diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs new file mode 100644 index 0000000000..6d284150e4 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs @@ -0,0 +1,153 @@ +using Avalonia.Media; + +namespace Avalonia.Themes.Fluent; + +public partial class ColorPaletteResources +{ + private bool _hasAccentColor; + private Color _accentColor; + private Color _accentColorDark1, _accentColorDark2, _accentColorDark3; + private Color _accentColorLight1, _accentColorLight2, _accentColorLight3; + + public static readonly DirectProperty AccentProperty + = AvaloniaProperty.RegisterDirect(nameof(Accent), r => r.Accent, (r, v) => r.Accent = v); + + /// + /// Gets or sets the Accent color value. + /// + public Color Accent + { + get => _accentColor; + set => SetAndRaise(AccentProperty, ref _accentColor, value); + } + + /// + /// Gets or sets the AltHigh color value. + /// + public Color AltHigh { get => GetColor("SystemAltHighColor"); set => SetColor("SystemAltHighColor", value); } + + /// + /// Gets or sets the AltLow color value. + /// + public Color AltLow { get => GetColor("SystemAltLowColor"); set => SetColor("SystemAltLowColor", value); } + + /// + /// Gets or sets the AltMedium color value. + /// + public Color AltMedium { get => GetColor("SystemAltMediumColor"); set => SetColor("SystemAltMediumColor", value); } + + /// + /// Gets or sets the AltMediumHigh color value. + /// + public Color AltMediumHigh { get => GetColor("SystemAltMediumHighColor"); set => SetColor("SystemAltMediumHighColor", value); } + + /// + /// Gets or sets the AltMediumLow color value. + /// + public Color AltMediumLow { get => GetColor("SystemAltMediumLowColor"); set => SetColor("SystemAltMediumLowColor", value); } + + /// + /// Gets or sets the BaseHigh color value. + /// + public Color BaseHigh { get => GetColor("SystemBaseHighColor"); set => SetColor("SystemBaseHighColor", value); } + + /// + /// Gets or sets the BaseLow color value. + /// + public Color BaseLow { get => GetColor("SystemBaseLowColor"); set => SetColor("SystemBaseLowColor", value); } + + /// + /// Gets or sets the BaseMedium color value. + /// + public Color BaseMedium { get => GetColor("SystemBaseMediumColor"); set => SetColor("SystemBaseMediumColor", value); } + + /// + /// Gets or sets the BaseMediumHigh color value. + /// + public Color BaseMediumHigh { get => GetColor("SystemBaseMediumHighColor"); set => SetColor("SystemBaseMediumHighColor", value); } + + /// + /// Gets or sets the BaseMediumLow color value. + /// + public Color BaseMediumLow { get => GetColor("SystemBaseMediumLowColor"); set => SetColor("SystemBaseMediumLowColor", value); } + + /// + /// Gets or sets the ChromeAltLow color value. + /// + public Color ChromeAltLow { get => GetColor("SystemChromeAltLowColor"); set => SetColor("SystemChromeAltLowColor", value); } + + /// + /// Gets or sets the ChromeBlackHigh color value. + /// + public Color ChromeBlackHigh { get => GetColor("SystemChromeBlackHighColor"); set => SetColor("SystemChromeBlackHighColor", value); } + + /// + /// Gets or sets the ChromeBlackLow color value. + /// + public Color ChromeBlackLow { get => GetColor("SystemChromeBlackLowColor"); set => SetColor("SystemChromeBlackLowColor", value); } + + /// + /// Gets or sets the ChromeBlackMedium color value. + /// + public Color ChromeBlackMedium { get => GetColor("SystemChromeBlackMediumColor"); set => SetColor("SystemChromeBlackMediumColor", value); } + + /// + /// Gets or sets the ChromeBlackMediumLow color value. + /// + public Color ChromeBlackMediumLow { get => GetColor("SystemChromeBlackMediumLowColor"); set => SetColor("SystemChromeBlackMediumLowColor", value); } + + /// + /// Gets or sets the ChromeDisabledHigh color value. + /// + public Color ChromeDisabledHigh { get => GetColor("SystemChromeDisabledHighColor"); set => SetColor("SystemChromeDisabledHighColor", value); } + + /// + /// Gets or sets the ChromeDisabledLow color value. + /// + public Color ChromeDisabledLow { get => GetColor("SystemChromeDisabledLowColor"); set => SetColor("SystemChromeDisabledLowColor", value); } + + /// + /// Gets or sets the ChromeGray color value. + /// + public Color ChromeGray { get => GetColor("SystemChromeGrayColor"); set => SetColor("SystemChromeGrayColor", value); } + + /// + /// Gets or sets the ChromeHigh color value. + /// + public Color ChromeHigh { get => GetColor("SystemChromeHighColor"); set => SetColor("SystemChromeHighColor", value); } + + /// + /// Gets or sets the ChromeLow color value. + /// + public Color ChromeLow { get => GetColor("SystemChromeLowColor"); set => SetColor("SystemChromeLowColor", value); } + + /// + /// Gets or sets the ChromeMedium color value. + /// + public Color ChromeMedium { get => GetColor("SystemChromeMediumColor"); set => SetColor("SystemChromeMediumColor", value); } + + /// + /// Gets or sets the ChromeMediumLow color value. + /// + public Color ChromeMediumLow { get => GetColor("SystemChromeMediumLowColor"); set => SetColor("SystemChromeMediumLowColor", value); } + + /// + /// Gets or sets the ChromeWhite color value. + /// + public Color ChromeWhite { get => GetColor("SystemChromeWhiteColor"); set => SetColor("SystemChromeWhiteColor", value); } + + /// + /// Gets or sets the ErrorText color value. + /// + public Color ErrorText { get => GetColor("SystemErrorTextColor"); set => SetColor("SystemErrorTextColor", value); } + + /// + /// Gets or sets the ListLow color value. + /// + public Color ListLow { get => GetColor("SystemListLowColor"); set => SetColor("SystemListLowColor", value); } + + /// + /// Gets or sets the ListMedium color value. + /// + public Color ListMedium { get => GetColor("SystemListMediumColor"); set => SetColor("SystemListMediumColor", value); } +} diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs new file mode 100644 index 0000000000..ce52f51752 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.Themes.Fluent.Accents; + +namespace Avalonia.Themes.Fluent; + +/// +/// Represents a specialized resource dictionary that contains color resources used by FluentTheme elements. +/// +/// +/// This class can only be used in . +/// +public partial class ColorPaletteResources : AvaloniaObject, IResourceNode +{ + private readonly Dictionary _colors = new(StringComparer.InvariantCulture); + + public bool HasResources => _hasAccentColor || _colors.Count > 0; + + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + if (key is string strKey) + { + if (strKey.Equals(SystemAccentColors.AccentKey, StringComparison.InvariantCulture)) + { + value = _accentColor; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark1Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark1; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark2Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark2; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentDark3Key, StringComparison.InvariantCulture)) + { + value = _accentColorDark3; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight1Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight1; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight2Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight2; + return _hasAccentColor; + } + + if (strKey.Equals(SystemAccentColors.AccentLight3Key, StringComparison.InvariantCulture)) + { + value = _accentColorLight3; + return _hasAccentColor; + } + + if (_colors.TryGetValue(strKey, out var color)) + { + value = color; + return true; + } + } + + value = null; + return false; + } + + private Color GetColor(string key) + { + if (_colors.TryGetValue(key, out var color)) + { + return color; + } + + return default; + } + + private void SetColor(string key, Color value) + { + if (value == default) + { + _colors.Remove(key); + } + else + { + _colors[key] = value; + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == AccentProperty) + { + _hasAccentColor = _accentColor != default; + + if (_hasAccentColor) + { + (_accentColorDark1, _accentColorDark2, _accentColorDark3, + _accentColorLight1, _accentColorLight2, _accentColorLight3) = + SystemAccentColors.CalculateAccentShades(_accentColor); + } + } + } +} diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs new file mode 100644 index 0000000000..261de5497d --- /dev/null +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResourcesCollection.cs @@ -0,0 +1,65 @@ +using System; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent; + +internal class ColorPaletteResourcesCollection : AvaloniaDictionary, IResourceProvider +{ + public ColorPaletteResourcesCollection() : base(2) + { + this.ForEachItem( + (_, x) => + { + if (Owner is not null) + { + x.PropertyChanged += Palette_PropertyChanged; + } + }, + (_, x) => + { + if (Owner is not null) + { + x.PropertyChanged -= Palette_PropertyChanged; + } + }, + () => throw new NotSupportedException("Dictionary reset not supported")); + } + + public bool HasResources => Count > 0; + public bool TryGetResource(object key, ThemeVariant? theme, out object? value) + { + theme ??= ThemeVariant.Default; + if (base.TryGetValue(theme, out var paletteResources) + && paletteResources.TryGetResource(key, theme, out value)) + { + return true; + } + + value = null; + return false; + } + + public IResourceHost? Owner { get; private set; } + public event EventHandler? OwnerChanged; + public void AddOwner(IResourceHost owner) + { + Owner = owner; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + + public void RemoveOwner(IResourceHost owner) + { + Owner = null; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + + private void Palette_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ColorPaletteResources.AccentProperty) + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 9154505c28..8f3c96d96a 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -1,11 +1,19 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:fluent="using:Avalonia.Themes.Fluent" + xmlns:accents="clr-namespace:Avalonia.Themes.Fluent.Accents"> - + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index 95539bc08a..5af22dbd1d 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Styling; @@ -31,6 +32,9 @@ namespace Avalonia.Themes.Fluent EnsureCompactStyles(); + Palettes = Resources.MergedDictionaries.OfType().FirstOrDefault() + ?? throw new InvalidOperationException("FluentTheme was initialized with missing ColorPaletteResourcesCollection."); + object GetAndRemove(string key) { var val = Resources[key] @@ -52,6 +56,8 @@ namespace Avalonia.Themes.Fluent set => SetValue(DensityStyleProperty, value); } + public IDictionary Palettes { get; } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); From 1aba81a27f1f568d41c948676ac3580c58cb6a5b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 22 Apr 2023 22:41:45 -0400 Subject: [PATCH 51/54] Fix non dynamic accent brushes --- .../Accents/FluentControlResources.xaml | 114 +++++++++++------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml index a9bc622221..61a74f26a4 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResources.xaml @@ -4,8 +4,8 @@ - - + + @@ -52,7 +52,8 @@ - + @@ -291,15 +292,17 @@ ResourceKey="SystemControlHighlightBaseHighBrush" /> - + - - + + @@ -309,13 +312,17 @@ ResourceKey="SystemControlBackgroundBaseMediumLowBrush" /> - - + + - - + + - - + + - - + + @@ -470,8 +481,8 @@ - - + + @@ -502,8 +513,8 @@ - - + + @@ -701,8 +712,9 @@ - - + + @@ -775,8 +787,8 @@ - - + + @@ -823,7 +835,8 @@ - + @@ -1065,14 +1078,17 @@ ResourceKey="SystemControlHighlightBaseHighBrush" /> - - + + - - + + @@ -1082,13 +1098,17 @@ ResourceKey="SystemControlBackgroundBaseMediumLowBrush" /> - - + + - - + + - - + + - - + + @@ -1243,8 +1267,8 @@ - - + + @@ -1275,12 +1299,12 @@ - - + + - - + + @@ -1476,8 +1500,8 @@ - - + + From bbbc1280ff4f671073ebd033133cea9f62891579 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 23 Apr 2023 00:44:48 -0400 Subject: [PATCH 52/54] Rename RegionBrush to SystemRegionColor and use it in default templates --- src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml | 4 ++-- src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml | 4 ++-- .../ColorPaletteResources.Properties.cs | 5 +++++ .../Controls/EmbeddableControlRoot.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/Window.xaml | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml index b8b5fcf1f4..362d543646 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseColorsPalette.xaml @@ -29,7 +29,7 @@ #19000000 #33000000 #C50500 - #FFFFFFFF + #FFFFFFFF #17000000 #2E000000 @@ -60,7 +60,7 @@ #19FFFFFF #33FFFFFF #FFF000 - #FF000000 + #FF000000 #18FFFFFF #30FFFFFF diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml index c1f79e45d5..517a80fd7e 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseResources.xaml @@ -212,7 +212,7 @@ - + @@ -372,7 +372,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs index 6d284150e4..366af8e227 100644 --- a/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs +++ b/src/Avalonia.Themes.Fluent/ColorPaletteResources.Properties.cs @@ -150,4 +150,9 @@ public partial class ColorPaletteResources /// Gets or sets the ListMedium color value. /// public Color ListMedium { get => GetColor("SystemListMediumColor"); set => SetColor("SystemListMediumColor", value); } + + /// + /// Gets or sets the RegionColor color value. + /// + public Color RegionColor { get => GetColor("SystemRegionColor"); set => SetColor("SystemRegionColor", value); } } diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index f60424a2dc..ee51ef8085 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + diff --git a/src/Avalonia.Themes.Fluent/Controls/Window.xaml b/src/Avalonia.Themes.Fluent/Controls/Window.xaml index ff27cce800..8db01fa4c8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Window.xaml @@ -1,7 +1,7 @@ - + From 753b821035769f054c5e03717a57a7c69abc856f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 23 Apr 2023 00:45:03 -0400 Subject: [PATCH 53/54] Fix merged dictionaries ordering --- src/Avalonia.Themes.Fluent/FluentTheme.xaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 8f3c96d96a..0528c40c21 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -6,15 +6,15 @@ - - - - - - + + + + + + From b8ecad2cbcfef7141d46dc6be6219ba960ca5e5b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 24 Apr 2023 02:42:46 -0400 Subject: [PATCH 54/54] Restore resource include validation --- .../XamlMergeResourceGroupTransformer.cs | 10 ++++++- .../Xaml/MergeResourceIncludeTests.cs | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs index 7d68979c26..8e04a7d467 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -24,6 +24,7 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; var mergeSourceNodes = new List(); + var mergedResourceWasAdded = false; foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) { void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) @@ -37,7 +38,8 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer && objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) { parent.Children.Remove(assignmentNode); - mergeSourceNodes.Add(sourceAssignmentNode); + mergeSourceNodes.Add(sourceAssignmentNode); + mergedResourceWasAdded = true; } else { @@ -46,6 +48,12 @@ internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer valueNode); } } + else if (mergeSourceNodes.Any()) + { + throw new XamlDocumentParseException(context.CurrentDocument, + "MergeResourceInclude should always be included last when mixing with other dictionaries inside of the ResourceDictionary.MergedDictionaries.", + valueNode); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs index aa76756069..d6f554cdfe 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -83,6 +83,34 @@ public class MergeResourceIncludeTests Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); } + [Fact] + public void MergeResourceInclude_Is_Allowed_After_ResourceInclude() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Blue +"), + new RuntimeXamlLoaderDocument(@" + + + + + +") + }; + + AvaloniaRuntimeXamlLoader.LoadGroup(documents); + } + [Fact] public void MergeResourceInclude_Works_With_Multiple_Resources() {