From 4d34a2c6e76fc415d2c62b22142aa937305cdf0c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:26:43 -0500 Subject: [PATCH 01/12] Move headless projects to a subfolder --- Avalonia.sln | 8 ++++++-- .../ControlCatalog.NetCore/ControlCatalog.NetCore.csproj | 2 +- .../MobileSandbox.Desktop/MobileSandbox.Desktop.csproj | 2 +- .../Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj | 2 +- .../Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs | 0 .../HeadlessVncPlatformExtensions.cs | 0 .../Avalonia.Headless/Avalonia.Headless.csproj | 6 +++--- .../Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 0 .../Avalonia.Headless/HeadlessPlatformRenderInterface.cs | 0 .../Avalonia.Headless/HeadlessPlatformStubs.cs | 0 .../HeadlessPlatformThreadingInterface.cs | 0 .../Avalonia.Headless/HeadlessWindowImpl.cs | 0 src/{ => Headless}/Avalonia.Headless/IHeadlessWindow.cs | 0 13 files changed, 12 insertions(+), 8 deletions(-) rename src/{ => Headless}/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj (85%) rename src/{ => Headless}/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs (100%) rename src/{ => Headless}/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs (100%) rename src/{ => Headless}/Avalonia.Headless/Avalonia.Headless.csproj (60%) rename src/{ => Headless}/Avalonia.Headless/AvaloniaHeadlessPlatform.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformRenderInterface.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformStubs.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs (100%) rename src/{ => Headless}/Avalonia.Headless/HeadlessWindowImpl.cs (100%) rename src/{ => Headless}/Avalonia.Headless/IHeadlessWindow.cs (100%) diff --git a/Avalonia.sln b/Avalonia.sln index e66b73de0e..295f7cb149 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -181,9 +181,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.Vnc", "src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" EndProject @@ -246,6 +246,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -644,6 +646,8 @@ Global {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e465e9caf3..877d475fb6 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -26,7 +26,7 @@ - + diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj index a24e55de81..31a6b05175 100644 --- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj +++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj similarity index 85% rename from src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj rename to src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index c713440dc9..5bdedec81d 100644 --- a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -9,5 +9,5 @@ - + diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs similarity index 100% rename from src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs similarity index 100% rename from src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs rename to src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs diff --git a/src/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj similarity index 60% rename from src/Avalonia.Headless/Avalonia.Headless.csproj rename to src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index 95f7b79009..c3a472e950 100644 --- a/src/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -7,7 +7,7 @@ - - - + + + diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs similarity index 100% rename from src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs rename to src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformStubs.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs rename to src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs similarity index 100% rename from src/Avalonia.Headless/HeadlessWindowImpl.cs rename to src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs diff --git a/src/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs similarity index 100% rename from src/Avalonia.Headless/IHeadlessWindow.cs rename to src/Headless/Avalonia.Headless/IHeadlessWindow.cs From a2bd18457463a847566ac2095abbc8bc62d80cde Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:46:22 -0500 Subject: [PATCH 02/12] Move NoopStorageProvider to the base project --- .../Platform/Storage/NoopStorageProvider.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/TopLevel.cs | 5 +++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs diff --git a/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs new file mode 100644 index 0000000000..153634027c --- /dev/null +++ b/src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform.Storage.FileIO; + +namespace Avalonia.Platform.Storage; + +internal class NoopStorageProvider : BclStorageProvider +{ + public override bool CanOpen => false; + public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } + + public override bool CanSave => false; + public override Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + return Task.FromResult(null); + } + + public override bool CanPickFolder => false; + public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + return Task.FromResult>(Array.Empty()); + } +} diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index fdcb8cc537..eec6b08ecb 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -387,10 +387,13 @@ namespace Avalonia.Controls IStyleHost IStyleHost.StylingParent => _globalStyles!; + /// + /// File System storage service used for file pickers and bookmarks. + /// public IStorageProvider StorageProvider => _storageProvider ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? PlatformImpl?.TryGetFeature() - ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); + ?? new NoopStorageProvider(); /// Point IRenderRoot.PointToClient(PixelPoint p) From 9e93791ea59a767067c4fb04b5f9641ded6e358b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 24 Feb 2023 21:46:28 -0500 Subject: [PATCH 03/12] Enable nullable in headless platform --- .../Avalonia.Controls.csproj | 1 + .../Platform/ScreenHelper.cs | 3 +- .../Avalonia.Headless.Vnc.csproj | 3 + .../HeadlessVncFramebufferSource.cs | 2 +- .../HeadlessVncPlatformExtensions.cs | 5 +- .../Avalonia.Headless.csproj | 5 +- .../AvaloniaHeadlessPlatform.cs | 13 ++- .../HeadlessPlatformRenderInterface.cs | 43 +++++----- .../HeadlessPlatformStubs.cs | 73 +++++++--------- .../HeadlessPlatformThreadingInterface.cs | 14 ++-- .../Avalonia.Headless/HeadlessWindowImpl.cs | 83 +++++++++---------- .../Avalonia.Headless/IHeadlessWindow.cs | 2 +- 12 files changed, 120 insertions(+), 127 deletions(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 3195c38eef..5adb5a8f2e 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -16,5 +16,6 @@ + diff --git a/src/Avalonia.Controls/Platform/ScreenHelper.cs b/src/Avalonia.Controls/Platform/ScreenHelper.cs index 0bd2be69d0..59b29b4748 100644 --- a/src/Avalonia.Controls/Platform/ScreenHelper.cs +++ b/src/Avalonia.Controls/Platform/ScreenHelper.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using Avalonia.Controls; using Avalonia.Utilities; #nullable enable namespace Avalonia.Platform { - public static class ScreenHelper + internal static class ScreenHelper { public static Screen? ScreenFromPoint(PixelPoint point, IReadOnlyList screens) { diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 5bdedec81d..3812fb196d 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -9,5 +9,8 @@ + + + diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 18c149ce2e..be0f3579a5 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -17,7 +17,7 @@ namespace Avalonia.Headless.Vnc private VncButton _previousButtons; public HeadlessVncFramebufferSource(VncServerSession session, Window window) { - Window = (IHeadlessWindow)window.PlatformImpl; + Window = window.PlatformImpl as IHeadlessWindow ?? throw new InvalidOperationException("Invalid window parameter"); session.PointerChanged += (_, args) => { var pt = new Point(args.X, args.Y); diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs index efc8c66fde..8e5cd1a316 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Sockets; using Avalonia.Controls; @@ -25,7 +26,7 @@ namespace Avalonia }) .AfterSetup(_ => { - var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); + var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance!.ApplicationLifetime!); lt.Startup += async delegate { while (true) @@ -38,7 +39,7 @@ namespace Avalonia var session = new VncServerSession(); session.SetFramebufferSource(new HeadlessVncFramebufferSource( - session, lt.MainWindow)); + session, lt.MainWindow ?? throw new InvalidOperationException("MainWindow wasn't initialized"))); session.Connect(client.GetStream(), options); } diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index c3a472e950..fbfa89f5a4 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -1,13 +1,14 @@  net6.0;netstandard2.0 - + - + + diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b0b1d731d2..6619b533f6 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,8 +1,6 @@ using System; using System.Diagnostics; using Avalonia.Reactive; -using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -14,11 +12,12 @@ namespace Avalonia.Headless { public static class AvaloniaHeadlessPlatform { - internal static Compositor Compositor { get; private set; } - class RenderTimer : DefaultRenderTimer + internal static Compositor? Compositor { get; private set; } + + private class RenderTimer : DefaultRenderTimer { private readonly int _framesPerSecond; - private Action _forceTick; + private Action? _forceTick; protected override IDisposable StartCore(Action tick) { bool cancelled = false; @@ -46,7 +45,7 @@ namespace Avalonia.Headless public void ForceTick() => _forceTick?.Invoke(); } - class HeadlessWindowingPlatform : IWindowingPlatform + private class HeadlessWindowingPlatform : IWindowingPlatform { public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false); @@ -54,7 +53,7 @@ namespace Avalonia.Headless public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); - public ITrayIconImpl CreateTrayIcon() => null; + public ITrayIconImpl? CreateTrayIcon() => null; } internal static void Initialize(AvaloniaHeadlessPlatformOptions opts) diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 31aaebcdc7..9944a10f94 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Numerics; using System.Runtime.InteropServices; @@ -23,7 +24,7 @@ namespace Avalonia.Headless public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; - public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this; public bool SupportsIndividualRoundRects => false; @@ -52,7 +53,7 @@ namespace Avalonia.Headless public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; - public object TryGetFeature(Type featureType) => null; + public object? TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -130,7 +131,7 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } - class HeadlessGlyphRunStub : IGlyphRunImpl + private class HeadlessGlyphRunStub : IGlyphRunImpl { public Size Size => new Size(8, 12); @@ -144,7 +145,7 @@ namespace Avalonia.Headless => Array.Empty(); } - class HeadlessGeometryStub : IGeometryImpl + private class HeadlessGeometryStub : IGeometryImpl { public HeadlessGeometryStub(Rect bounds) { @@ -157,7 +158,7 @@ namespace Avalonia.Headless public virtual bool FillContains(Point point) => Bounds.Contains(point); - public Rect GetRenderBounds(IPen pen) + public Rect GetRenderBounds(IPen? pen) { if(pen is null) { @@ -167,7 +168,7 @@ namespace Avalonia.Headless return Bounds.Inflate(pen.Thickness / 2); } - public bool StrokeContains(IPen pen, Point point) + public bool StrokeContains(IPen? pen, Point point) { return false; } @@ -191,21 +192,21 @@ namespace Avalonia.Headless return false; } - public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { segmentGeometry = null; return false; } } - class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl + private class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl { public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform)) { } - static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) + private static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) { if (b is HeadlessTransformedGeometryStub transformed) { @@ -227,7 +228,7 @@ namespace Avalonia.Headless public Matrix Transform { get; } } - class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl + private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { public HeadlessStreamingGeometryStub() : base(default) { @@ -243,7 +244,7 @@ namespace Avalonia.Headless return new HeadlessStreamingGeometryContextStub(this); } - class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl + private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl { private readonly HeadlessStreamingGeometryStub _parent; private double _x1, _y1, _x2, _y2; @@ -252,7 +253,7 @@ namespace Avalonia.Headless _parent = parent; } - void Track(Point pt) + private void Track(Point pt) { if (_x1 > pt.X) _x1 = pt.X; @@ -301,7 +302,7 @@ namespace Avalonia.Headless } } - class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl + private class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl { public Size Size { get; } @@ -363,7 +364,7 @@ namespace Avalonia.Headless } } - class HeadlessDrawingContextStub : IDrawingContextImpl + private class HeadlessDrawingContextStub : IDrawingContextImpl { public void Dispose() { @@ -437,16 +438,16 @@ namespace Avalonia.Headless } - public object GetFeature(Type t) + public object? GetFeature(Type t) { return null; } - public void DrawLine(IPen pen, Point p1, Point p2) + public void DrawLine(IPen? pen, Point p1, Point p2) { } - public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { } @@ -464,16 +465,16 @@ namespace Avalonia.Headless } - public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadow = default) + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadow = default) { } - public void DrawEllipse(IBrush brush, IPen pen, Rect rect) + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { } - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) { } @@ -484,7 +485,7 @@ namespace Avalonia.Headless } } - class HeadlessRenderTarget : IRenderTarget + private class HeadlessRenderTarget : IRenderTarget { public void Dispose() { diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 46e3515d11..ecdc7a1d35 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -18,17 +18,17 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessClipboardStub : IClipboard + internal class HeadlessClipboardStub : IClipboard { - private string _text; - private IDataObject _data; + private string? _text; + private IDataObject? _data; - public Task GetTextAsync() + public Task GetTextAsync() { return Task.Run(() => _text); } - public Task SetTextAsync(string text) + public Task SetTextAsync(string? text) { return Task.Run(() => _text = text); } @@ -45,16 +45,29 @@ namespace Avalonia.Headless public Task GetFormatsAsync() { - throw new NotImplementedException(); + return Task.Run(() => + { + if (_data is not null) + { + return _data.GetDataFormats().ToArray(); + } + + if (_text is not null) + { + return new[] { DataFormats.Text }; + } + + return Array.Empty(); + }); } - public async Task GetDataAsync(string format) + public async Task GetDataAsync(string format) { return await Task.Run(() => _data); } } - class HeadlessCursorFactoryStub : ICursorFactory + internal class HeadlessCursorFactoryStub : ICursorFactory { public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorStub(); @@ -65,7 +78,7 @@ namespace Avalonia.Headless } } - class HeadlessGlyphTypefaceImpl : IGlyphTypeface + internal class HeadlessGlyphTypefaceImpl : IGlyphTypeface { public FontMetrics Metrics => new FontMetrics { @@ -117,7 +130,7 @@ namespace Avalonia.Headless public bool TryGetTable(uint tag, out byte[] table) { - table = null; + table = null!; return false; } @@ -133,7 +146,7 @@ namespace Avalonia.Headless } } - class HeadlessTextShaperStub : ITextShaperImpl + internal class HeadlessTextShaperStub : ITextShaperImpl { public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) { @@ -145,7 +158,7 @@ namespace Avalonia.Headless } } - class HeadlessFontManagerStub : IFontManagerImpl + internal class HeadlessFontManagerStub : IFontManagerImpl { public IGlyphTypeface CreateGlyphTypeface(Typeface typeface) { @@ -163,17 +176,16 @@ namespace Avalonia.Headless } public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, - FontFamily fontFamily, CultureInfo culture, out Typeface typeface) + FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface) { typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch); return true; } } - class HeadlessIconLoaderStub : IPlatformIconLoader + internal class HeadlessIconLoaderStub : IPlatformIconLoader { - - class IconStub : IWindowIconImpl + private class IconStub : IWindowIconImpl { public void Save(Stream outputStream) { @@ -196,7 +208,7 @@ namespace Avalonia.Headless } } - class HeadlessScreensStub : IScreenImpl + internal class HeadlessScreensStub : IScreenImpl { public int ScreenCount { get; } = 1; @@ -206,40 +218,19 @@ namespace Avalonia.Headless new PixelRect(0, 0, 1920, 1280), true), }; - public Screen ScreenFromPoint(PixelPoint point) + public Screen? ScreenFromPoint(PixelPoint point) { return ScreenHelper.ScreenFromPoint(point, AllScreens); } - public Screen ScreenFromRect(PixelRect rect) + public Screen? ScreenFromRect(PixelRect rect) { return ScreenHelper.ScreenFromRect(rect, AllScreens); } - public Screen ScreenFromWindow(IWindowBaseImpl window) + public Screen? ScreenFromWindow(IWindowBaseImpl window) { return ScreenHelper.ScreenFromWindow(window, AllScreens); } } - - internal class NoopStorageProvider : BclStorageProvider - { - public override bool CanOpen => false; - public override Task> OpenFilePickerAsync(FilePickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - - public override bool CanSave => false; - public override Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - return Task.FromResult(null); - } - - public override bool CanPickFolder => false; - public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) - { - return Task.FromResult>(Array.Empty()); - } - } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs index 046e4645e3..fa455c8032 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs @@ -6,16 +6,16 @@ using Avalonia.Threading; namespace Avalonia.Headless { - class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface + internal class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface { public HeadlessPlatformThreadingInterface() { _thread = Thread.CurrentThread; } - private AutoResetEvent _event = new AutoResetEvent(false); - private Thread _thread; - private object _lock = new object(); + private readonly AutoResetEvent _event = new AutoResetEvent(false); + private readonly Thread? _thread; + private readonly object _lock = new object(); private DispatcherPriority? _signaledPriority; public void RunLoop(CancellationToken cancellationToken) @@ -40,7 +40,7 @@ namespace Avalonia.Headless interval = TimeSpan.FromMilliseconds(10); var stopped = false; - Timer timer = null; + Timer? timer = null; timer = new Timer(_ => { if (stopped) @@ -55,7 +55,7 @@ namespace Avalonia.Headless finally { if (!stopped) - timer.Change(interval, Timeout.InfiniteTimeSpan); + timer?.Change(interval, Timeout.InfiniteTimeSpan); } }); }, @@ -81,6 +81,6 @@ namespace Avalonia.Headless } public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; - public event Action Signaled; + public event Action? Signaled; } } diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index 15e2c696ac..f2f31debad 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -18,20 +18,20 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow + internal class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow { - private IKeyboardDevice _keyboard; - private Stopwatch _st = Stopwatch.StartNew(); - private Pointer _mousePointer; - private WriteableBitmap _lastRenderedFrame; - private object _sync = new object(); + private readonly IKeyboardDevice _keyboard; + private readonly Stopwatch _st = Stopwatch.StartNew(); + private readonly Pointer _mousePointer; + private WriteableBitmap? _lastRenderedFrame; + private readonly object _sync = new object(); public bool IsPopup { get; } public HeadlessWindowImpl(bool isPopup) { IsPopup = isPopup; Surfaces = new object[] { this }; - _keyboard = AvaloniaLocator.Current.GetService(); + _keyboard = AvaloniaLocator.Current.GetRequiredService(); _mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); MouseDevice = new MouseDevice(_mousePointer); ClientSize = new Size(1024, 768); @@ -49,13 +49,13 @@ namespace Avalonia.Headless public double RenderScaling { get; } = 1; public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } - public Action Input { get; set; } - public Action Paint { get; set; } - public Action Resized { get; set; } - public Action ScalingChanged { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } + public Action? ScalingChanged { get; set; } public IRenderer CreateRenderer(IRenderRoot root) => - new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor, () => Surfaces); + new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor!, () => Surfaces); public void Invalidate(Rect rect) { @@ -66,18 +66,18 @@ namespace Avalonia.Headless InputRoot = inputRoot; } - public IInputRoot InputRoot { get; set; } + public IInputRoot? InputRoot { get; set; } public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); - public void SetCursor(ICursorImpl cursor) + public void SetCursor(ICursorImpl? cursor) { } - public Action Closed { get; set; } + public Action? Closed { get; set; } public IMouseDevice MouseDevice { get; } public void Show(bool activate, bool isDialog) @@ -102,14 +102,14 @@ namespace Avalonia.Headless } public PixelPoint Position { get; set; } - public Action PositionChanged { get; set; } + public Action? PositionChanged { get; set; } public void Activate() { Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); } - public Action Deactivated { get; set; } - public Action Activated { get; set; } + public Action? Deactivated { get; set; } + public Action? Activated { get; set; } public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); public Size MaxClientSize { get; } = new Size(1920, 1280); public void Resize(Size clientSize, PlatformResizeReason reason) @@ -124,7 +124,7 @@ namespace Avalonia.Headless }); } - void DoResize(Size clientSize) + private void DoResize(Size clientSize) { // Uncomment this check and experience a weird bug in layout engine if (ClientSize != clientSize) @@ -146,8 +146,8 @@ namespace Avalonia.Headless public IScreenImpl Screen { get; } = new HeadlessScreensStub(); public WindowState WindowState { get; set; } - public Action WindowStateChanged { get; set; } - public void SetTitle(string title) + public Action? WindowStateChanged { get; set; } + public void SetTitle(string? title) { } @@ -157,7 +157,7 @@ namespace Avalonia.Headless } - public void SetIcon(IWindowIconImpl icon) + public void SetIcon(IWindowIconImpl? icon) { } @@ -172,9 +172,9 @@ namespace Avalonia.Headless } - public Func Closing { get; set; } + public Func? Closing { get; set; } - class FramebufferProxy : ILockedFramebuffer + private class FramebufferProxy : ILockedFramebuffer { private readonly ILockedFramebuffer _fb; private readonly Action _onDispose; @@ -215,7 +215,7 @@ namespace Avalonia.Headless }); } - public IRef GetLastRenderedFrame() + public IRef? GetLastRenderedFrame() { lock (_sync) return _lastRenderedFrame?.PlatformImpl?.CloneAs(); @@ -224,19 +224,19 @@ namespace Avalonia.Headless private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; // TODO: Hook recent Popup changes. - IPopupPositioner IPopupImpl.PopupPositioner => null; + IPopupPositioner IPopupImpl.PopupPositioner => null!; public Size MaxAutoSizeHint => new Size(1920, 1080); - public Action TransparencyLevelChanged { get; set; } + public Action? TransparencyLevelChanged { get; set; } public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; - public Action GotInputWhenDisabled { get; set; } + public Action? GotInputWhenDisabled { get; set; } public bool IsClientAreaExtendedToDecorations => false; - public Action ExtendClientAreaToDecorationsChanged { get; set; } + public Action? ExtendClientAreaToDecorationsChanged { get; set; } public bool NeedsManagedDecorations => false; @@ -244,32 +244,27 @@ namespace Avalonia.Headless public Thickness OffScreenMargin => new Thickness(); - public Action LostFocus { get; set; } + public Action? LostFocus { get; set; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1); - public object TryGetFeature(Type featureType) + public object? TryGetFeature(Type featureType) { - if (featureType == typeof(IStorageProvider)) - { - return new NoopStorageProvider(); - } - return null; } void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyDown, key, modifiers)); } void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers) { - Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyUp, key, modifiers)); + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); } void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, button == 0 ? RawPointerEventType.LeftButtonDown : button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, point, modifiers)); @@ -277,13 +272,13 @@ namespace Avalonia.Headless void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, RawPointerEventType.Move, point, modifiers)); } void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) { - Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, button == 0 ? RawPointerEventType.LeftButtonUp : button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, point, modifiers)); @@ -291,14 +286,14 @@ namespace Avalonia.Headless void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot, + Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, InputRoot!, point, delta, modifiers)); } void IHeadlessWindow.DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers) { var device = AvaloniaLocator.Current.GetRequiredService(); - Input?.Invoke(new RawDragEvent(device, type, InputRoot, point, data, effects, modifiers)); + Input?.Invoke(new RawDragEvent(device, type, InputRoot!, point, data, effects, modifiers)); } void IWindowImpl.Move(PixelPoint point) @@ -306,7 +301,7 @@ namespace Avalonia.Headless } - public IPopupImpl CreatePopup() + public IPopupImpl? CreatePopup() { // TODO: Hook recent Popup changes. return null; diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index dfb3a4c433..42ec38a934 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -8,7 +8,7 @@ namespace Avalonia.Headless { public interface IHeadlessWindow { - IRef GetLastRenderedFrame(); + IRef? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); From a26566548a548c63b55385619c656ebcdb387479 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:29:58 -0500 Subject: [PATCH 04/12] Add headless xunit integration project --- Avalonia.sln | 7 +++ src/Avalonia.Controls/AppBuilder.cs | 39 +++++++++++- .../Avalonia.Controls.csproj | 1 + .../Remote/RemoteDesignerEntryPoint.cs | 12 +--- .../Avalonia.Headless.XUnit.csproj | 19 ++++++ .../AvaloniaTestFramework.cs | 35 +++++++++++ .../AvaloniaTestFrameworkAttribute.cs | 45 ++++++++++++++ .../AvaloniaTestRunner.cs | 61 +++++++++++++++++++ 8 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs create mode 100644 src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs diff --git a/Avalonia.sln b/Avalonia.sln index 295f7cb149..6d0c4dee2a 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -248,6 +248,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF237916-7150-496B-89ED-6CA3292896E7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -579,6 +581,10 @@ Global {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -648,6 +654,7 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} + {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index cf79fcd1a8..871d6bf52a 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -116,6 +116,43 @@ namespace Avalonia }; } + /// + /// Begin configuring an . + /// Should only be used for testing and design purposes, as it relies on dynamic code. + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + /// An instance. If can't be created, thrown an exception. + internal static AppBuilder Configure( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type entryPointType) + { + var appBuilderObj = entryPointType + .GetMethod( + "BuildAvaloniaApp", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, + null, + Array.Empty(), + null)? + .Invoke(null, Array.Empty()); + + if (appBuilderObj is AppBuilder appBuilder) + { + return appBuilder; + } + + if (typeof(Application).IsAssignableFrom(entryPointType)) + { + return Configure(() => (Application)Activator.CreateInstance(entryPointType)!); + } + + throw new InvalidOperationException( + $"Unable to create AppBuilder from type {entryPointType.Name}." + + $"Input type either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application type."); + } + protected AppBuilder Self => this; public AppBuilder AfterSetup(Action callback) @@ -204,7 +241,7 @@ namespace Avalonia _optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind().ToFunc(options); }; return Self; } - + /// /// Sets up the platform-specific services for the . /// diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 5adb5a8f2e..b9e2d3d259 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -17,5 +17,6 @@ + diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 85605ccd9d..313063269b 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -179,17 +179,9 @@ namespace Avalonia.DesignerSupport.Remote var entryPoint = asm.EntryPoint; if (entryPoint == null) throw Die($"Assembly {args.AppPath} doesn't have an entry point"); - var builderMethod = entryPoint.DeclaringType.GetMethod( - BuilderMethodName, - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy, - null, - Array.Empty(), - null); - if (builderMethod == null) - throw Die($"{entryPoint.DeclaringType.FullName} doesn't have a method named {BuilderMethodName}"); + Log($"Obtaining AppBuilder instance from {entryPoint.DeclaringType!.FullName}"); + var appBuilder = AppBuilder.Configure(entryPoint.DeclaringType); Design.IsDesignMode = true; - Log($"Obtaining AppBuilder instance from {builderMethod.DeclaringType.FullName}.{builderMethod.Name}"); - var appBuilder = builderMethod.Invoke(null, null); Log($"Initializing application in design mode"); var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer)); transport = initializer.ConfigureApp(transport, args, appBuilder); diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj new file mode 100644 index 0000000000..c2c58b4f94 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj @@ -0,0 +1,19 @@ + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs new file mode 100644 index 0000000000..21086fa946 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestFramework : XunitTestFramework +{ + public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink) + { + } + + protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) + => new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); + + + private class Executor : XunitTestFrameworkExecutor + { + public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, + IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider, + diagnosticMessageSink) + { + } + + protected override async void RunTestCases(IEnumerable testCases, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) + { + executionOptions.SetValue("xunit.execution.DisableParallelization", false); + using (var assemblyRunner = new AvaloniaTestRunner( + TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, + executionOptions)) await assemblyRunner.RunAsync(); + } + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs new file mode 100644 index 0000000000..d0249a5b25 --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +/// +/// +/// +[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] +public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute +{ + /// + /// Creates instance of . + /// + /// + /// Parameter from which should be created. + /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application. + /// + public AvaloniaTestFrameworkAttribute( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type appBuilderEntryPointType) { } +} + +/// +/// Discoverer implementation for the Avalonia testing framework. +/// +public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer +{ + /// + /// Creates instance of . + /// + public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _) + { + } + + /// + public Type GetTestFrameworkType(IAttributeInfo attribute) + { + var builderType = attribute.GetConstructorArguments().First() as Type + ?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute."); + return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType); + } +} diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs new file mode 100644 index 0000000000..579232521d --- /dev/null +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs @@ -0,0 +1,61 @@ +using Avalonia.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Avalonia.Headless.XUnit; + +internal class AvaloniaTestRunner : XunitTestAssemblyRunner +{ + private CancellationTokenSource? _cancellationTokenSource; + + public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable testCases, + IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, + executionMessageSink, executionOptions) + { + } + + protected override void SetupSyncContext(int maxParallelThreads) + { + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = new CancellationTokenSource(); + SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result); + } + + public override void Dispose() + { + _cancellationTokenSource?.Dispose(); + base.Dispose(); + } + + internal static Task InitNewApplicationContext(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + + new Thread(() => + { + try + { + var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry)); + + // If windowing subsystem wasn't initialized by user, force headless with default parameters. + if (appBuilder.WindowingSubsystemName != "Headless") + { + appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); + } + + appBuilder.SetupWithoutStarting(); + + tcs.SetResult(SynchronizationContext.Current!); + } + catch (Exception e) + { + tcs.SetException(e); + } + + Dispatcher.UIThread.MainLoop(cancellationToken); + }) { IsBackground = true }.Start(); + + return tcs.Task; + } +} From 5e4509deb167115b7b403754b6d2a38b2eceade9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:30:20 -0500 Subject: [PATCH 05/12] Run tests on Avalonia.Headless --- Avalonia.sln | 7 ++++ nukebuild/Build.cs | 1 + .../HeadlessPlatformRenderInterface.cs | 3 +- .../Avalonia.Headless/HeadlessWindowImpl.cs | 13 ++++++- .../Avalonia.Headless/IHeadlessWindow.cs | 2 +- .../Avalonia.Headless.UnitTests.csproj | 19 +++++++++ .../Avalonia.Headless.UnitTests/InputTests.cs | 37 ++++++++++++++++++ .../RenderingTests.cs | 39 +++++++++++++++++++ .../TestApplication.cs | 24 ++++++++++++ .../ThreadingTests.cs | 32 +++++++++++++++ 10 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj create mode 100644 tests/Avalonia.Headless.UnitTests/InputTests.cs create mode 100644 tests/Avalonia.Headless.UnitTests/RenderingTests.cs create mode 100644 tests/Avalonia.Headless.UnitTests/TestApplication.cs create mode 100644 tests/Avalonia.Headless.UnitTests/ThreadingTests.cs diff --git a/Avalonia.sln b/Avalonia.sln index 6d0c4dee2a..597eb632ca 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -250,6 +250,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -585,6 +587,10 @@ Global {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Debug|Any CPU.Build.0 = Debug|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.ActiveCfg = Release|Any CPU {F47F8316-4D4B-4026-8EF3-16B2CFDA8119}.Release|Any CPU.Build.0 = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -655,6 +661,7 @@ Global {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} {F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7} + {3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 3704cee890..9883745bfa 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -212,6 +212,7 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.Headless.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9944a10f94..7fa2e46c42 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -19,7 +19,8 @@ namespace Avalonia.Headless public static void Initialize() { AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new HeadlessPlatformRenderInterface()); + .Bind().ToConstant(new HeadlessPlatformRenderInterface()) + .Bind().ToConstant(new HeadlessFontManagerStub()); } public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index f2f31debad..e1c09107c8 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -215,10 +215,19 @@ namespace Avalonia.Headless }); } - public IRef? GetLastRenderedFrame() + public Bitmap? GetLastRenderedFrame() { lock (_sync) - return _lastRenderedFrame?.PlatformImpl?.CloneAs(); + { + if (_lastRenderedFrame is null) + { + return null; + } + + using var lockedFramebuffer = _lastRenderedFrame.Lock(); + return new Bitmap(lockedFramebuffer.Format, AlphaFormat.Opaque, lockedFramebuffer.Address, + lockedFramebuffer.Size, lockedFramebuffer.Dpi, lockedFramebuffer.RowBytes); + } } private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index 42ec38a934..3391a103d1 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -8,7 +8,7 @@ namespace Avalonia.Headless { public interface IHeadlessWindow { - IRef? GetLastRenderedFrame(); + Bitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); diff --git a/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj new file mode 100644 index 0000000000..78a3ab186e --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + true + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs new file mode 100644 index 0000000000..c4d9f1a517 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -0,0 +1,37 @@ +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class InputTests +{ + [Fact] + public void Should_Click_Button_On_Window() + { + var buttonClicked = false; + var button = new Button + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + + button.Click += (_, _) => buttonClicked = true; + + var window = new Window + { + Width = 100, + Height = 100, + Content = button + }; + window.Show(); + + Dispatcher.UIThread.RunJobs(); + + ((IHeadlessWindow)window.PlatformImpl!).MouseDown(new Point(50, 50), 0); + ((IHeadlessWindow)window.PlatformImpl!).MouseUp(new Point(50, 50), 0); + + Assert.True(buttonClicked); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs new file mode 100644 index 0000000000..67b99541d6 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class RenderingTests +{ + [Fact] + public void Should_Render_Last_Frame_To_Bitmap() + { + var window = new Window + { + Content = new ContentControl + { + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch, + Padding = new Thickness(4), + Content = new PathIcon + { + Data = StreamGeometry.Parse("M0,9 L10,0 20,9 19,10 10,2 1,10 z") + } + }, + SizeToContent = SizeToContent.WidthAndHeight + }; + window.Show(); + + Dispatcher.UIThread.RunJobs(); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + + var frame = ((IHeadlessWindow)window.PlatformImpl!).GetLastRenderedFrame(); + Assert.NotNull(frame); + } +} diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs new file mode 100644 index 0000000000..0b2927fa29 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -0,0 +1,24 @@ +using Avalonia.Headless.XUnit; +using Avalonia.Headless.XUnit.Tests; +using Avalonia.Themes.Simple; +using Xunit; + +[assembly: AvaloniaTestFramework(typeof(TestApplication))] +[assembly: CollectionBehavior(DisableTestParallelization = true)] + +namespace Avalonia.Headless.XUnit.Tests; + +public class TestApplication : Application +{ + public TestApplication() + { + Styles.Add(new SimpleTheme()); + } + + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseSkia() + .UseHeadless(new AvaloniaHeadlessPlatformOptions + { + UseHeadlessDrawing = false + }); +} diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs new file mode 100644 index 0000000000..efcd2d9081 --- /dev/null +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Threading; +using Xunit; + +namespace Avalonia.Headless.XUnit.Tests; + +public class ThreadingTests +{ + [Fact] + public void Should_Be_On_Dispatcher_Thread() + { + Dispatcher.UIThread.VerifyAccess(); + } + + [Fact] + public async Task DispatcherTimer_Works_On_The_Same_Thread() + { + var currentThread = Thread.CurrentThread; + var tcs = new TaskCompletionSource(); + + DispatcherTimer.RunOnce(() => + { + Assert.Equal(currentThread, Thread.CurrentThread); + + tcs.SetResult(); + }, TimeSpan.FromTicks(1)); + + await tcs.Task; + } +} From 6b81b5a93543bb8dcf679b6fca4e2e6c6b2a4261 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 00:51:42 -0500 Subject: [PATCH 06/12] Make headless easier to use with HeadlessWindowExtensions --- .../HeadlessWindowExtensions.cs | 50 +++++++++++++++++++ .../Avalonia.Headless/HeadlessWindowImpl.cs | 28 ++++++++--- .../Avalonia.Headless/IHeadlessWindow.cs | 8 +-- .../Avalonia.Headless.UnitTests/InputTests.cs | 9 ++-- .../RenderingTests.cs | 10 ++-- .../TestApplication.cs | 6 +-- .../ThreadingTests.cs | 2 +- 7 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs new file mode 100644 index 0000000000..ad5b7e680c --- /dev/null +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -0,0 +1,50 @@ +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media.Imaging; +using Avalonia.Threading; + +namespace Avalonia.Headless; + +public static class HeadlessWindowExtensions +{ + public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) + { + var impl = GetImpl(topLevel); + AvaloniaHeadlessPlatform.ForceRenderTimerTick(); + return impl.GetLastRenderedFrame(); + } + + public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) => + GetImpl(topLevel).GetLastRenderedFrame(); + + public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + GetImpl(topLevel).KeyPress(key, modifiers); + + public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => + GetImpl(topLevel).KeyRelease(key, modifiers); + + public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + + public static void MouseMove(this TopLevel topLevel, Point point, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + + public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + + public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, + RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + + public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, + DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => + GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + + private static IHeadlessWindow GetImpl(this TopLevel topLevel) + { + Dispatcher.UIThread.RunJobs(); + return topLevel.PlatformImpl as IHeadlessWindow ?? + throw new InvalidOperationException("TopLevel must be a headless window."); + } +} diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs index e1c09107c8..35f2774965 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs @@ -271,12 +271,18 @@ namespace Avalonia.Headless Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); } - void IHeadlessWindow.MouseDown(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseDown(Point point, MouseButton button, RawInputModifiers modifiers) { Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, - button == 0 ? RawPointerEventType.LeftButtonDown : - button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, - point, modifiers)); + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonDown, + MouseButton.Right => RawPointerEventType.RightButtonDown, + MouseButton.Middle => RawPointerEventType.MiddleButtonDown, + MouseButton.XButton1 => RawPointerEventType.XButton1Down, + MouseButton.XButton2 => RawPointerEventType.XButton2Down, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseMove(Point point, RawInputModifiers modifiers) @@ -285,12 +291,18 @@ namespace Avalonia.Headless RawPointerEventType.Move, point, modifiers)); } - void IHeadlessWindow.MouseUp(Point point, int button, RawInputModifiers modifiers) + void IHeadlessWindow.MouseUp(Point point, MouseButton button, RawInputModifiers modifiers) { Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!, - button == 0 ? RawPointerEventType.LeftButtonUp : - button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, - point, modifiers)); + button switch + { + MouseButton.Left => RawPointerEventType.LeftButtonUp, + MouseButton.Right => RawPointerEventType.RightButtonUp, + MouseButton.Middle => RawPointerEventType.MiddleButtonUp, + MouseButton.XButton1 => RawPointerEventType.XButton1Up, + MouseButton.XButton2 => RawPointerEventType.XButton2Up, + _ => RawPointerEventType.Move, + }, point, modifiers)); } void IHeadlessWindow.MouseWheel(Point point, Vector delta, RawInputModifiers modifiers) diff --git a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs index 3391a103d1..f3da2335bc 100644 --- a/src/Headless/Avalonia.Headless/IHeadlessWindow.cs +++ b/src/Headless/Avalonia.Headless/IHeadlessWindow.cs @@ -6,15 +6,15 @@ using Avalonia.Utilities; namespace Avalonia.Headless { - public interface IHeadlessWindow + internal interface IHeadlessWindow { Bitmap? GetLastRenderedFrame(); void KeyPress(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers); - void MouseDown(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseDown(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); - void MouseUp(Point point, int button, RawInputModifiers modifiers = RawInputModifiers.None); + void MouseUp(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseWheel(Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None); - void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers); + void DragDrop(Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None); } } diff --git a/tests/Avalonia.Headless.UnitTests/InputTests.cs b/tests/Avalonia.Headless.UnitTests/InputTests.cs index c4d9f1a517..3c0ecbfdb7 100644 --- a/tests/Avalonia.Headless.UnitTests/InputTests.cs +++ b/tests/Avalonia.Headless.UnitTests/InputTests.cs @@ -1,9 +1,10 @@ using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class InputTests { @@ -27,10 +28,8 @@ public class InputTests }; window.Show(); - Dispatcher.UIThread.RunJobs(); - - ((IHeadlessWindow)window.PlatformImpl!).MouseDown(new Point(50, 50), 0); - ((IHeadlessWindow)window.PlatformImpl!).MouseUp(new Point(50, 50), 0); + window.MouseDown(new Point(50, 50), MouseButton.Left); + window.MouseUp(new Point(50, 50), MouseButton.Left); Assert.True(buttonClicked); } diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index 67b99541d6..33f15bce1a 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -1,13 +1,10 @@ -using System.IO; -using System.Linq; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; -using Avalonia.Media.Imaging; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class RenderingTests { @@ -32,8 +29,7 @@ public class RenderingTests Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - - var frame = ((IHeadlessWindow)window.PlatformImpl!).GetLastRenderedFrame(); + var frame = window.CaptureRenderedFrame(); Assert.NotNull(frame); } } diff --git a/tests/Avalonia.Headless.UnitTests/TestApplication.cs b/tests/Avalonia.Headless.UnitTests/TestApplication.cs index 0b2927fa29..7bfa0144f3 100644 --- a/tests/Avalonia.Headless.UnitTests/TestApplication.cs +++ b/tests/Avalonia.Headless.UnitTests/TestApplication.cs @@ -1,12 +1,12 @@ -using Avalonia.Headless.XUnit; -using Avalonia.Headless.XUnit.Tests; +using Avalonia.Headless.UnitTests; +using Avalonia.Headless.XUnit; using Avalonia.Themes.Simple; using Xunit; [assembly: AvaloniaTestFramework(typeof(TestApplication))] [assembly: CollectionBehavior(DisableTestParallelization = true)] -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class TestApplication : Application { diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs index efcd2d9081..419ee5519e 100644 --- a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Avalonia.Threading; using Xunit; -namespace Avalonia.Headless.XUnit.Tests; +namespace Avalonia.Headless.UnitTests; public class ThreadingTests { From 2450cf05077af78ba09ae86b41f085a53d81f0a3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:16:31 -0500 Subject: [PATCH 07/12] Update VNC project --- .../Avalonia.Headless.Vnc.csproj | 1 + .../HeadlessVncFramebufferSource.cs | 35 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj index 3812fb196d..1f06f28687 100644 --- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj +++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -2,6 +2,7 @@ net6.0;netstandard2.0 + true diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index be0f3579a5..7389a9ba15 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -10,22 +10,28 @@ namespace Avalonia.Headless.Vnc { public class HeadlessVncFramebufferSource : IVncFramebufferSource { - public IHeadlessWindow Window { get; set; } + public Window Window { get; set; } private object _lock = new object(); public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32); private VncButton _previousButtons; public HeadlessVncFramebufferSource(VncServerSession session, Window window) { - Window = window.PlatformImpl as IHeadlessWindow ?? throw new InvalidOperationException("Invalid window parameter"); + Window = window; session.PointerChanged += (_, args) => { var pt = new Point(args.X, args.Y); var buttons = (VncButton)args.PressedButtons; - int TranslateButton(VncButton vncButton) => - vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2; + MouseButton TranslateButton(VncButton vncButton) => + vncButton switch + { + VncButton.Left => MouseButton.Left, + VncButton.Middle => MouseButton.Middle, + VncButton.Right => MouseButton.Right, + _ => MouseButton.None + }; var modifiers = (RawInputModifiers)(((int)buttons & 7) << 4); @@ -58,34 +64,25 @@ namespace Avalonia.Headless.Vnc private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; - public VncFramebuffer Capture() + public unsafe VncFramebuffer Capture() { lock (_lock) { using (var bmpRef = Window.GetLastRenderedFrame()) { - if (bmpRef?.Item == null) + if (bmpRef == null) return _framebuffer; - var bmp = bmpRef.Item; + var bmp = bmpRef; if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height) { _framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height, VncPixelFormat.RGB32); } - using (var fb = bmp.Lock()) + var buffer = _framebuffer.GetBuffer(); + fixed (byte* bufferPtr = buffer) { - var buf = _framebuffer.GetBuffer(); - if (_framebuffer.Stride == fb.RowBytes) - Marshal.Copy(fb.Address, buf, 0, buf.Length); - else - for (var y = 0; y < fb.Size.Height; y++) - { - var sourceStart = fb.RowBytes * y; - var dstStart = _framebuffer.Stride * y; - var row = fb.Size.Width * 4; - Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row); - } + bmp.CopyPixels(new PixelRect(default, bmp.PixelSize), (IntPtr)bufferPtr, buffer.Length, _framebuffer.Stride); } } } From f61f8f2bb65aad48761f47c825d490a41def4bbb Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:25:24 -0500 Subject: [PATCH 08/12] Minor changes, missing documentation --- .../HeadlessVncFramebufferSource.cs | 3 +- .../AvaloniaTestFrameworkAttribute.cs | 2 +- .../Avalonia.Headless.csproj | 4 ++ .../AvaloniaHeadlessPlatform.cs | 6 ++- .../HeadlessWindowExtensions.cs | 46 +++++++++++++++++-- .../RenderingTests.cs | 2 - 6 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 7389a9ba15..85caeb1ac5 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Platform; using Avalonia.Threading; using RemoteViewing.Vnc; using RemoteViewing.Vnc.Server; @@ -68,7 +69,7 @@ namespace Avalonia.Headless.Vnc { lock (_lock) { - using (var bmpRef = Window.GetLastRenderedFrame()) + using (var bmpRef = Window.CaptureRenderedFrame()) { if (bmpRef == null) return _framebuffer; diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs index d0249a5b25..3eace30805 100644 --- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs +++ b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs @@ -5,7 +5,7 @@ using Xunit.Sdk; namespace Avalonia.Headless.XUnit; /// -/// +/// Sets up global avalonia test framework using avalonia application builder passed as a parameter. /// [TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj index fbfa89f5a4..b626eaeb68 100644 --- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj +++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index 6619b533f6..882f8b842d 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -72,7 +72,11 @@ namespace Avalonia.Headless Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), null); } - + /// + /// Forces renderer to process a rendering timer tick. + /// Use this method before calling . + /// + /// Count of frames to be ticked on the timer. public static void ForceRenderTimerTick(int count = 1) { var timer = AvaloniaLocator.Current.GetService() as RenderTimer; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index ad5b7e680c..5487a7943a 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -3,40 +3,78 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Media.Imaging; +using Avalonia.Platform; using Avalonia.Threading; namespace Avalonia.Headless; public static class HeadlessWindowExtensions { + /// + /// Triggers a renderer timer tick and captures last rendered frame. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) { - var impl = GetImpl(topLevel); + Dispatcher.UIThread.RunJobs(); AvaloniaHeadlessPlatform.ForceRenderTimerTick(); - return impl.GetLastRenderedFrame(); + return topLevel.GetLastRenderedFrame(); } - public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) => - GetImpl(topLevel).GetLastRenderedFrame(); + /// + /// Reads last rendered frame. + /// Note, in order to trigger rendering timer, call method. + /// + /// Bitmap with last rendered frame. Null, if nothing was rendered. + public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) + { + if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) + { + throw new NotSupportedException("To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); + } + + return GetImpl(topLevel).GetLastRenderedFrame(); + } + /// + /// Simulates keyboard press on the headless window/toplevel. + /// public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => GetImpl(topLevel).KeyPress(key, modifiers); + /// + /// Simulates keyboard release on the headless window/toplevel. + /// public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => GetImpl(topLevel).KeyRelease(key, modifiers); + /// + /// Simulates mouse down on the headless window/toplevel. + /// public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + /// + /// Simulates mouse move on the headless window/toplevel. + /// public static void MouseMove(this TopLevel topLevel, Point point, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + /// + /// Simulates mouse up on the headless window/toplevel. + /// public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + /// + /// Simulates mouse wheel on the headless window/toplevel. + /// public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + /// + /// Simulates drag'n'drop target on the headless window/toplevel. + /// public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index 33f15bce1a..bc50686235 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs @@ -27,8 +27,6 @@ public class RenderingTests }; window.Show(); - Dispatcher.UIThread.RunJobs(); - AvaloniaHeadlessPlatform.ForceRenderTimerTick(); var frame = window.CaptureRenderedFrame(); Assert.NotNull(frame); } From c691972f44753cd9b13c0d6f4677a0189c6947cc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:35:29 -0500 Subject: [PATCH 09/12] Add missing InternalsVisibleTo as ScreenHelper was moved --- src/Avalonia.Controls/Avalonia.Controls.csproj | 4 ++++ src/Avalonia.Native/Avalonia.Native.csproj | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index b9e2d3d259..9c4bacbedf 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -18,5 +18,9 @@ + + + + diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 095662a538..e69c39a41e 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -26,8 +26,4 @@ - - - - From 46c5db37258d0b1f4cdfc29f18efbf391553ae50 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 01:53:49 -0500 Subject: [PATCH 10/12] Fixes after VNC check --- .../Properties/launchSettings.json | 6 +++- .../HeadlessVncFramebufferSource.cs | 2 +- .../HeadlessWindowExtensions.cs | 35 +++++++++++++------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Properties/launchSettings.json b/samples/ControlCatalog.NetCore/Properties/launchSettings.json index 5964ca320e..11feb94bb3 100644 --- a/samples/ControlCatalog.NetCore/Properties/launchSettings.json +++ b/samples/ControlCatalog.NetCore/Properties/launchSettings.json @@ -6,6 +6,10 @@ "Dxgi": { "commandName": "Project", "commandLineArgs": "--dxgi" + }, + "VNC": { + "commandName": "Project", + "commandLineArgs": "--vnc" } } -} \ No newline at end of file +} diff --git a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs index 85caeb1ac5..24703003da 100644 --- a/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs +++ b/src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -69,7 +69,7 @@ namespace Avalonia.Headless.Vnc { lock (_lock) { - using (var bmpRef = Window.CaptureRenderedFrame()) + using (var bmpRef = Window.GetLastRenderedFrame()) { if (bmpRef == null) return _framebuffer; diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs index 5487a7943a..8fbc5ec6ef 100644 --- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs +++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs @@ -8,6 +8,9 @@ using Avalonia.Threading; namespace Avalonia.Headless; +/// +/// Set of extension methods to simplify usage of Avalonia.Headless platform. +/// public static class HeadlessWindowExtensions { /// @@ -20,7 +23,7 @@ public static class HeadlessWindowExtensions AvaloniaHeadlessPlatform.ForceRenderTimerTick(); return topLevel.GetLastRenderedFrame(); } - + /// /// Reads last rendered frame. /// Note, in order to trigger rendering timer, call method. @@ -30,7 +33,8 @@ public static class HeadlessWindowExtensions { if (AvaloniaLocator.Current.GetService() is HeadlessPlatformRenderInterface) { - throw new NotSupportedException("To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); + throw new NotSupportedException( + "To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); } return GetImpl(topLevel).GetLastRenderedFrame(); @@ -40,49 +44,58 @@ public static class HeadlessWindowExtensions /// Simulates keyboard press on the headless window/toplevel. /// public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - GetImpl(topLevel).KeyPress(key, modifiers); + RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers); /// /// Simulates keyboard release on the headless window/toplevel. /// public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => - GetImpl(topLevel).KeyRelease(key, modifiers); + RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers); /// /// Simulates mouse down on the headless window/toplevel. /// public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseDown(point, button, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers); /// /// Simulates mouse move on the headless window/toplevel. /// public static void MouseMove(this TopLevel topLevel, Point point, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseMove(point, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers); /// /// Simulates mouse up on the headless window/toplevel. /// public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseUp(point, button, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers); /// /// Simulates mouse wheel on the headless window/toplevel. /// public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, - RawInputModifiers modifiers = RawInputModifiers.None) => GetImpl(topLevel).MouseWheel(point, delta, modifiers); + RawInputModifiers modifiers = RawInputModifiers.None) => + RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers); /// /// Simulates drag'n'drop target on the headless window/toplevel. /// public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => - GetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); + RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); - private static IHeadlessWindow GetImpl(this TopLevel topLevel) + private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel) { Dispatcher.UIThread.RunJobs(); + return GetImpl(topLevel); + } + + private static IHeadlessWindow GetImpl(this TopLevel topLevel) + { return topLevel.PlatformImpl as IHeadlessWindow ?? - throw new InvalidOperationException("TopLevel must be a headless window."); + throw new InvalidOperationException("TopLevel must be a headless window."); } } From 4d415d544912fb5e1f7e61b801e2588f9956bac0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sat, 25 Feb 2023 02:23:03 -0500 Subject: [PATCH 11/12] 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 1bce8de6867a125cb46d8c1b2f5c24a0240d0477 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 21 Apr 2023 22:09:25 -0400 Subject: [PATCH 12/12] Fix merge conflict --- src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs index b83de21ac4..cefb6772c9 100644 --- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs +++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using Avalonia.Controls.Platform; using Avalonia.Reactive; using Avalonia.Input; using Avalonia.Input.Platform;