Browse Source

Merge remote-tracking branch 'origin/master' into msbuild-version-property

pull/11116/head
Dan Walmsley 3 years ago
parent
commit
11bcf2b53e
  1. 22
      Avalonia.sln
  2. 1
      nukebuild/Build.cs
  3. 2
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  4. 6
      samples/ControlCatalog.NetCore/Properties/launchSettings.json
  5. 2
      samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj
  6. 27
      src/Avalonia.Base/Platform/Storage/NoopStorageProvider.cs
  7. 39
      src/Avalonia.Controls/AppBuilder.cs
  8. 6
      src/Avalonia.Controls/Avalonia.Controls.csproj
  9. 3
      src/Avalonia.Controls/Platform/ScreenHelper.cs
  10. 5
      src/Avalonia.Controls/TopLevel.cs
  11. 12
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  12. 13
      src/Avalonia.Headless/Avalonia.Headless.csproj
  13. 4
      src/Avalonia.Native/Avalonia.Native.csproj
  14. 6
      src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
  15. 36
      src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
  16. 5
      src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs
  17. 19
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  18. 35
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
  19. 45
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
  20. 61
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
  21. 18
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  22. 20
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  23. 46
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  24. 73
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  25. 101
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  26. 124
      src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
  27. 10
      src/Headless/Avalonia.Headless/IHeadlessWindow.cs
  28. 19
      tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj
  29. 36
      tests/Avalonia.Headless.UnitTests/InputTests.cs
  30. 33
      tests/Avalonia.Headless.UnitTests/RenderingTests.cs
  31. 24
      tests/Avalonia.Headless.UnitTests/TestApplication.cs
  32. 32
      tests/Avalonia.Headless.UnitTests/ThreadingTests.cs

22
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
@ -260,6 +260,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Desktop", "sam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.iOS", "samples\SafeAreaDemo.iOS\SafeAreaDemo.iOS.csproj", "{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}"
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
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
@ -599,6 +605,14 @@ 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
{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
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
@ -690,6 +704,10 @@ 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}
{F47F8316-4D4B-4026-8EF3-16B2CFDA8119} = {FF237916-7150-496B-89ED-6CA3292896E7}
{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2} = {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}
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}

1
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 => _ => _

2
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -26,7 +26,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />

6
samples/ControlCatalog.NetCore/Properties/launchSettings.json

@ -6,6 +6,10 @@
"Dxgi": {
"commandName": "Project",
"commandLineArgs": "--dxgi"
},
"VNC": {
"commandName": "Project",
"commandLineArgs": "--vnc"
}
}
}
}

2
samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj

@ -19,7 +19,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Dialogs\Avalonia.Dialogs.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
<ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />

27
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<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
}
public override bool CanSave => false;
public override Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromResult<IStorageFile?>(null);
}
public override bool CanPickFolder => false;
public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());
}
}

39
src/Avalonia.Controls/AppBuilder.cs

@ -118,6 +118,43 @@ namespace Avalonia
};
}
/// <summary>
/// Begin configuring an <see cref="Application"/>.
/// Should only be used for testing and design purposes, as it relies on dynamic code.
/// </summary>
/// <param name="entryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
/// <returns>An <see cref="AppBuilder"/> instance. If can't be created, thrown an exception.</returns>
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<Type>(),
null)?
.Invoke(null, Array.Empty<object?>());
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<AppBuilder> callback)
@ -206,7 +243,7 @@ namespace Avalonia
_optionsInitializers += () => { AvaloniaLocator.CurrentMutable.Bind<T>().ToFunc(options); };
return Self;
}
/// <summary>
/// Registers an action that is executed with the current font manager.
/// </summary>

6
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -16,5 +16,11 @@
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Headless.XUnit, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Browser, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

3
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<Screen> screens)
{

5
src/Avalonia.Controls/TopLevel.cs

@ -432,10 +432,13 @@ namespace Avalonia.Controls
IStyleHost IStyleHost.StylingParent => _globalStyles!;
/// <summary>
/// File System storage service used for file pickers and bookmarks.
/// </summary>
public IStorageProvider StorageProvider => _storageProvider
??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
?? PlatformImpl?.TryGetFeature<IStorageProvider>()
?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
?? new NoopStorageProvider();
public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();

12
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<Type>(),
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);

13
src/Avalonia.Headless/Avalonia.Headless.csproj

@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
</Project>

4
src/Avalonia.Native/Avalonia.Native.csproj

@ -26,8 +26,4 @@
<Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
<ItemGroup>
<Compile Remove="..\Shared\ModuleInitializer.cs" />
</ItemGroup>
</Project>

