From 4d34a2c6e76fc415d2c62b22142aa937305cdf0c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:26:43 -0500 Subject: [PATCH 001/311] 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 002/311] 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 003/311] 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 004/311] 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 005/311] 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 006/311] 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 007/311] 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 008/311] 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 009/311] 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 010/311] 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 011/311] 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 7f9cbb6aed7259f5471060ab46ee178dd23e7bc5 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 28 Feb 2023 15:34:37 +0100 Subject: [PATCH 012/311] fix(DevTools): Debug Application without ApplicationLifetime --- Avalonia.Desktop.slnf | 1 + Avalonia.sln | 17 ++-- samples/AppWithoutLifetime/App.axaml | 7 ++ samples/AppWithoutLifetime/App.axaml.cs | 12 +++ .../AppWithoutLifetime.csproj | 21 +++++ samples/AppWithoutLifetime/MainWindow.axaml | 13 ++++ .../AppWithoutLifetime/MainWindow.axaml.cs | 30 +++++++ samples/AppWithoutLifetime/Program.cs | 28 +++++++ samples/AppWithoutLifetime/Sub.axaml | 9 +++ samples/AppWithoutLifetime/Sub.axaml.cs | 24 ++++++ samples/AppWithoutLifetime/app.manifest | 18 +++++ .../Avalonia.Controls.csproj | 1 + .../DesktopApplicationExtensions.cs | 1 - .../Diagnostics/DevTools.cs | 78 +++++++++---------- 14 files changed, 214 insertions(+), 46 deletions(-) create mode 100644 samples/AppWithoutLifetime/App.axaml create mode 100644 samples/AppWithoutLifetime/App.axaml.cs create mode 100644 samples/AppWithoutLifetime/AppWithoutLifetime.csproj create mode 100644 samples/AppWithoutLifetime/MainWindow.axaml create mode 100644 samples/AppWithoutLifetime/MainWindow.axaml.cs create mode 100644 samples/AppWithoutLifetime/Program.cs create mode 100644 samples/AppWithoutLifetime/Sub.axaml create mode 100644 samples/AppWithoutLifetime/Sub.axaml.cs create mode 100644 samples/AppWithoutLifetime/app.manifest diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index d4cde99240..3bdd00746b 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -3,6 +3,7 @@ "path": "Avalonia.sln", "projects": [ "packages\\Avalonia\\Avalonia.csproj", + "samples\\AppWithoutLifetime\\AppWithoutLifetime.csproj", "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj", "samples\\ControlCatalog\\ControlCatalog.csproj", "samples\\GpuInterop\\GpuInterop.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index b21df07628..7dad7a31d9 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -244,13 +244,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppWithoutLifetime", "samples\AppWithoutLifetime\AppWithoutLifetime.csproj", "{F8928267-688E-4A51-989C-612A72446D33}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -595,6 +597,10 @@ Global {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU {A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -661,10 +667,11 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {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} {DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/AppWithoutLifetime/App.axaml b/samples/AppWithoutLifetime/App.axaml new file mode 100644 index 0000000000..5f86b8be93 --- /dev/null +++ b/samples/AppWithoutLifetime/App.axaml @@ -0,0 +1,7 @@ + + + + + diff --git a/samples/AppWithoutLifetime/App.axaml.cs b/samples/AppWithoutLifetime/App.axaml.cs new file mode 100644 index 0000000000..9cc99929c4 --- /dev/null +++ b/samples/AppWithoutLifetime/App.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia; +using Avalonia.Markup.Xaml; + +namespace AppWithoutLifetime; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } +} diff --git a/samples/AppWithoutLifetime/AppWithoutLifetime.csproj b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj new file mode 100644 index 0000000000..fce12af298 --- /dev/null +++ b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj @@ -0,0 +1,21 @@ + + + WinExe + net6.0 + enable + app.manifest + + + + + + + + + + + + + + + diff --git a/samples/AppWithoutLifetime/MainWindow.axaml b/samples/AppWithoutLifetime/MainWindow.axaml new file mode 100644 index 0000000000..3f31cb7fae --- /dev/null +++ b/samples/AppWithoutLifetime/MainWindow.axaml @@ -0,0 +1,13 @@ + + + + + //TODO Binding [AssignBinding] - [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] + [InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))] public virtual IBinding Binding { get diff --git a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs index ae52e5f970..ee9cc04420 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs @@ -122,9 +122,9 @@ namespace Avalonia.Controls // We need to use the raw ItemsSource as opposed to DataSource because DataSource // may be the ItemsSource wrapped in a collection view, in which case we wouldn't // be able to take T to be the type if we're given IEnumerable - if (_dataType == null && _owner.Items != null) + if (_dataType == null && _owner.ItemsSource != null) { - _dataType = _owner.Items.GetItemType(); + _dataType = _owner.ItemsSource.GetItemType(); } return _dataType; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 00318e2dd8..0bfb4b6913 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls (o, v) => o.CellTemplate = v); [Content] - [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] + [InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))] public IDataTemplate CellTemplate { get { return _cellTemplate; } @@ -51,7 +51,7 @@ namespace Avalonia.Controls /// /// If this property is the column is read-only. /// - [InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] + [InheritDataTypeFromItems(nameof(DataGrid.ItemsSource), AncestorType = typeof(DataGrid))] public IDataTemplate CellEditingTemplate { get => _cellEditingCellTemplate; diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index c2e63da31c..63b002d110 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -51,7 +51,7 @@ Date: Tue, 28 Mar 2023 22:16:29 +0200 Subject: [PATCH 014/311] AutoCompleteBox.Items -> ItemsSource. --- .../Pages/AutoCompleteBoxPage.xaml.cs | 4 +-- samples/ControlCatalog/Pages/DialogsPage.xaml | 4 +-- .../AutoCompleteBox.Properties.cs | 36 +++++++++---------- .../AutoCompleteBox/AutoCompleteBox.cs | 26 +++++++------- .../Utils/ISelectionAdapter.cs | 2 +- .../SelectingItemsControlSelectionAdapter.cs | 2 +- .../AutoCompleteBoxTests.cs | 10 +++--- 7 files changed, 42 insertions(+), 42 deletions(-) diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs index ae7e43f511..81d379ce05 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml.cs @@ -114,7 +114,7 @@ namespace ControlCatalog.Pages foreach (AutoCompleteBox box in GetAllAutoCompleteBox().Where(x => x.Name != "CustomAutocompleteBox")) { - box.Items = States; + box.ItemsSource = States; } var converter = new FuncMultiValueConverter(parts => @@ -132,7 +132,7 @@ namespace ControlCatalog.Pages asyncBox.AsyncPopulator = PopulateAsync; var customAutocompleteBox = this.Get("CustomAutocompleteBox"); - customAutocompleteBox.Items = Sentences.SelectMany(x => x); + customAutocompleteBox.ItemsSource = Sentences.SelectMany(x => x); customAutocompleteBox.TextFilter = LastWordContains; customAutocompleteBox.TextSelector = AppendWord; } diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index 90c717e7ed..980b210aaa 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -45,7 +45,7 @@ - + Desktop Documents @@ -54,7 +54,7 @@ Videos Music - + - /// Identifies the property. + /// Identifies the property. /// - /// The identifier for the property. - public static readonly StyledProperty ItemsProperty = + /// The identifier for the property. + public static readonly StyledProperty ItemsSourceProperty = AvaloniaProperty.Register( - nameof(Items)); + nameof(ItemsSource)); /// /// Identifies the property. @@ -311,10 +311,10 @@ namespace Avalonia.Controls /// /// Gets the text that is used to filter items in the - /// item collection. + /// item collection. /// /// The text that is used to filter items in the - /// item collection. + /// item collection. /// /// The SearchText value is typically the same as the /// Text property, but is set after the TextChanged event occurs @@ -339,7 +339,7 @@ namespace Avalonia.Controls /// /// Gets or sets how the text in the text box is used to filter items - /// specified by the + /// specified by the /// property for display in the drop-down. /// /// One of the @@ -366,11 +366,11 @@ namespace Avalonia.Controls /// /// Gets or sets the custom method that uses user-entered text to filter - /// the items specified by the + /// the items specified by the /// property for display in the drop-down. /// /// The custom method that uses the user-entered text to filter - /// the items specified by the + /// the items specified by the /// property. The default is null. /// /// The filter mode is automatically set to Custom if you set the @@ -384,11 +384,11 @@ namespace Avalonia.Controls /// /// Gets or sets the custom method that uses the user-entered text to - /// filter items specified by the + /// filter items specified by the /// property in a text-based way for display in the drop-down. /// /// The custom method that uses the user-entered text to filter - /// items specified by the + /// items specified by the /// property in a text-based way for display in the drop-down. /// /// The search mode is automatically set to Custom if you set the @@ -402,11 +402,11 @@ namespace Avalonia.Controls /// /// Gets or sets the custom method that combines the user-entered - /// text and one of the items specified by the . + /// text and one of the items specified by the . /// /// /// The custom method that combines the user-entered - /// text and one of the items specified by the . + /// text and one of the items specified by the . /// public AutoCompleteSelector? ItemSelector { @@ -417,11 +417,11 @@ namespace Avalonia.Controls /// /// Gets or sets the custom method that combines the user-entered /// text and one of the items specified by the - /// in a text-based way. + /// in a text-based way. /// /// /// The custom method that combines the user-entered - /// text and one of the items specified by the + /// text and one of the items specified by the /// in a text-based way. /// public AutoCompleteSelector? TextSelector @@ -442,10 +442,10 @@ namespace Avalonia.Controls /// /// The collection that is used to generate the items of the /// drop-down portion of the control. - public IEnumerable? Items + public IEnumerable? ItemsSource { - get => GetValue(ItemsProperty); - set => SetValue(ItemsProperty, value); + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); } } } diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index 72a23144cf..e10cc1d100 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -419,9 +419,9 @@ namespace Avalonia.Controls /// ItemsSourceProperty property changed handler. /// /// Event arguments. - private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) + private void OnItemsSourcePropertyChanged(AvaloniaPropertyChangedEventArgs e) { - OnItemsChanged((IEnumerable?)e.NewValue); + OnItemsSourceChanged((IEnumerable?)e.NewValue); } private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e) @@ -461,7 +461,7 @@ namespace Avalonia.Controls SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); - ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); + ItemsSourceProperty.Changed.AddClassHandler((x,e) => x.OnItemsSourcePropertyChanged(e)); ItemTemplateProperty.Changed.AddClassHandler((x,e) => x.OnItemTemplatePropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } @@ -559,7 +559,7 @@ namespace Avalonia.Controls _adapter.Commit -= OnAdapterSelectionComplete; _adapter.Cancel -= OnAdapterSelectionCanceled; _adapter.Cancel -= OnAdapterSelectionComplete; - _adapter.Items = null; + _adapter.ItemsSource = null; } _adapter = value; @@ -570,7 +570,7 @@ namespace Avalonia.Controls _adapter.Commit += OnAdapterSelectionComplete; _adapter.Cancel += OnAdapterSelectionCanceled; _adapter.Cancel += OnAdapterSelectionComplete; - _adapter.Items = _view; + _adapter.ItemsSource = _view; } } } @@ -1128,7 +1128,7 @@ namespace Avalonia.Controls { if (!cancellationToken.IsCancellationRequested) { - SetCurrentValue(ItemsProperty, resultList); + SetCurrentValue(ItemsSourceProperty, resultList); PopulateComplete(); } }); @@ -1475,7 +1475,7 @@ namespace Avalonia.Controls /// adapter's ItemsSource to the view if appropriate. /// /// The new enumerable reference. - private void OnItemsChanged(IEnumerable? newValue) + private void OnItemsSourceChanged(IEnumerable? newValue) { // Remove handler for oldValue.CollectionChanged (if present) _collectionChangeSubscription?.Dispose(); @@ -1492,9 +1492,9 @@ namespace Avalonia.Controls // Clear and set the view on the selection adapter ClearView(); - if (SelectionAdapter != null && SelectionAdapter.Items != _view) + if (SelectionAdapter != null && SelectionAdapter.ItemsSource != _view) { - SelectionAdapter.Items = _view; + SelectionAdapter.ItemsSource = _view; } if (IsDropDownOpen) { @@ -1545,9 +1545,9 @@ namespace Avalonia.Controls { // Significant changes to the underlying data. ClearView(); - if (Items != null) + if (ItemsSource != null) { - _items = new List(Items.Cast()); + _items = new List(ItemsSource.Cast()); } } @@ -1582,9 +1582,9 @@ namespace Avalonia.Controls PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view!)); OnPopulated(populated); - if (SelectionAdapter != null && SelectionAdapter.Items != _view) + if (SelectionAdapter != null && SelectionAdapter.ItemsSource != _view) { - SelectionAdapter.Items = _view; + SelectionAdapter.ItemsSource = _view; } bool isDropDownOpen = _userCalledPopulate && (_view!.Count > 0); diff --git a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs index 3ede518ffa..55739500a1 100644 --- a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs @@ -36,7 +36,7 @@ namespace Avalonia.Controls.Utils /// /// The collection that is used to generate content for the /// selection adapter. - IEnumerable? Items { get; set; } + IEnumerable? ItemsSource { get; set; } /// /// Occurs when a selected item is not cancelled and is committed as the diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 5f528e2c72..ca8827eb45 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -140,7 +140,7 @@ namespace Avalonia.Controls.Utils /// /// The collection used to generate content for the selection /// adapter. - public IEnumerable? Items + public IEnumerable? ItemsSource { get { diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 4582659763..acae583b5c 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -89,7 +89,7 @@ namespace Avalonia.Controls.UnitTests bool closeEvent = false; control.DropDownOpened += (s, e) => openEvent = true; control.DropDownClosed += (s, e) => closeEvent = true; - control.Items = CreateSimpleStringArray(); + control.ItemsSource = CreateSimpleStringArray(); textbox.Text = "a"; Dispatcher.UIThread.RunJobs(); @@ -258,7 +258,7 @@ namespace Avalonia.Controls.UnitTests control.FilterMode = AutoCompleteFilterMode.None; control.Populating += (s, e) => { - control.Items = new string[] { custom }; + control.ItemsSource = new string[] { custom }; Assert.Equal(search, e.Parameter); }; control.Populated += (s, e) => @@ -380,7 +380,7 @@ namespace Avalonia.Controls.UnitTests { RunTest((control, textbox) => { - object selectedItem = control.Items.Cast().First(); + object selectedItem = control.ItemsSource.Cast().First(); string input = "42"; control.TextSelector = (text, item) => text + item; @@ -397,7 +397,7 @@ namespace Avalonia.Controls.UnitTests { RunTest((control, textbox) => { - object selectedItem = control.Items.Cast().First(); + object selectedItem = control.ItemsSource.Cast().First(); string input = "42"; control.ItemSelector = (text, item) => text + item; @@ -1053,7 +1053,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(Services)) { AutoCompleteBox control = CreateControl(); - control.Items = CreateSimpleStringArray(); + control.ItemsSource = CreateSimpleStringArray(); TextBox textBox = GetTextBox(control); var window = new Window {Content = control}; window.ApplyStyling(); From 22f266f99499d433009e6d616efb309c05e947ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 22:18:56 +0200 Subject: [PATCH 015/311] ItemsRepeater.Items -> ItemsSource. --- .../Pages/ItemsRepeaterPage.xaml | 2 +- .../Controls/ItemsRepeater.cs | 22 +++++++++---------- .../ItemsRepeaterTests.cs | 8 +++---- .../Xaml/StyleTests.cs | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 6bf29765f4..88be9f8e6e 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -66,7 +66,7 @@ - diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index 499904deac..5fe9c6fa05 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -36,13 +36,13 @@ namespace Avalonia.Controls ItemsControl.ItemTemplateProperty.AddOwner(); /// - /// Defines the property. + /// Defines the property. /// - public static readonly DirectProperty ItemsProperty = + public static readonly DirectProperty ItemsSourceProperty = AvaloniaProperty.RegisterDirect( - nameof(Items), - o => o.Items, - (o, v) => o.Items = v); + nameof(ItemsSource), + o => o.ItemsSource, + (o, v) => o.ItemsSource = v); /// /// Defines the property. @@ -65,7 +65,7 @@ namespace Avalonia.Controls private readonly ViewManager _viewManager; private readonly ViewportManager _viewportManager; private readonly TargetWeakEventSubscriber _layoutWeakSubscriber; - private IEnumerable? _items; + private IEnumerable? _itemsSource; private RepeaterLayoutContext? _layoutContext; private EventHandler? _childIndexChanged; private bool _isLayoutInProgress; @@ -116,16 +116,16 @@ namespace Avalonia.Controls /// /// Gets or sets an object source used to generate the content of the ItemsRepeater. /// - public IEnumerable? Items + public IEnumerable? ItemsSource { - get => _items; - set => SetAndRaise(ItemsProperty, ref _items, value); + get => _itemsSource; + set => SetAndRaise(ItemsSourceProperty, ref _itemsSource, value); } /// /// Gets or sets the template used to display each item. /// - [InheritDataTypeFromItems(nameof(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); @@ -415,7 +415,7 @@ namespace Avalonia.Controls /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (change.Property == ItemsProperty) + if (change.Property == ItemsSourceProperty) { var (oldEnumerable, newEnumerable) = change.GetOldAndNewValue(); diff --git a/tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs index 321676abc0..3c4777688d 100644 --- a/tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs +++ b/tests/Avalonia.Controls.ItemsRepeater.UnitTests/ItemsRepeaterTests.cs @@ -9,16 +9,16 @@ namespace Avalonia.Controls.UnitTests public void Can_Reassign_Items() { var target = new ItemsRepeater(); - target.Items = new ObservableCollection(); - target.Items = new ObservableCollection(); + target.ItemsSource = new ObservableCollection(); + target.ItemsSource = new ObservableCollection(); } [Fact] public void Can_Reassign_Items_To_Null() { var target = new ItemsRepeater(); - target.Items = new ObservableCollection(); - target.Items = null; + target.ItemsSource = new ObservableCollection(); + target.ItemsSource = null; } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 20a4543b72..2c20b7a0b7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -424,7 +424,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml }; var list = window.FindControl("list"); - list.Items = collection; + list.ItemsSource = collection; window.Show(); From e77d6cc2942d576c417bcaf0619f70ef1f141955 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 28 Mar 2023 22:59:50 +0200 Subject: [PATCH 016/311] MenuFlyout.Items -> ItemsSource. --- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 34 +++++++++++-------- src/Avalonia.Controls/ItemsSourceView.cs | 2 +- .../MaskedTextBoxTests.cs | 2 +- .../MenuItemTests.cs | 2 +- .../TextBoxTests.cs | 2 +- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index 8e1bc214a7..a2ce93ee6d 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -1,6 +1,5 @@ using System.Collections; using System.ComponentModel; -using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Metadata; @@ -12,17 +11,15 @@ namespace Avalonia.Controls { public MenuFlyout() { - _items = new AvaloniaList(); + Items = new ItemCollection(); } /// - /// Defines the property + /// Defines the property /// - public static readonly DirectProperty ItemsProperty = - AvaloniaProperty.RegisterDirect( - nameof(Items), - x => x.Items, - (x, v) => x.Items = v); + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register( + nameof(ItemsSource)); /// /// Defines the property @@ -45,14 +42,16 @@ namespace Avalonia.Controls public Classes FlyoutPresenterClasses => _classes ??= new Classes(); + [Content] + public ItemCollection Items { get; } + /// /// Gets or sets the items of the MenuFlyout /// - [Content] - public IEnumerable? Items + public IEnumerable? ItemsSource { - get => _items; - set => SetAndRaise(ItemsProperty, ref _items, value); + get => GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); } /// @@ -83,14 +82,13 @@ namespace Avalonia.Controls } private Classes? _classes; - private IEnumerable? _items; private IDataTemplate? _itemTemplate; protected override Control CreatePresenter() { return new MenuFlyoutPresenter { - [!ItemsControl.ItemsSourceProperty] = this[!ItemsProperty], + ItemsSource = Items, [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty], [!ItemsControl.ItemContainerThemeProperty] = this[!ItemContainerThemeProperty], }; @@ -113,5 +111,13 @@ namespace Avalonia.Controls base.OnOpening(args); } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ItemsSourceProperty) + Items.SetItemsSource(change.GetNewValue()); + } } } diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 614b70d0ba..977d712371 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -262,7 +262,7 @@ namespace Avalonia.Controls _source = source switch { - ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), + ItemsSourceView isv => isv.Source, IList list => list, INotifyCollectionChanged => throw new ArgumentException( "Collection implements INotifyCollectionChanged but not IList.", diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 19009416ef..9464df230d 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -72,7 +72,7 @@ namespace Avalonia.Controls.UnitTests Text = "1234", ContextFlyout = new MenuFlyout { - Items = new List + Items = { new MenuItem { Header = "Item 1" }, new MenuItem {Header = "Item 2" }, diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 1049ff2678..7b959ccc5e 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -230,7 +230,7 @@ namespace Avalonia.Controls.UnitTests return true; }); var target = new MenuItem(); - var flyout = new MenuFlyout { Items = new AvaloniaList { target } }; + var flyout = new MenuFlyout { Items = { target } }; var button = new Button { Flyout = flyout }; var window = new Window { Content = button }; window.ApplyStyling(); diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 0f0fd8f6c4..d71abe5a67 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -93,7 +93,7 @@ namespace Avalonia.Controls.UnitTests Text = "1234", ContextFlyout = new MenuFlyout { - Items = new List + Items = { new MenuItem { Header = "Item 1" }, new MenuItem {Header = "Item 2" }, From 6c852f805f7d61f8a272398c9668a420e6f6c9cf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 19:13:31 +0200 Subject: [PATCH 017/311] Don't allow window zoom when CanResize=false. --- src/Avalonia.Native/WindowImpl.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 5d0e6a2d18..817fe3d080 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -21,6 +21,7 @@ namespace Avalonia.Native private DoubleClickHelper _doubleClickHelper; private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; private readonly AvaloniaNativeTextInputMethod _inputMethod; + private bool _canResize = true; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) @@ -75,6 +76,7 @@ namespace Avalonia.Native public void CanResize(bool value) { + _canResize = value; _native.SetCanResize(value.AsComBool()); } @@ -137,14 +139,10 @@ namespace Avalonia.Native { if (_doubleClickHelper.IsDoubleClick(e.Timestamp, e.Position)) { - // TOGGLE WINDOW STATE. - if (WindowState == WindowState.Maximized || WindowState == WindowState.FullScreen) + if (_canResize) { - WindowState = WindowState.Normal; - } - else - { - WindowState = WindowState.Maximized; + WindowState = WindowState is WindowState.Maximized or WindowState.FullScreen ? + WindowState.Normal : WindowState.Maximized; } } else From 63295e817f23679cfcc3aefbb4320680d21b47ff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 23:28:11 +0200 Subject: [PATCH 018/311] Fix ControlAutomationPeer.GetName. :facepalm: --- .../Automation/Peers/ControlAutomationPeer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs index e8fb6b75ad..d04dfec3e8 100644 --- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs @@ -88,10 +88,10 @@ namespace Avalonia.Automation.Peers if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy) { - return labeledBy.GetName(); + result = labeledBy.GetName(); } - return null; + return result; } protected override AutomationPeer? GetParentCore() From f8a8c4ab1facf4d795ea3b37b0874a6a90866609 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 23:29:20 +0200 Subject: [PATCH 019/311] Run check for disabled maximize button on Windows. And also check with client-side decorations. --- .../Controls/CaptionButtons.xaml | 9 ++- .../WindowTests.cs | 55 ++++++++++++++++++- .../WindowTests_MacOS.cs | 16 ------ 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml index 71ae012289..7ce775e4c2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CaptionButtons.xaml @@ -48,7 +48,8 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/VirtualizationDemo/MainWindow.xaml.cs b/samples/VirtualizationDemo/MainWindow.xaml.cs deleted file mode 100644 index cea200dcec..0000000000 --- a/samples/VirtualizationDemo/MainWindow.xaml.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using VirtualizationDemo.ViewModels; - -namespace VirtualizationDemo -{ - public class MainWindow : Window - { - public MainWindow() - { - this.InitializeComponent(); - this.AttachDevTools(); - DataContext = new MainWindowViewModel(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/samples/VirtualizationDemo/Models/Chat.cs b/samples/VirtualizationDemo/Models/Chat.cs new file mode 100644 index 0000000000..e84760135c --- /dev/null +++ b/samples/VirtualizationDemo/Models/Chat.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace VirtualizationDemo.Models; + +public class ChatFile +{ + public ChatMessage[] Chat { get; set; } + + public static ChatFile Load(string path) + { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + using var s = File.OpenRead(path); + return JsonSerializer.Deserialize(s, options); + } +} + +public record ChatMessage(string Sender, string Message, DateTimeOffset Timestamp); diff --git a/samples/VirtualizationDemo/Program.cs b/samples/VirtualizationDemo/Program.cs index febda46450..87212b6daa 100644 --- a/samples/VirtualizationDemo/Program.cs +++ b/samples/VirtualizationDemo/Program.cs @@ -1,15 +1,14 @@ using Avalonia; -namespace VirtualizationDemo +namespace VirtualizationDemo; + +class Program { - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } + public static int Main(string[] args) + => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } diff --git a/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs new file mode 100644 index 0000000000..5ade0ec9ec --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ChatPageViewModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using System.IO; +using VirtualizationDemo.Models; + +namespace VirtualizationDemo.ViewModels; + +public class ChatPageViewModel +{ + public ChatPageViewModel() + { + var chat = ChatFile.Load(Path.Combine("Assets", "chat.json")); + Messages = new(chat.Chat); + } + + public ObservableCollection Messages { get; } +} diff --git a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs deleted file mode 100644 index 9ba505ffe5..0000000000 --- a/samples/VirtualizationDemo/ViewModels/ItemViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using MiniMvvm; - -namespace VirtualizationDemo.ViewModels -{ - internal class ItemViewModel : ViewModelBase - { - private string _prefix; - private int _index; - private double _height = double.NaN; - - public ItemViewModel(int index, string prefix = "Item") - { - _prefix = prefix; - _index = index; - } - - public string Header => $"{_prefix} {_index}"; - - public double Height - { - get => _height; - set => this.RaiseAndSetIfChanged(ref _height, value); - } - } -} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 96dbbc1a83..6d3590307c 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -1,160 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive; -using Avalonia.Collections; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Layout; -using Avalonia.Controls.Selection; -using MiniMvvm; +using MiniMvvm; -namespace VirtualizationDemo.ViewModels -{ - internal class MainWindowViewModel : ViewModelBase - { - private int _itemCount = 200; - private string _newItemString = "New Item"; - private int _newItemIndex; - private AvaloniaList _items; - private string _prefix = "Item"; - private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; - private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; - private Orientation _orientation = Orientation.Vertical; - - public MainWindowViewModel() - { - this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); - RecreateCommand = MiniCommand.Create(() => Recreate()); - - AddItemCommand = MiniCommand.Create(() => AddItem()); - - RemoveItemCommand = MiniCommand.Create(() => Remove()); - - SelectFirstCommand = MiniCommand.Create(() => SelectItem(0)); - - SelectLastCommand = MiniCommand.Create(() => SelectItem(Items.Count - 1)); - } - - public string NewItemString - { - get { return _newItemString; } - set { this.RaiseAndSetIfChanged(ref _newItemString, value); } - } - - public int ItemCount - { - get { return _itemCount; } - set { this.RaiseAndSetIfChanged(ref _itemCount, value); } - } - - public SelectionModel Selection { get; } = new SelectionModel(); - - public AvaloniaList Items - { - get { return _items; } - private set { this.RaiseAndSetIfChanged(ref _items, value); } - } - - public Orientation Orientation - { - get { return _orientation; } - set { this.RaiseAndSetIfChanged(ref _orientation, value); } - } - - public IEnumerable Orientations => - Enum.GetValues(typeof(Orientation)).Cast(); - - public ScrollBarVisibility HorizontalScrollBarVisibility - { - get { return _horizontalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _horizontalScrollBarVisibility, value); } - } +namespace VirtualizationDemo.ViewModels; - public ScrollBarVisibility VerticalScrollBarVisibility - { - get { return _verticalScrollBarVisibility; } - set { this.RaiseAndSetIfChanged(ref _verticalScrollBarVisibility, value); } - } - - public IEnumerable ScrollBarVisibilities => - Enum.GetValues(typeof(ScrollBarVisibility)).Cast(); - - public MiniCommand AddItemCommand { get; private set; } - public MiniCommand RecreateCommand { get; private set; } - public MiniCommand RemoveItemCommand { get; private set; } - public MiniCommand SelectFirstCommand { get; private set; } - public MiniCommand SelectLastCommand { get; private set; } - - public void RandomizeSize() - { - var random = new Random(); - - foreach (var i in Items) - { - i.Height = random.Next(240) + 10; - } - } - - public void ResetSize() - { - foreach (var i in Items) - { - i.Height = double.NaN; - } - } - - private void ResizeItems(int count) - { - if (Items == null) - { - var items = Enumerable.Range(0, count) - .Select(x => new ItemViewModel(x)); - Items = new AvaloniaList(items); - } - else if (count > Items.Count) - { - var items = Enumerable.Range(Items.Count, count - Items.Count) - .Select(x => new ItemViewModel(x)); - Items.AddRange(items); - } - else if (count < Items.Count) - { - Items.RemoveRange(count, Items.Count - count); - } - } - - private void AddItem() - { - var index = Items.Count; - - if (Selection.SelectedItems.Count > 0) - { - index = Selection.SelectedIndex; - } - - Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); - } - - private void Remove() - { - if (Selection.SelectedItems.Count > 0) - { - Items.RemoveAll(Selection.SelectedItems.ToList()); - } - } - - private void Recreate() - { - _prefix = _prefix == "Item" ? "Recreated" : "Item"; - var items = Enumerable.Range(0, _itemCount) - .Select(x => new ItemViewModel(x, _prefix)); - Items = new AvaloniaList(items); - } - - private void SelectItem(int index) - { - Selection.SelectedIndex = index; - } - } +internal class MainWindowViewModel : ViewModelBase +{ + public ChatPageViewModel Chat { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml b/samples/VirtualizationDemo/Views/ChatPageView.axaml new file mode 100644 index 0000000000..fc182f15ae --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs new file mode 100644 index 0000000000..b5c90db69c --- /dev/null +++ b/samples/VirtualizationDemo/Views/ChatPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace VirtualizationDemo.Views; + +public partial class ChatPageView : UserControl +{ + public ChatPageView() + { + InitializeComponent(); + } +} diff --git a/samples/VirtualizationDemo/VirtualizationDemo.csproj b/samples/VirtualizationDemo/VirtualizationDemo.csproj index 81b30c6cbe..3ac7aab589 100644 --- a/samples/VirtualizationDemo/VirtualizationDemo.csproj +++ b/samples/VirtualizationDemo/VirtualizationDemo.csproj @@ -1,19 +1,24 @@  - Exe + WinExe net6.0 + true + + + - - + + + + + PreserveNewest + - - - - - + + From 35d70577f48d352c9dec7ce3e9389ff2ac4bc7d4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:45:45 +0200 Subject: [PATCH 026/311] Added repro for #10367 to virtualization demo. --- samples/VirtualizationDemo/MainWindow.axaml | 3 +++ .../ViewModels/ExpanderItemViewModel.cs | 21 +++++++++++++++++++ .../ViewModels/ExpanderPageViewModel.cs | 17 +++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 1 + .../Views/ExpanderPageView.axaml | 18 ++++++++++++++++ .../Views/ExpanderPageView.axaml.cs | 13 ++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs create mode 100644 samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml create mode 100644 samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs diff --git a/samples/VirtualizationDemo/MainWindow.axaml b/samples/VirtualizationDemo/MainWindow.axaml index 94e7c96a76..e064e6ab32 100644 --- a/samples/VirtualizationDemo/MainWindow.axaml +++ b/samples/VirtualizationDemo/MainWindow.axaml @@ -12,5 +12,8 @@ + + + diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs new file mode 100644 index 0000000000..a17fc2d303 --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderItemViewModel.cs @@ -0,0 +1,21 @@ +using MiniMvvm; + +namespace VirtualizationDemo.ViewModels; + +public class ExpanderItemViewModel : ViewModelBase +{ + private string? _header; + private bool _isExpanded; + + public string? Header + { + get => _header; + set => RaiseAndSetIfChanged(ref _header, value); + } + + public bool IsExpanded + { + get => _isExpanded; + set => RaiseAndSetIfChanged(ref _isExpanded, value); + } +} diff --git a/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs new file mode 100644 index 0000000000..f2807a803b --- /dev/null +++ b/samples/VirtualizationDemo/ViewModels/ExpanderPageViewModel.cs @@ -0,0 +1,17 @@ +using System.Collections.ObjectModel; +using System.Linq; + +namespace VirtualizationDemo.ViewModels; + +internal class ExpanderPageViewModel +{ + public ExpanderPageViewModel() + { + Items = new(Enumerable.Range(0, 100).Select(x => new ExpanderItemViewModel + { + Header = $"Item {x}", + })); + } + + public ObservableCollection Items { get; set; } +} diff --git a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs index 6d3590307c..6432503595 100644 --- a/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationDemo/ViewModels/MainWindowViewModel.cs @@ -5,4 +5,5 @@ namespace VirtualizationDemo.ViewModels; internal class MainWindowViewModel : ViewModelBase { public ChatPageViewModel Chat { get; } = new(); + public ExpanderPageViewModel Expanders { get; } = new(); } diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml new file mode 100644 index 0000000000..972d885229 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs new file mode 100644 index 0000000000..df3689cf24 --- /dev/null +++ b/samples/VirtualizationDemo/Views/ExpanderPageView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace VirtualizationDemo.Views; + +public partial class ExpanderPageView : UserControl +{ + public ExpanderPageView() + { + InitializeComponent(); + } +} \ No newline at end of file From 345fb7e1d6b8aaadf01b3f7f2f722dbff433a261 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 16:23:57 +0200 Subject: [PATCH 027/311] Register anchor candidate in panel. We need to register controls as anchor candidates in the panel instead of in `ItemsControl` because the candidate needs to be registered after arrange. Consider this scenario: - In Measure: - Container is realized and registered as an anchor candidate - Container is unrealized and unregistered - Container is recycled and registered, but it is still placed in the position from before it was recycled - In Arrange: - The container is placed in its new position - The `ScrollContentPresenter` sees it's been moved and adjusts the viewport to anchor it This is obviously incorrect, but was what was happening when `ItemsControl` was responsible for registering anchor candidates. Instead of tracking which containers have already been registered, change the list of anchor candidates in `ScrollContentPresenter` to a `HashSet` so we can just register it multiple times. --- src/Avalonia.Controls/ItemsControl.cs | 4 ---- .../Presenters/ScrollContentPresenter.cs | 5 +++-- src/Avalonia.Controls/VirtualizingStackPanel.cs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 54ffba462f..4d71cc8d4f 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -94,7 +94,6 @@ namespace Avalonia.Controls private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; private IDataTemplate? _displayMemberItemTemplate; - private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; /// @@ -457,7 +456,6 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } @@ -629,7 +627,6 @@ namespace Avalonia.Controls internal void ItemContainerPrepared(Control container, object? item, int index) { _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index)); - _scrollViewer?.RegisterAnchorCandidate(container); ContainerPrepared?.Invoke(this, new(container, index)); } @@ -642,7 +639,6 @@ namespace Avalonia.Controls internal void ClearItemContainer(Control container) { - _scrollViewer?.UnregisterAnchorCandidate(container); ClearContainerForItemOverride(container); ContainerClearing?.Invoke(this, new(container)); } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index bc86558ab3..261b7d3533 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Presenters private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; private Dictionary? _scrollGestureSnapPoints; - private List? _anchorCandidates; + private HashSet? _anchorCandidates; private Control? _anchorElement; private Rect _anchorElementBounds; private bool _isAnchorElementDirty; @@ -310,7 +310,7 @@ namespace Avalonia.Controls.Presenters "An anchor control must be a visual descendent of the ScrollContentPresenter."); } - _anchorCandidates ??= new List(); + _anchorCandidates ??= new(); _anchorCandidates.Add(element); _isAnchorElementDirty = true; } @@ -410,6 +410,7 @@ namespace Avalonia.Controls.Presenters try { _arranging = true; + Offset = newOffset; } finally diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 5a766b9cd3..ad2ae9278c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -67,6 +67,7 @@ namespace Avalonia.Controls private double _lastEstimatedElementSizeU = 25; private RealizedStackElements? _measureElements; private RealizedStackElements? _realizedElements; + private ScrollViewer? _scrollViewer; private Rect _viewport = s_invalidViewport; private Stack? _recyclePool; private Control? _unrealizedFocusedElement; @@ -203,6 +204,7 @@ namespace Avalonia.Controls new Rect(u, 0, sizeU, finalSize.Height) : new Rect(0, u, finalSize.Width, sizeU); e.Arrange(rect); + _scrollViewer?.RegisterAnchorCandidate(e); u += orientation == Orientation.Horizontal ? rect.Width : rect.Height; } } @@ -217,6 +219,18 @@ namespace Avalonia.Controls } } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _scrollViewer = this.FindAncestorOfType(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _scrollViewer = null; + } + protected override void OnItemsChanged(IReadOnlyList items, NotifyCollectionChangedEventArgs e) { InvalidateMeasure(); @@ -598,6 +612,8 @@ namespace Avalonia.Controls { Debug.Assert(ItemContainerGenerator is not null); + _scrollViewer?.UnregisterAnchorCandidate(element); + if (element.IsSet(ItemIsOwnContainerProperty)) { element.IsVisible = false; From 54449850cc39d9c04c13412fc3fb9f27c35c03aa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 31 Mar 2023 22:56:13 +0200 Subject: [PATCH 028/311] Make thumb drag delta relative to parent. Prevents the viewport jumping around when scrolling a virtualized list with differing sizes. Instead the thumb jumps around. --- src/Avalonia.Controls/Primitives/Thumb.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index cb3195cf52..c205830bc2 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Metadata; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -80,20 +81,22 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { + var point = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, - Vector = e.GetPosition(this) - _lastPoint.Value, + Vector = point - _lastPoint.Value, }; RaiseEvent(ev); + _lastPoint = point; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this); + _lastPoint = e.GetPosition(this.GetVisualParent()); var ev = new VectorEventArgs { @@ -116,7 +119,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this), + Vector = (Vector)e.GetPosition(this.GetVisualParent()), }; RaiseEvent(ev); From bc32c061e8fe3229cd60ff4a1767e83389dd0667 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 3 Apr 2023 10:06:50 +0200 Subject: [PATCH 029/311] Added tests to detect scroll jumps. --- .../VirtualizingStackPanel.cs | 10 ++ .../VirtualizingStackPanelTests.cs | 146 +++++++++++++++++- 2 files changed, 149 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index ad2ae9278c..7cca0986ad 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -130,6 +130,16 @@ namespace Avalonia.Controls set { SetValue(AreVerticalSnapPointsRegularProperty, value); } } + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1; + protected override Size MeasureOverride(Size availableSize) { if (!IsEffectivelyVisible) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 76fc31c50d..2cdd4eaf95 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10}); - var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false); + var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null); Assert.Equal(1000, scroll.Extent.Height); @@ -252,7 +252,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var items = new ObservableCollection