6
src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj → src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@ -9,5 +10,8 @@
<PackageReference Include="Quamotion.RemoteViewing" Version="1.1.21" />
</ItemGroup>
<Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
</Project>

36
src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs → 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;
@ -10,22 +11,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 = (IHeadlessWindow)window.PlatformImpl;
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 +65,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);
}
}
}

5
src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs → 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);
}

19
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit.core" Version="2.4.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
</Project>

35
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<TAppBuilderEntry> : 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<IXunitTestCase> testCases,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
{
executionOptions.SetValue("xunit.execution.DisableParallelization", false);
using (var assemblyRunner = new AvaloniaTestRunner<TAppBuilderEntry>(
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
executionOptions)) await assemblyRunner.RunAsync();
}
}
}

45
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;
/// <summary>
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
/// </summary>
[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute
{
/// <summary>
/// Creates instance of <see cref="AvaloniaTestFrameworkAttribute"/>.
/// </summary>
/// <param name="appBuilderEntryPointType">
/// Parameter from which <see cref="AppBuilder"/> should be created.
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
/// </param>
public AvaloniaTestFrameworkAttribute(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type appBuilderEntryPointType) { }
}
/// <summary>
/// Discoverer implementation for the Avalonia testing framework.
/// </summary>
public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
{
/// <summary>
/// Creates instance of <see cref="AvaloniaTestFrameworkTypeDiscoverer"/>.
/// </summary>
public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _)
{
}
/// <inheritdoc/>
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);
}
}

61
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<TAppBuilderEntry> : XunitTestAssemblyRunner
{
private CancellationTokenSource? _cancellationTokenSource;
public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> 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?.Cancel();
base.Dispose();
}
internal static Task<SynchronizationContext> InitNewApplicationContext(CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<SynchronizationContext>();
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;
}
}

18
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Headless.Vnc, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup>
</Project>

20
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs → src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -1,8 +1,7 @@
using System;
using System.Diagnostics;
using Avalonia.Reactive;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Reactive;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Platform;
@ -14,11 +13,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<TimeSpan> tick)
{
bool cancelled = false;
@ -48,7 +48,7 @@ namespace Avalonia.Headless
public void ForceTick() => _forceTick?.Invoke();
}
class HeadlessWindowingPlatform : IWindowingPlatform
private class HeadlessWindowingPlatform : IWindowingPlatform
{
public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false);
@ -56,7 +56,7 @@ namespace Avalonia.Headless
public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true);
public ITrayIconImpl CreateTrayIcon() => null;
public ITrayIconImpl? CreateTrayIcon() => null;
}
internal static void Initialize(AvaloniaHeadlessPlatformOptions opts)
@ -75,7 +75,11 @@ namespace Avalonia.Headless
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null);
}
/// <summary>
/// Forces renderer to process a rendering timer tick.
/// Use this method before calling <see cref="HeadlessWindowExtensions.GetLastRenderedFrame"/>.
/// </summary>
/// <param name="count">Count of frames to be ticked on the timer.</param>
public static void ForceRenderTimerTick(int count = 1)
{
var timer = AvaloniaLocator.Current.GetService<IRenderTimer>() as RenderTimer;

46
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs → 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;
@ -18,12 +19,13 @@ namespace Avalonia.Headless
public static void Initialize()
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface());
.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface())
.Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub());
}
public IEnumerable<string> InstalledFontNames { get; } = new[] { "Tahoma" };
public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this;
public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsContext) => this;
public bool SupportsIndividualRoundRects => false;
@ -52,7 +54,7 @@ namespace Avalonia.Headless
public IRenderTarget CreateRenderTarget(IEnumerable<object> 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 +132,7 @@ namespace Avalonia.Headless
return new HeadlessGlyphRunStub();
}
class HeadlessGlyphRunStub : IGlyphRunImpl
private class HeadlessGlyphRunStub : IGlyphRunImpl
{
public Rect Bounds => new Rect(new Size(8, 12));
@ -144,7 +146,7 @@ namespace Avalonia.Headless
=> Array.Empty<float>();
}
class HeadlessGeometryStub : IGeometryImpl
private class HeadlessGeometryStub : IGeometryImpl
{
public HeadlessGeometryStub(Rect bounds)
{
@ -157,7 +159,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 +169,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 +193,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 +229,7 @@ namespace Avalonia.Headless
public Matrix Transform { get; }
}
class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
{
public HeadlessStreamingGeometryStub() : base(default)
{
@ -243,7 +245,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 +254,7 @@ namespace Avalonia.Headless
_parent = parent;
}
void Track(Point pt)
private void Track(Point pt)
{
if (_x1 > pt.X)
_x1 = pt.X;
@ -301,7 +303,7 @@ namespace Avalonia.Headless
}
}
class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl
private class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl
{
public Size Size { get; }
@ -363,7 +365,7 @@ namespace Avalonia.Headless
}
}
class HeadlessDrawingContextStub : IDrawingContextImpl
private class HeadlessDrawingContextStub : IDrawingContextImpl
{
public void Dispose()
{
@ -437,16 +439,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 +466,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<IGlyphRunImpl> glyphRun)
public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun)
{
}
@ -484,7 +486,7 @@ namespace Avalonia.Headless
}
}
class HeadlessRenderTarget : IRenderTarget
private class HeadlessRenderTarget : IRenderTarget
{
public void Dispose()
{

73
src/Avalonia.Headless/HeadlessPlatformStubs.cs → 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<string> GetTextAsync()
public Task<string?> 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<string[]> 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<string>();
});
}
public async Task<object> GetDataAsync(string format)
public async Task<object?> 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
{
@ -125,7 +138,7 @@ namespace Avalonia.Headless
public bool TryGetTable(uint tag, out byte[] table)
{
table = null;
table = null!;
return false;
}
@ -141,7 +154,7 @@ namespace Avalonia.Headless
}
}
class HeadlessTextShaperStub : ITextShaperImpl
internal class HeadlessTextShaperStub : ITextShaperImpl
{
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{
@ -153,7 +166,7 @@ namespace Avalonia.Headless
}
}
class HeadlessFontManagerStub : IFontManagerImpl
internal class HeadlessFontManagerStub : IFontManagerImpl
{
public string GetDefaultFontFamilyName()
{
@ -179,17 +192,16 @@ namespace Avalonia.Headless
return true;
}
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo culture, out Typeface typeface)
public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, 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)
{
@ -212,7 +224,7 @@ namespace Avalonia.Headless
}
}
class HeadlessScreensStub : IScreenImpl
internal class HeadlessScreensStub : IScreenImpl
{
public int ScreenCount { get; } = 1;
@ -222,40 +234,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<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>());
}
public override bool CanSave => false;
public override Task<IStorageFile> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromResult<IStorageFile>(null);
}
public override bool CanPickFolder => false;
public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>());
}
}
}

101
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@ -0,0 +1,101 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
namespace Avalonia.Headless;
/// <summary>
/// Set of extension methods to simplify usage of Avalonia.Headless platform.
/// </summary>
public static class HeadlessWindowExtensions
{
/// <summary>
/// Triggers a renderer timer tick and captures last rendered frame.
/// </summary>
/// <returns>Bitmap with last rendered frame. Null, if nothing was rendered.</returns>
public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel)
{
Dispatcher.UIThread.RunJobs();
AvaloniaHeadlessPlatform.ForceRenderTimerTick();
return topLevel.GetLastRenderedFrame();
}
/// <summary>
/// Reads last rendered frame.
/// Note, in order to trigger rendering timer, call <see cref="AvaloniaHeadlessPlatform.ForceRenderTimerTick"/> method.
/// </summary>
/// <returns>Bitmap with last rendered frame. Null, if nothing was rendered.</returns>
public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel)
{
if (AvaloniaLocator.Current.GetService<IPlatformRenderInterface>() 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();
}
/// <summary>
/// Simulates keyboard press on the headless window/toplevel.
/// </summary>
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers);
/// <summary>
/// Simulates keyboard release on the headless window/toplevel.
/// </summary>
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers);
/// <summary>
/// Simulates mouse down on the headless window/toplevel.
/// </summary>
public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers);
/// <summary>
/// Simulates mouse move on the headless window/toplevel.
/// </summary>
public static void MouseMove(this TopLevel topLevel, Point point,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers);
/// <summary>
/// Simulates mouse up on the headless window/toplevel.
/// </summary>
public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers);
/// <summary>
/// Simulates mouse wheel on the headless window/toplevel.
/// </summary>
public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta,
RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers);
/// <summary>
/// Simulates drag'n'drop target on the headless window/toplevel.
/// </summary>
public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data,
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) =>
RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers);
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.");
}
}

124
src/Avalonia.Headless/HeadlessWindowImpl.cs → src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs

@ -17,20 +17,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<IKeyboardDevice>();
_keyboard = AvaloniaLocator.Current.GetRequiredService<IKeyboardDevice>();
_mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
MouseDevice = new MouseDevice(_mousePointer);
ClientSize = new Size(1024, 768);
@ -48,13 +48,13 @@ namespace Avalonia.Headless
public double RenderScaling { get; } = 1;
public double DesktopScaling => RenderScaling;
public IEnumerable<object> Surfaces { get; }
public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; }
public Action<Size, WindowResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; }
public Action<RawInputEventArgs>? Input { get; set; }
public Action<Rect>? Paint { get; set; }
public Action<Size, WindowResizeReason>? Resized { get; set; }
public Action<double>? 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)
{
@ -65,18 +65,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)
@ -101,14 +101,14 @@ namespace Avalonia.Headless
}
public PixelPoint Position { get; set; }
public Action<PixelPoint> PositionChanged { get; set; }
public Action<PixelPoint>? 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, WindowResizeReason reason)
@ -123,7 +123,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)
@ -145,8 +145,8 @@ namespace Avalonia.Headless
public IScreenImpl Screen { get; } = new HeadlessScreensStub();
public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public void SetTitle(string title)
public Action<WindowState>? WindowStateChanged { get; set; }
public void SetTitle(string? title)
{
}
@ -156,7 +156,7 @@ namespace Avalonia.Headless
}
public void SetIcon(IWindowIconImpl icon)
public void SetIcon(IWindowIconImpl? icon)
{
}
@ -171,9 +171,9 @@ namespace Avalonia.Headless
}
public Func<WindowCloseReason, bool> Closing { get; set; }
public Func<WindowCloseReason, bool>? Closing { get; set; }
class FramebufferProxy : ILockedFramebuffer
private class FramebufferProxy : ILockedFramebuffer
{
private readonly ILockedFramebuffer _fb;
private readonly Action _onDispose;
@ -214,28 +214,37 @@ namespace Avalonia.Headless
});
}
public IRef<IWriteableBitmapImpl> GetLastRenderedFrame()
public Bitmap? GetLastRenderedFrame()
{
lock (_sync)
return _lastRenderedFrame?.PlatformImpl?.CloneAs<IWriteableBitmapImpl>();
{
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;
// TODO: Hook recent Popup changes.
IPopupPositioner IPopupImpl.PopupPositioner => null;
IPopupPositioner IPopupImpl.PopupPositioner => null!;
public Size MaxAutoSizeHint => new Size(1920, 1080);
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; }
public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None;
public Action GotInputWhenDisabled { get; set; }
public Action? GotInputWhenDisabled { get; set; }
public bool IsClientAreaExtendedToDecorations => false;
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
public Action<bool>? ExtendClientAreaToDecorationsChanged { get; set; }
public bool NeedsManagedDecorations => false;
@ -243,17 +252,12 @@ 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();
}
if(featureType == typeof(IClipboard))
if(featureType == typeof(IClipboard))
{
return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
}
@ -263,46 +267,58 @@ namespace Avalonia.Headless
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)
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));
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!,
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)
{
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)
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));
Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot!,
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)
{
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<IDragDropDevice>();
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)
@ -310,7 +326,7 @@ namespace Avalonia.Headless
}
public IPopupImpl CreatePopup()
public IPopupImpl? CreatePopup()
{
// TODO: Hook recent Popup changes.
return null;

10
src/Avalonia.Headless/IHeadlessWindow.cs → src/Headless/Avalonia.Headless/IHeadlessWindow.cs

@ -6,15 +6,15 @@ using Avalonia.Utilities;
namespace Avalonia.Headless
{
public interface IHeadlessWindow
internal interface IHeadlessWindow
{
IRef<IWriteableBitmapImpl> 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);
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);
}
}

19
tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\UnitTests.NetFX.props" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
</Project>

36
tests/Avalonia.Headless.UnitTests/InputTests.cs

@ -0,0 +1,36 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Threading;
using Xunit;
namespace Avalonia.Headless.UnitTests;
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();
window.MouseDown(new Point(50, 50), MouseButton.Left);
window.MouseUp(new Point(50, 50), MouseButton.Left);
Assert.True(buttonClicked);
}
}

33
tests/Avalonia.Headless.UnitTests/RenderingTests.cs

@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
using Xunit;
namespace Avalonia.Headless.UnitTests;
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();
var frame = window.CaptureRenderedFrame();
Assert.NotNull(frame);
}
}

24
tests/Avalonia.Headless.UnitTests/TestApplication.cs

@ -0,0 +1,24 @@
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.UnitTests;
public class TestApplication : Application
{
public TestApplication()
{
Styles.Add(new SimpleTheme());
}
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<TestApplication>()
.UseSkia()
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{
UseHeadlessDrawing = false
});
}

32
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.UnitTests;
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;
}
}
Loading…
Cancel
Save