Browse Source

Merge branch 'master' into fixes/DevTools/WithoutApplicationLifetime

Max Katz 2 years ago
parent
commit
13c98937b3
90 changed files with 1385 additions and 1581 deletions
  1. 3 1
      .editorconfig
  2. 0 4
      .nuke/build.schema.json
  3. 34 13
      Avalonia.sln
  4. 3 2
      dirs.proj
  5. 7 6
      nukebuild/Build.cs
  6. 22 17
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  7. 4 4
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  8. 1 0
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  9. 13 1
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  10. 1 0
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  11. 2 2
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  12. 17 0
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  13. 2 6
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  14. 0 1
      src/Avalonia.Controls.DataGrid/Themes/Simple.xaml
  15. 1 1
      src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs
  16. 15 6
      src/Avalonia.Controls/AppBuilder.cs
  17. 0 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  18. 3 1
      src/Avalonia.Controls/Control.cs
  19. 2 2
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  20. 1 1
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  21. 28 25
      src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
  22. 1 1
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  23. 6 3
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  24. 19 0
      src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
  25. 25 0
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs
  26. 116 0
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs
  27. 24 0
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs
  28. 4 4
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  29. 35 0
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs
  30. 126 0
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs
  31. 47 0
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  32. 98 0
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
  33. 4 4
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
  34. 7 14
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
  35. 0 61
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
  36. 37 0
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs
  37. 31 0
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs
  38. 9 0
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  39. 9 8
      src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  40. 27 0
      src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs
  41. 80 19
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  42. 85 24
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  43. 214 0
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  44. 12 9
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  45. 2 2
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  46. 4 3
      tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
  47. 3 2
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  48. 1 0
      tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
  49. 0 285
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  50. 2 5
      tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs
  51. 0 17
      tests/Avalonia.Benchmarks/NullCursorFactory.cs
  52. 0 107
      tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs
  53. 0 149
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  54. 0 27
      tests/Avalonia.Benchmarks/NullThreadingPlatform.cs
  55. 3 2
      tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs
  56. 1 4
      tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs
  57. 1 4
      tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs
  58. 1 4
      tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs
  59. 1 4
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs
  60. 0 3
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  61. 4 3
      tests/Avalonia.Controls.UnitTests/DatePickerTests.cs
  62. 4 3
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  63. 7 6
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  64. 4 3
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  65. 6 5
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  66. 3 2
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
  67. 4 3
      tests/Avalonia.Controls.UnitTests/TimePickerTests.cs
  68. 4 3
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  69. 7 0
      tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs
  70. 31 0
      tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj
  71. 50 12
      tests/Avalonia.Headless.UnitTests/InputTests.cs
  72. 5 2
      tests/Avalonia.Headless.UnitTests/RenderingTests.cs
  73. 0 5
      tests/Avalonia.Headless.UnitTests/TestApplication.cs
  74. 28 7
      tests/Avalonia.Headless.UnitTests/ThreadingTests.cs
  75. 7 0
      tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs
  76. 6 1
      tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj
  77. 2 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  78. 2 1
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  79. 1 0
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs
  80. 1 0
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
  81. 1 0
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  82. 0 62
      tests/Avalonia.UnitTests/ImmediateDispatcher.cs
  83. 0 63
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  84. 0 34
      tests/Avalonia.UnitTests/MockGlyphRun.cs
  85. 0 81
      tests/Avalonia.UnitTests/MockGlyphTypeface.cs
  86. 0 174
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  87. 0 179
      tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
  88. 0 38
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs
  89. 14 20
      tests/Avalonia.UnitTests/TestServices.cs
  90. 0 15
      tests/Avalonia.UnitTests/TextTestHelper.cs

+ 3 - 1
.editorconfig

@@ -177,7 +177,9 @@ dotnet_diagnostic.CA1828.severity = warning
 dotnet_diagnostic.CA1829.severity = warning
 #CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
 dotnet_diagnostic.CA1847.severity = warning
-#CACA2211:Non-constant fields should not be visible
+#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
+dotnet_diagnostic.CA1854.severity = warning
+#CA2211:Non-constant fields should not be visible
 dotnet_diagnostic.CA2211.severity = error
 
 # Wrapping preferences

+ 0 - 4
.nuke/build.schema.json

@@ -101,10 +101,6 @@
           "type": "boolean",
           "description": "skip-tests"
         },
-        "Solution": {
-          "type": "string",
-          "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln"
-        },
         "Target": {
           "type": "array",
           "description": "List of targets to be invoked. Default is '{default_target}'",

+ 34 - 13
Avalonia.sln

@@ -246,8 +246,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
@@ -265,7 +263,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headless", "Headless", "{FF
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit", "src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj", "{F47F8316-4D4B-4026-8EF3-16B2CFDA8119}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.UnitTests", "tests\Avalonia.Headless.UnitTests\Avalonia.Headless.UnitTests.csproj", "{3B2405E8-9E7A-46D1-8E2D-EF9ED124C9F2}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit", "src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj", "{ED976634-B118-43F8-8B26-0279C7A7044F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.NUnit.UnitTests", "tests\Avalonia.Headless.NUnit.UnitTests\Avalonia.Headless.NUnit.UnitTests.csproj", "{2999D79E-3C20-4A90-B651-CA7E0AC92D35}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.UnitTests", "tests\Avalonia.Headless.XUnit.UnitTests\Avalonia.Headless.XUnit.UnitTests.csproj", "{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -598,14 +602,14 @@ Global
 		{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU
-		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU
 		{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{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
 		{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
@@ -634,14 +638,26 @@ Global
 		{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Build.0 = Release|Any CPU
 		{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD}.Release|Any CPU.Deploy.0 = Release|Any CPU
+		{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{ED976634-B118-43F8-8B26-0279C7A7044F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{ED976634-B118-43F8-8B26-0279C7A7044F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{ED976634-B118-43F8-8B26-0279C7A7044F}.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
+		{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2999D79E-3C20-4A90-B651-CA7E0AC92D35}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -711,16 +727,21 @@ 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}
 		{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}
 		{F8928267-688E-4A51-989C-612A72446D33} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{4CDAD037-34A2-4CCF-A03A-C6C7B988A572} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{FC956F9A-4C3A-4A1A-ACDD-BB54DCB661DD} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+		{ED976634-B118-43F8-8B26-0279C7A7044F} = {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}
+		{4B8EBBEB-A1AD-49EC-8B69-B93ED15BFA64} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 3 - 2
dirs.proj

@@ -9,10 +9,11 @@
     <ProjectReference Remove="**/*.shproj" />
     <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
     <ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
-    <!-- Exclude iOS, Android and Web samples from build -->
+    <!-- Exclude iOS, Android and Browser samples from build -->
     <ProjectReference Remove="samples/*.iOS/*.csproj" />
     <ProjectReference Remove="samples/*.Android/*.csproj" />
-    <ProjectReference Remove="samples/*.Web/*.csproj" />
+    <ProjectReference Remove="samples/*.Browser/*.csproj" />
+    <ProjectReference Remove="samples/*.Blazor/*.csproj" />
   </ItemGroup>
   <ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows')) OR '$(MSBuildRuntimeType)' != 'Full'">
     <ProjectReference Remove="src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj" />

+ 7 - 6
nukebuild/Build.cs

@@ -35,8 +35,6 @@ using MicroCom.CodeGenerator;
 
 partial class Build : NukeBuild
 {
-    [Solution("Avalonia.sln")] readonly Solution Solution;
-
     BuildParameters Parameters { get; set; }
     protected override void OnBuildInitialized()
     {
@@ -143,10 +141,12 @@ partial class Build : NukeBuild
     void RunCoreTest(string projectName)
     {
         Information($"Running tests from {projectName}");
-        var project = Solution.GetProject(projectName).NotNull("project != null");
+        var project = RootDirectory.GlobFiles(@$"**\{projectName}.csproj").FirstOrDefault()
+            ?? throw new InvalidOperationException($"Project {projectName} doesn't exist");
+
         // Nuke and MSBuild tools have build-in helpers to get target frameworks from the project.
         // Unfortunately, it gets broken with every second SDK update, so we had to do it manually.
-        var fileXml = XDocument.Parse(File.ReadAllText(project.Path));
+        var fileXml = XDocument.Parse(File.ReadAllText(project));
         var targetFrameworks = fileXml.Descendants("TargetFrameworks")
             .FirstOrDefault()?.Value.Split(';').Select(f => f.Trim());
         if (targetFrameworks is null)
@@ -212,7 +212,8 @@ partial class Build : NukeBuild
             RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
             RunCoreTest("Avalonia.Skia.UnitTests");
             RunCoreTest("Avalonia.ReactiveUI.UnitTests");
-            RunCoreTest("Avalonia.Headless.UnitTests");
+            RunCoreTest("Avalonia.Headless.NUnit.UnitTests");
+            RunCoreTest("Avalonia.Headless.XUnit.UnitTests");
         });
 
     Target RunRenderTests => _ => _
@@ -311,7 +312,7 @@ partial class Build : NukeBuild
 
     public static int Main() =>
         RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
-            ? Execute<Build>(x => x.Package)
+            ? Execute<Build>(x => x.RunToolsTests)
             : Execute<Build>(x => x.RunTests);
 
 }

+ 22 - 17
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -729,6 +729,8 @@ namespace Avalonia.Controls
             RowDetailsTemplateProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsTemplateChanged(e));
             RowDetailsVisibilityModeProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnRowDetailsVisibilityModeChanged(e));
             AutoGenerateColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnAutoGenerateColumnsChanged(e));
+
+            FocusableProperty.OverrideDefaultValue<DataGrid>(true);
         }
 
         /// <summary>
@@ -2478,7 +2480,7 @@ namespace Avalonia.Controls
 
             if (_hScrollBar != null)
             {
-                //_hScrollBar.IsTabStop = false;
+                _hScrollBar.IsTabStop = false;
                 _hScrollBar.Maximum = 0.0;
                 _hScrollBar.Orientation = Orientation.Horizontal;
                 _hScrollBar.IsVisible = false;
@@ -2494,7 +2496,7 @@ namespace Avalonia.Controls
 
             if (_vScrollBar != null)
             {
-                //_vScrollBar.IsTabStop = false;
+                _vScrollBar.IsTabStop = false;
                 _vScrollBar.Maximum = 0.0;
                 _vScrollBar.Orientation = Orientation.Vertical;
                 _vScrollBar.IsVisible = false;
@@ -3734,7 +3736,7 @@ namespace Avalonia.Controls
             if (sender is Control editingElement)
             {
                 editingElement.LostFocus -= EditingElement_LostFocus;
-                if (EditingRow != null && EditingColumnIndex != -1)
+                if (EditingRow != null && _editingColumnIndex != -1)
                 {
                     FocusEditingCell(true);
                 }
@@ -4039,18 +4041,22 @@ namespace Avalonia.Controls
                 return true;
             }
 
-            Debug.Assert(EditingRow != null);
+            var editingRow = EditingRow;
+            if (editingRow is null)
+            {
+                return true;
+            }
+
             Debug.Assert(_editingColumnIndex >= 0);
             Debug.Assert(_editingColumnIndex < ColumnsItemsInternal.Count);
             Debug.Assert(_editingColumnIndex == CurrentColumnIndex);
-            Debug.Assert(EditingRow != null && EditingRow.Slot == CurrentSlot);
 
             // Cache these to see if they change later
             int currentSlot = CurrentSlot;
             int currentColumnIndex = CurrentColumnIndex;
 
             // We're ready to start ending, so raise the event
-            DataGridCell editingCell = EditingRow.Cells[_editingColumnIndex];
+            DataGridCell editingCell = editingRow.Cells[_editingColumnIndex];
             var editingElement = editingCell.Content as Control;
             if (editingElement == null)
             {
@@ -4058,7 +4064,7 @@ namespace Avalonia.Controls
             }
             if (raiseEvents)
             {
-                DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, EditingRow, editingElement, editAction);
+                DataGridCellEditEndingEventArgs e = new DataGridCellEditEndingEventArgs(CurrentColumn, editingRow, editingElement, editAction);
                 OnCellEditEnding(e);
                 if (e.Cancel)
                 {
@@ -4112,7 +4118,7 @@ namespace Avalonia.Controls
                     }
                     else
                     {
-                        if (EditingRow != null)
+                        if (editingRow != null)
                         {
                             if (editingCell.IsValid)
                             {
@@ -4120,10 +4126,10 @@ namespace Avalonia.Controls
                                 editingCell.UpdatePseudoClasses();
                             }
 
-                            if (EditingRow.IsValid)
+                            if (editingRow.IsValid)
                             {
-                                EditingRow.IsValid = false;
-                                EditingRow.UpdatePseudoClasses();
+                                editingRow.IsValid = false;
+                                editingRow.UpdatePseudoClasses();
                             }
                         }
 
@@ -4169,22 +4175,22 @@ namespace Avalonia.Controls
                 PopulateCellContent(
                     isCellEdited: !exitEditingMode,
                     dataGridColumn: CurrentColumn,
-                    dataGridRow: EditingRow,
+                    dataGridRow: editingRow,
                     dataGridCell: editingCell);
 
-                EditingRow.InvalidateDesiredHeight();
+                editingRow.InvalidateDesiredHeight();
                 var column = editingCell.OwningColumn;
                 if (column.Width.IsSizeToCells || column.Width.IsAuto)
                 {// Invalidate desired width and force recalculation
                     column.SetWidthDesiredValue(0);
-                    EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
+                    editingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width);
                 }
             }
 
             // We're done, so raise the CellEditEnded event
             if (raiseEvents)
             {
-                OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, EditingRow, editAction));
+                OnCellEditEnded(new DataGridCellEditEndedEventArgs(CurrentColumn, editingRow, editAction));
             }
 
             // There's a chance that somebody reopened this cell for edit within the CellEditEnded handler,
@@ -4427,8 +4433,7 @@ namespace Avalonia.Controls
                     dataGridCell.Focus();
                     success = dataGridCell.ContainsFocusedElement();
                 }
-                //TODO Check
-                //success = dataGridCell.ContainsFocusedElement() ? true : dataGridCell.Focus();
+
                 _focusEditingControl = !success;
             }
             return success;

+ 4 - 4
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@@ -33,6 +33,8 @@ namespace Avalonia.Controls
         {
             PointerPressedEvent.AddClassHandler<DataGridCell>(
                 (x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true);
+            FocusableProperty.OverrideDefaultValue<DataGridCell>(true);
+            IsTabStopProperty.OverrideDefaultValue<DataGridCell>(false);
         }
         public DataGridCell()
         { }
@@ -169,8 +171,7 @@ namespace Avalonia.Controls
             OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
             if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
             {
-                if (!e.Handled)
-                //if (!e.Handled && OwningGrid.IsTabStop)
+                if (!e.Handled && OwningGrid.IsTabStop)
                 {
                     OwningGrid.Focus();
                 }
@@ -190,8 +191,7 @@ namespace Avalonia.Controls
             }
             else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
             {
-                if (!e.Handled)
-                //if (!e.Handled && OwningGrid.IsTabStop)
+                if (!e.Handled && OwningGrid.IsTabStop)
                 {
                     OwningGrid.Focus();
                 }

+ 1 - 0
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -72,6 +72,7 @@ namespace Avalonia.Controls
         {
             AreSeparatorsVisibleProperty.Changed.AddClassHandler<DataGridColumnHeader>((x, e) => x.OnAreSeparatorsVisibleChanged(e));
             PressedMixin.Attach<DataGridColumnHeader>();
+            IsTabStopProperty.OverrideDefaultValue<DataGridColumnHeader>(false);
         }
 
         /// <summary>

+ 13 - 1
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@@ -12,6 +12,7 @@ using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Diagnostics;
 using System.Reflection;
+using Avalonia.Layout;
 
 namespace Avalonia.Controls
 {
@@ -489,7 +490,7 @@ namespace Avalonia.Controls
         {
             DataGridFillerColumn fillerColumn = ColumnsInternal.FillerColumn;
             double totalColumnsWidth = ColumnsInternal.VisibleEdgedColumnsWidth;
-            if (finalWidth > totalColumnsWidth)
+            if (finalWidth - totalColumnsWidth > LayoutHelper.LayoutEpsilon)
             {
                 fillerColumn.FillerWidth = finalWidth - totalColumnsWidth;
             }
@@ -971,6 +972,12 @@ namespace Avalonia.Controls
                         {
                             cx += _negHorizontalOffset;
                             _horizontalOffset -= _negHorizontalOffset;
+                            if (_horizontalOffset < LayoutHelper.LayoutEpsilon)
+                            {
+                                // Snap to zero to avoid trying to partially scroll in first scrolled off column below
+                                _horizontalOffset = 0;
+                            }
+
                             _negHorizontalOffset = 0;
                         }
                         else
@@ -979,6 +986,11 @@ namespace Avalonia.Controls
                             _negHorizontalOffset -= displayWidth - cx;
                             cx = displayWidth;
                         }
+
+                        // Make sure the HorizontalAdjustment is not greater than the new HorizontalOffset
+                        // since it would cause an assertion failure in DataGridCellsPresenter.ShouldDisplayCell
+                        // called by DataGridCellsPresenter.MeasureOverride.
+                        HorizontalAdjustment = Math.Min(HorizontalAdjustment, _horizontalOffset);
                     }
                     // second try to scroll entire columns
                     if (cx < displayWidth && _horizontalOffset > 0)

+ 1 - 0
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@@ -128,6 +128,7 @@ namespace Avalonia.Controls
             DetailsTemplateProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnDetailsTemplateChanged(e));
             AreDetailsVisibleProperty.Changed.AddClassHandler<DataGridRow>((x, e) => x.OnAreDetailsVisibleChanged(e));
             PointerPressedEvent.AddClassHandler<DataGridRow>((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true);
+            IsTabStopProperty.OverrideDefaultValue<DataGridRow>(false);
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@@ -106,6 +106,7 @@ namespace Avalonia.Controls
         {
             SublevelIndentProperty.Changed.AddClassHandler<DataGridRowGroupHeader>((x,e) => x.OnSublevelIndentChanged(e));
             PressedMixin.Attach<DataGridRowGroupHeader>();
+            IsTabStopProperty.OverrideDefaultValue<DataGridRowGroupHeader>(false);
         }
 
         /// <summary>
@@ -301,8 +302,7 @@ namespace Avalonia.Controls
                 }
                 else
                 {
-                    //if (!e.Handled && OwningGrid.IsTabStop)
-                    if (!e.Handled)
+                    if (!e.Handled && OwningGrid.IsTabStop)
                     {
                         OwningGrid.Focus();
                     }

+ 17 - 0
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@@ -1589,6 +1589,23 @@ namespace Avalonia.Controls
             CorrectSlotsAfterDeletion(slot, isRow);
 
             OnRemovedElement(slot, item);
+            
+            // Synchronize CurrentCellCoordinates, CurrentColumn, CurrentColumnIndex, CurrentItem
+            // and CurrentSlot with the currently edited cell, since OnRemovingElement called
+            // SetCurrentCellCore(-1, -1) to temporarily reset the current cell.
+            if (_temporarilyResetCurrentCell &&
+                _editingColumnIndex != -1 &&
+                _previousCurrentItem != null &&
+                EditingRow != null &&
+                EditingRow.Slot != -1)
+            {
+                ProcessSelectionAndCurrency(
+                    columnIndex: _editingColumnIndex,
+                    item: _previousCurrentItem,
+                    backupSlot: this.EditingRow.Slot,
+                    action: DataGridSelectionAction.None,
+                    scrollIntoView: false);
+            }
         }
 
         private void RemoveNonDisplayedRows(int newFirstDisplayedSlot, int newLastDisplayedSlot)

+ 2 - 6
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@@ -82,7 +82,6 @@
       <Setter Property="VerticalContentAlignment" Value="Stretch" />
       <Setter Property="FontSize" Value="15" />
       <Setter Property="MinHeight" Value="32" />
-      <Setter Property="Focusable" Value="False" />
       <Setter Property="Template">
         <ControlTemplate>
           <Border x:Name="CellBorder"
@@ -157,7 +156,6 @@
       <Setter Property="Background" Value="{DynamicResource DataGridColumnHeaderBackgroundBrush}" />
       <Setter Property="HorizontalContentAlignment" Value="Stretch" />
       <Setter Property="VerticalContentAlignment" Value="Center" />
-      <Setter Property="Focusable" Value="False" />
       <Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
       <Setter Property="Padding" Value="12,0,0,0" />
       <Setter Property="FontSize" Value="12" />
@@ -268,7 +266,6 @@
     </ControlTheme>
 
     <ControlTheme x:Key="{x:Type DataGridRowHeader}" TargetType="DataGridRowHeader">
-      <Setter Property="Focusable" Value="False" />
       <Setter Property="SeparatorBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
       <Setter Property="AreSeparatorsVisible" Value="False" />
       <Setter Property="Template">
@@ -310,7 +307,6 @@
     </ControlTheme>
 
     <ControlTheme x:Key="{x:Type DataGridRow}" TargetType="DataGridRow">
-      <Setter Property="Focusable" Value="False" />
       <Setter Property="Background" Value="{Binding $parent[DataGrid].RowBackground}" />
       <Setter Property="Template">
         <ControlTemplate>
@@ -408,7 +404,6 @@
     </ControlTheme>
 
     <ControlTheme x:Key="{x:Type DataGridRowGroupHeader}" TargetType="DataGridRowGroupHeader">
-      <Setter Property="Focusable" Value="False" />
       <Setter Property="Foreground" Value="{DynamicResource DataGridRowGroupHeaderForegroundBrush}" />
       <Setter Property="Background" Value="{DynamicResource DataGridRowGroupHeaderBackgroundBrush}" />
       <Setter Property="FontSize" Value="15" />
@@ -433,7 +428,7 @@
                           BorderThickness="{TemplateBinding BorderThickness}"
                           Background="{TemplateBinding Background}"
                           CornerRadius="{TemplateBinding CornerRadius}"
-                          Focusable="False"
+                          IsTabStop="False"
                           Foreground="{TemplateBinding Foreground}" />
 
             <StackPanel Grid.Column="3"
@@ -503,6 +498,7 @@
       <Setter Property="GridLinesVisibility" Value="None" />
       <Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
       <Setter Property="VerticalGridLinesBrush" Value="{DynamicResource DataGridGridLinesBrush}" />
+      <Setter Property="FocusAdorner" Value="{x:Null}" />
       <Setter Property="DropLocationIndicatorTemplate">
         <Template>
           <Rectangle Fill="{DynamicResource DataGridDropLocationIndicatorBackground}"

+ 0 - 1
src/Avalonia.Controls.DataGrid/Themes/Simple.xaml

@@ -126,7 +126,6 @@
 
     <ControlTheme x:Key="{x:Type DataGridRowHeader}"
                   TargetType="DataGridRowHeader">
-      <Setter Property="Focusable" Value="False" />
       <Setter Property="Template">
         <ControlTemplate>
           <Grid x:Name="PART_Root"

+ 1 - 1
src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs

@@ -54,7 +54,7 @@ namespace Avalonia.Controls.Utils
         /// <returns>True if the currently focused element is within the visual tree of the parent</returns>
         internal static bool ContainsFocusedElement(this Visual element)
         {
-            return (element == null) ? false : element.ContainsChild(FocusManager.Instance.Current as Visual);
+            return element is InputElement { IsKeyboardFocusWithin: true };
         }
     }
 }

+ 15 - 6
src/Avalonia.Controls/AppBuilder.cs

@@ -288,17 +288,26 @@ namespace Avalonia
             }
 
             s_setupWasAlreadyCalled = true;
+            SetupUnsafe();
+        }
+
+        /// <summary>
+        /// Setup method that doesn't check for input initalizers being set.
+        /// Nor 
+        /// </summary>
+        internal void SetupUnsafe()
+        {
             _optionsInitializers?.Invoke();
-            RuntimePlatformServicesInitializer();
-            RenderingSubsystemInitializer();
-            WindowingSubsystemInitializer();
-            AfterPlatformServicesSetupCallback(Self);
-            Instance = _appFactory();
+            RuntimePlatformServicesInitializer?.Invoke();
+            RenderingSubsystemInitializer?.Invoke();
+            WindowingSubsystemInitializer?.Invoke();
+            AfterPlatformServicesSetupCallback?.Invoke(Self);
+            Instance = _appFactory!();
             Instance.ApplicationLifetime = _lifetime;
             AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
             Instance.RegisterServices();
             Instance.Initialize();
-            AfterSetupCallback(Self);
+            AfterSetupCallback?.Invoke(Self);
             Instance.OnFrameworkInitializationCompleted();
         }
     }

+ 0 - 1
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -18,7 +18,6 @@
     <InternalsVisibleTo Include="Avalonia.Diagnostics, 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)" />

+ 3 - 1
src/Avalonia.Controls/Control.cs

@@ -403,7 +403,9 @@ namespace Avalonia.Controls
                 {
                     if (_focusAdorner == null)
                     {
-                        var template = GetValue(FocusAdornerProperty) ?? adornerLayer.DefaultFocusAdorner;
+                        var template = IsSet(FocusAdornerProperty)
+                            ? GetValue(FocusAdornerProperty)
+                            : adornerLayer.DefaultFocusAdorner;
 
                         if (template != null)
                         {

+ 2 - 2
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@@ -237,9 +237,9 @@ namespace Avalonia.Diagnostics.Views
                             else
                             {
                                 //TODO Use Dictionary.Remove(Key, out Value) in netstandard 2.1
-                                if (_frozenPopupStates.ContainsKey(popup))
+                                if (_frozenPopupStates.TryGetValue(popup, out var value))
                                 {
-                                    _frozenPopupStates[popup].Dispose();
+                                    value.Dispose();
                                     _frozenPopupStates.Remove(popup);
                                 }
                             }

+ 1 - 1
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@@ -13,7 +13,7 @@
 
   <ItemGroup>
     <PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" />
-    <PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.6" PrivateAssets="All" />
+    <PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.7" PrivateAssets="All" />
   </ItemGroup>
 
   <ItemGroup>

+ 28 - 25
src/Avalonia.FreeDesktop/DBusPlatformSettings.cs

@@ -1,8 +1,8 @@
 using System;
 using System.Threading.Tasks;
-using Avalonia.Logging;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Tmds.DBus.Protocol;
 using Tmds.DBus.SourceGenerator;
 
 namespace Avalonia.FreeDesktop
@@ -22,39 +22,47 @@ namespace Avalonia.FreeDesktop
 
             _settings = new OrgFreedesktopPortalSettings(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
             _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler);
-            _ = TryGetInitialValueAsync();
+            _ = TryGetInitialValuesAsync();
         }
 
         public override PlatformColorValues GetColorValues() => _lastColorValues ?? base.GetColorValues();
 
-        private async Task TryGetInitialValueAsync()
+        private async Task TryGetInitialValuesAsync()
+        {
+            _themeVariant = await TryGetThemeVariantAsync();
+            _accentColor = await TryGetAccentColorAsync();
+            _lastColorValues = BuildPlatformColorValues();
+            if (_lastColorValues is not null)
+                OnColorValuesChanged(_lastColorValues);
+        }
+
+        private async Task<PlatformThemeVariant?> TryGetThemeVariantAsync()
         {
             try
             {
                 var value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme");
-                _themeVariant = ReadAsColorScheme(value);
+                return ToColorScheme(((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value);
             }
-            catch (Exception ex)
+            catch (DBusException)
             {
-                Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.freedesktop.appearance.color-scheme value", ex);
+                return null;
             }
+        }
 
+        private async Task<Color?> TryGetAccentColorAsync()
+        {
             try
             {
                 var value = await _settings!.ReadAsync("org.kde.kdeglobals.General", "AccentColor");
-                _accentColor = ReadAsAccentColor(value);
+                return ToAccentColor(((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value);
             }
-            catch (Exception ex)
+            catch (DBusException)
             {
-                Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get org.kde.kdeglobals.General.AccentColor value", ex);
+                return null;
             }
-
-            _lastColorValues = BuildPlatformColorValues();
-            if (_lastColorValues is not null)
-                OnColorValuesChanged(_lastColorValues);
         }
 
-        private void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple)
+        private async void SettingsChangedHandler(Exception? exception, (string @namespace, string key, DBusVariantItem value) valueTuple)
         {
             if (exception is not null)
                 return;
@@ -62,12 +70,8 @@ namespace Avalonia.FreeDesktop
             switch (valueTuple)
             {
                 case ("org.freedesktop.appearance", "color-scheme", { } colorScheme):
-                    _themeVariant = ReadAsColorScheme(colorScheme);
-                    _lastColorValues = BuildPlatformColorValues();
-                    OnColorValuesChanged(_lastColorValues!);
-                    break;
-                case ("org.kde.kdeglobals.General", "AccentColor", { } accentColor):
-                    _accentColor = ReadAsAccentColor(accentColor);
+                    _themeVariant = ToColorScheme((colorScheme.Value as DBusUInt32Item)!.Value);
+                    _accentColor = await TryGetAccentColorAsync();
                     _lastColorValues = BuildPlatformColorValues();
                     OnColorValuesChanged(_lastColorValues!);
                     break;
@@ -85,21 +89,20 @@ namespace Avalonia.FreeDesktop
             return null;
         }
 
-        private static PlatformThemeVariant ReadAsColorScheme(DBusVariantItem value)
+        private static PlatformThemeVariant ToColorScheme(uint value)
         {
             /*
             <member>0: No preference</member>
             <member>1: Prefer dark appearance</member>
             <member>2: Prefer light appearance</member>
             */
-            var isDark = ((value.Value as DBusVariantItem)!.Value as DBusUInt32Item)!.Value == 1;
+            var isDark = value == 1;
             return isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light;
         }
 
-        private static Color ReadAsAccentColor(DBusVariantItem value)
+        private static Color ToAccentColor(string value)
         {
-            var colorStr = ((value.Value as DBusVariantItem)!.Value as DBusStringItem)!.Value;
-            var rgb = colorStr.Split(',');
+            var rgb = value.Split(',');
             return new Color(255, byte.Parse(rgb[0]), byte.Parse(rgb[1]), byte.Parse(rgb[2]));
         }
     }

+ 1 - 1
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@@ -21,7 +21,7 @@ namespace Avalonia.FreeDesktop
             var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
             try
             {
-                await dbusFileChooser.GetVersionAsync();
+                await dbusFileChooser.GetVersionPropertyAsync();
             }
             catch
             {

+ 6 - 3
src/Avalonia.Remote.Protocol/MetsysBson.cs

@@ -715,7 +715,8 @@ namespace Metsys.Bson
 
         public MagicProperty FindProperty(string name)
         {
-            return _properties.ContainsKey(name) ? _properties[name] : null;
+            _properties.TryGetValue(name, out var property);
+            return property;
         }
 
         public static TypeHelper GetHelperForType(Type type)
@@ -1196,7 +1197,9 @@ namespace Metsys.Bson
                 }
                 object container = null;
                 var property = typeHelper.FindProperty(name);
-                var propertyType = property != null ? property.Type : _typeMap.ContainsKey(storageType) ? _typeMap[storageType] : typeof(object);
+                var propertyType = property?.Type
+                    ?? (_typeMap.TryGetValue(storageType, out var type1) ? type1 : null)
+                    ?? typeof(object);
                 if (property != null && property.Setter == null)
                 {
                     container = property.Getter(instance);
@@ -1588,7 +1591,7 @@ namespace Metsys.Bson.Configuration
             {
                 return property;
             }
-            return map.ContainsKey(property) ? map[property] : property;
+            return map.TryGetValue(property, out var value) ? value : property;
         }
 
         public void AddIgnore<T>(string name)

+ 19 - 0
src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
+    <IsTestProject>false</IsTestProject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <!-- Use lower minor version, as it is supposed to be compatible  -->
+    <PackageReference Include="NUnit" Version="3.13.0" />
+  </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>

+ 25 - 0
src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using NUnit.Framework;
+using NUnit.Framework.Interfaces;
+using NUnit.Framework.Internal.Commands;
+
+namespace Avalonia.Headless.NUnit;
+
+/// <summary>
+/// Identifies a nunit test that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+/// </summary>
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+public sealed class AvaloniaTestAttribute : TestCaseAttribute, IWrapSetUpTearDown
+{
+    public TestCommand Wrap(TestCommand command)
+    {
+        var session =
+            HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly);
+
+        return AvaloniaTestMethodCommand.ProcessCommand(session, command);
+    }
+}

+ 116 - 0
src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs

@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using NUnit.Framework.Interfaces;
+using NUnit.Framework.Internal;
+using NUnit.Framework.Internal.Commands;
+
+namespace Avalonia.Headless.NUnit;
+
+internal class AvaloniaTestMethodCommand : TestCommand
+{
+    private readonly HeadlessUnitTestSession _session;
+    private readonly TestCommand _innerCommand;
+    private readonly List<Action> _beforeTest;
+    private readonly List<Action> _afterTest;
+
+    // There are multiple problems with NUnit integration at the moment when we wrote this integration.
+    // NUnit doesn't have extensibility API for running on custom dispatcher/sync-context.
+    // See https://github.com/nunit/nunit/issues/2917 https://github.com/nunit/nunit/issues/2774
+    // To workaround that we had to replace inner TestMethodCommand with our own implementation while keeping original hierarchy of commands.
+    // Which will respect proper async/await awaiting code that works with our session and can be block-awaited to fit in NUnit.
+    // Also, we need to push BeforeTest/AfterTest callbacks to the very same session call.
+    // I hope there will be a better solution without reflection, but for now that's it.
+    private static FieldInfo s_innerCommand = typeof(DelegatingTestCommand)
+        .GetField("innerCommand", BindingFlags.Instance | BindingFlags.NonPublic)!;
+    private static FieldInfo s_beforeTest = typeof(BeforeAndAfterTestCommand)
+        .GetField("BeforeTest", BindingFlags.Instance | BindingFlags.NonPublic)!;
+    private static FieldInfo s_afterTest = typeof(BeforeAndAfterTestCommand)
+        .GetField("AfterTest", BindingFlags.Instance | BindingFlags.NonPublic)!;
+    
+    private AvaloniaTestMethodCommand(
+        HeadlessUnitTestSession session,
+        TestCommand innerCommand,
+        List<Action> beforeTest,
+        List<Action> afterTest)
+        : base(innerCommand.Test)
+    {
+        _session = session;
+        _innerCommand = innerCommand;
+        _beforeTest = beforeTest;
+        _afterTest = afterTest;
+    }
+
+    public static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command)
+    {
+        return ProcessCommand(session, command, new List<Action>(), new List<Action>());
+    }
+    
+    private static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command, List<Action> before, List<Action> after)
+    {
+        if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand)
+        {
+            if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> beforeTest)
+            {
+                Action<TestExecutionContext> beforeAction = c => before.Add(() => beforeTest(c));
+                s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction);
+            }
+            if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action<TestExecutionContext> afterTest)
+            {
+                Action<TestExecutionContext> afterAction = c => after.Add(() => afterTest(c));
+                s_afterTest.SetValue(beforeAndAfterTestCommand, afterAction);
+            }
+        }
+        
+        if (command is DelegatingTestCommand delegatingTestCommand
+            && s_innerCommand.GetValue(delegatingTestCommand) is TestCommand inner)
+        {
+            s_innerCommand.SetValue(delegatingTestCommand, ProcessCommand(session, inner, before, after));
+        }
+        else if (command is TestMethodCommand methodCommand)
+        {
+            return new AvaloniaTestMethodCommand(session, methodCommand, before, after);
+        }
+
+        return command;
+    }
+
+    public override TestResult Execute(TestExecutionContext context)
+    {
+        return _session.Dispatch(() => ExecuteTestMethod(context), default).GetAwaiter().GetResult();
+    }
+
+    // Unfortunately, NUnit has issues with custom synchronization contexts, which means we need to add some hacks to make it work.
+    private async Task<TestResult> ExecuteTestMethod(TestExecutionContext context)
+    {
+        _beforeTest.ForEach(a => a());
+        
+        var testMethod = _innerCommand.Test.Method;
+        var methodInfo = testMethod!.MethodInfo;
+
+        var result = methodInfo.Invoke(context.TestObject, _innerCommand.Test.Arguments);
+        // Only Task, non generic ValueTask are supported in async context. No ValueTask<> nor F# tasks.
+        if (result is Task task)
+        {
+            await task;
+        }
+        else if (result is ValueTask valueTask)
+        {
+            await valueTask;
+        }
+
+        context.CurrentResult.SetResult(ResultState.Success);
+
+        if (context.CurrentResult.AssertionResults.Count > 0)
+            context.CurrentResult.RecordTestCompletion();
+
+        if (context.ExecutionStatus != TestExecutionStatus.AbortRequested)
+        {
+            _afterTest.ForEach(a => a());
+        }
+        
+        return context.CurrentResult;
+    }
+}

+ 24 - 0
src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using NUnit.Framework;
+using NUnit.Framework.Interfaces;
+using NUnit.Framework.Internal.Commands;
+
+namespace Avalonia.Headless.NUnit;
+
+/// <summary>
+/// Identifies a nunit theory that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+/// </summary>
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+public sealed class AvaloniaTheoryAttribute : TheoryAttribute, IWrapSetUpTearDown
+{
+    public TestCommand Wrap(TestCommand command)
+    {
+        var session = HeadlessUnitTestSession.GetOrStartForAssembly(command.Test.Method?.MethodInfo.DeclaringType?.Assembly);
+
+        return AvaloniaTestMethodCommand.ProcessCommand(session, command);
+    }
+}

+ 4 - 4
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
-    <ImplicitUsings>enable</ImplicitUsings>
-    <Nullable>enable</Nullable>
+    <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
+    <IsTestProject>false</IsTestProject>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="xunit.core" Version="2.4.2" />
+    <!-- Use lower minor version, as it is supposed to be compatible  -->
+    <PackageReference Include="xunit.core" Version="2.4.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 35 - 0
src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs

@@ -0,0 +1,35 @@
+using System;
+using System.ComponentModel;
+using System.Threading;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Avalonia.Headless.XUnit;
+
+/// <summary>
+/// Identifies an xunit test that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+/// </summary>
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer", "Avalonia.Headless.XUnit")]
+public sealed class AvaloniaFactAttribute : FactAttribute
+{
+    
+}
+
+[EditorBrowsable(EditorBrowsableState.Never)]
+public class AvaloniaUIFactDiscoverer : FactDiscoverer
+{
+    private readonly IMessageSink diagnosticMessageSink;
+    public AvaloniaUIFactDiscoverer(IMessageSink diagnosticMessageSink)
+        : base(diagnosticMessageSink)
+    {
+        this.diagnosticMessageSink = diagnosticMessageSink;
+    }
+
+    protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
+    {
+        return new AvaloniaTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod);
+    }
+}

+ 126 - 0
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs

@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Avalonia.Headless.XUnit;
+
+internal class AvaloniaTestAssemblyRunner : XunitTestAssemblyRunner
+{
+    private HeadlessUnitTestSession? _session;
+
+    public AvaloniaTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases,
+        IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink,
+        ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink,
+        executionMessageSink, executionOptions)
+    {
+    }
+
+    protected override void SetupSyncContext(int maxParallelThreads)
+    {
+        _session = HeadlessUnitTestSession.GetOrStartForAssembly(
+            Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name)));
+        base.SetupSyncContext(1);
+    }
+
+    public override void Dispose()
+    {
+        _session?.Dispose();
+        base.Dispose();
+    }
+
+    protected override Task<RunSummary> RunTestCollectionAsync(
+        IMessageBus messageBus,
+        ITestCollection testCollection,
+        IEnumerable<IXunitTestCase> testCases,
+        CancellationTokenSource cancellationTokenSource)
+    {
+        return new AvaloniaTestCollectionRunner(_session!, testCollection, testCases, DiagnosticMessageSink, messageBus,
+            TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
+    }
+
+    private class AvaloniaTestCollectionRunner : XunitTestCollectionRunner
+    {
+        private readonly HeadlessUnitTestSession _session;
+
+        public AvaloniaTestCollectionRunner(HeadlessUnitTestSession session,
+            ITestCollection testCollection, IEnumerable<IXunitTestCase> testCases,
+            IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer,
+            ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection,
+            testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
+        {
+            _session = session;
+        }
+
+        protected override Task<RunSummary> RunTestClassAsync(
+            ITestClass testClass,
+            IReflectionTypeInfo @class,
+            IEnumerable<IXunitTestCase> testCases)
+        {
+            return new AvaloniaTestClassRunner(_session, testClass, @class, testCases, DiagnosticMessageSink, MessageBus,
+                TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource,
+                CollectionFixtureMappings).RunAsync();
+        }
+    }
+
+    private class AvaloniaTestClassRunner : XunitTestClassRunner
+    {
+        private readonly HeadlessUnitTestSession _session;
+
+        public AvaloniaTestClassRunner(HeadlessUnitTestSession session, ITestClass testClass,
+            IReflectionTypeInfo @class,
+            IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus,
+            ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator,
+            CancellationTokenSource cancellationTokenSource, IDictionary<Type, object> collectionFixtureMappings) :
+            base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator,
+                cancellationTokenSource, collectionFixtureMappings)
+        {
+            _session = session;
+        }
+
+        protected override Task<RunSummary> RunTestMethodAsync(
+            ITestMethod testMethod,
+            IReflectionMethodInfo method,
+            IEnumerable<IXunitTestCase> testCases,
+            object[] constructorArguments)
+        {
+            return new AvaloniaTestMethodRunner(_session, testMethod, Class, method, testCases, DiagnosticMessageSink,
+                MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource,
+                constructorArguments).RunAsync();
+        }
+    }
+
+    private class AvaloniaTestMethodRunner : XunitTestMethodRunner
+    {
+        private readonly HeadlessUnitTestSession _session;
+        private readonly IMessageBus _messageBus;
+        private readonly ExceptionAggregator _aggregator;
+        private readonly CancellationTokenSource _cancellationTokenSource;
+        private readonly object[] _constructorArguments;
+
+        public AvaloniaTestMethodRunner(HeadlessUnitTestSession session, ITestMethod testMethod,
+            IReflectionTypeInfo @class,
+            IReflectionMethodInfo method, IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink,
+            IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource,
+            object[] constructorArguments) : base(testMethod, @class, method, testCases, diagnosticMessageSink,
+            messageBus, aggregator, cancellationTokenSource, constructorArguments)
+        {
+            _session = session;
+            _messageBus = messageBus;
+            _aggregator = aggregator;
+            _cancellationTokenSource = cancellationTokenSource;
+            _constructorArguments = constructorArguments;
+        }
+
+        protected override Task<RunSummary> RunTestCaseAsync(IXunitTestCase testCase)
+        {
+            return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason,
+                _constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator,
+                _cancellationTokenSource);
+        }
+    }
+}

+ 47 - 0
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs

@@ -0,0 +1,47 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Avalonia.Headless.XUnit;
+
+internal class AvaloniaTestCase : XunitTestCase
+{
+    public AvaloniaTestCase(
+        IMessageSink diagnosticMessageSink,
+        TestMethodDisplay defaultMethodDisplay,
+        ITestMethod testMethod,
+        object?[]? testMethodArguments = null)
+        : base(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testMethod, testMethodArguments)
+    {
+    }
+
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
+    public AvaloniaTestCase()
+    {
+    }
+
+    public override Task<RunSummary> RunAsync(
+        IMessageSink diagnosticMessageSink,
+        IMessageBus messageBus,
+        object[] constructorArguments,
+        ExceptionAggregator aggregator,
+        CancellationTokenSource cancellationTokenSource)
+    {
+        var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
+
+        // We need to block the XUnit thread to ensure its concurrency throttle is effective.
+        // See https://github.com/AArnott/Xunit.StaFact/pull/55#issuecomment-826187354 for details.
+        var runSummary = AvaloniaTestCaseRunner
+            .RunTest(session, this, DisplayName, SkipReason, constructorArguments,
+                TestMethodArguments, messageBus, aggregator, cancellationTokenSource)
+            .GetAwaiter().GetResult();
+
+        return Task.FromResult(runSummary);
+    }
+}

+ 98 - 0
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs

@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Threading;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Avalonia.Headless.XUnit;
+
+internal class AvaloniaTestCaseRunner : XunitTestCaseRunner
+{
+    private readonly Action? _onAfterTestInvoked;
+
+    public AvaloniaTestCaseRunner(
+        Action? onAfterTestInvoked,
+        IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments,
+        object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator,
+        CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments,
+        testMethodArguments, messageBus, aggregator, cancellationTokenSource)
+    {
+        _onAfterTestInvoked = onAfterTestInvoked;
+    }
+
+    public static Task<RunSummary> RunTest(HeadlessUnitTestSession session,
+        IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments,
+        object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator,
+        CancellationTokenSource cancellationTokenSource)
+    {
+        var afterTest = () => Dispatcher.UIThread.RunJobs();
+        return session.Dispatch(async () =>
+        {
+            var runner = new AvaloniaTestCaseRunner(afterTest, testCase, displayName,
+                skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource);
+            return await runner.RunAsync();
+        }, cancellationTokenSource.Token);
+    }
+    
+    protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass,
+        object[] constructorArguments,
+        MethodInfo testMethod, object[] testMethodArguments, string skipReason,
+        IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
+        ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
+    {
+        return new AvaloniaTestRunner(_onAfterTestInvoked, test, messageBus, testClass, constructorArguments,
+            testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource);
+    }
+
+    private class AvaloniaTestRunner : XunitTestRunner
+    {
+        private readonly Action? _onAfterTestInvoked;
+
+        public AvaloniaTestRunner(
+            Action? onAfterTestInvoked,
+            ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod,
+            object[] testMethodArguments, string skipReason,
+            IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, ExceptionAggregator aggregator,
+            CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments,
+            testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
+        {
+            _onAfterTestInvoked = onAfterTestInvoked;
+        }
+
+        protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
+        {
+            return new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass, ConstructorArguments,
+                TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync();
+        }
+    }
+
+    private class AvaloniaTestInvoker : XunitTestInvoker
+    {
+        private readonly Action? _onAfterTestInvoked;
+
+        public AvaloniaTestInvoker(
+            Action? onAfterTestInvoked,
+            ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod,
+            object[] testMethodArguments, IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
+            ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus,
+            testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator,
+            cancellationTokenSource)
+        {
+            _onAfterTestInvoked = onAfterTestInvoked;
+        }
+
+        protected override async Task AfterTestMethodInvokedAsync()
+        {
+            await base.AfterTestMethodInvokedAsync();
+
+            // Only here we can execute random code after the test, where exception will be properly handled by the XUnit.
+            if (_onAfterTestInvoked is not null)
+            {
+                Aggregator.Run(_onAfterTestInvoked);
+            }
+        }
+    }
+}

+ 4 - 4
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs

@@ -1,10 +1,11 @@
-using System.Reflection;
+using System.Collections.Generic;
+using System.Reflection;
 using Xunit.Abstractions;
 using Xunit.Sdk;
 
 namespace Avalonia.Headless.XUnit;
 
-internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework
+internal class AvaloniaTestFramework : XunitTestFramework
 {
     public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink)
     {
@@ -26,8 +27,7 @@ internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework
             IMessageSink executionMessageSink,
             ITestFrameworkExecutionOptions executionOptions)
         {
-            executionOptions.SetValue("xunit.execution.DisableParallelization", false);
-            using (var assemblyRunner = new AvaloniaTestRunner<TAppBuilderEntry>(
+            using (var assemblyRunner = new AvaloniaTestAssemblyRunner(
                        TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
                        executionOptions)) await assemblyRunner.RunAsync();
         }

+ 7 - 14
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs

@@ -1,4 +1,6 @@
-using System.Diagnostics.CodeAnalysis;
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 using Xunit.Abstractions;
 using Xunit.Sdk;
 
@@ -7,20 +9,13 @@ namespace Avalonia.Headless.XUnit;
 /// <summary>
 /// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
 /// </summary>
+/// <remarks>
+/// It is an alternative to using [AvaloniaFact] or [AvaloniaTheory] attributes on every test method.
+/// </remarks>
 [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>
@@ -38,8 +33,6 @@ public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
     /// <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);
+        return typeof(AvaloniaTestFramework);
     }
 }

+ 0 - 61
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs

@@ -1,61 +0,0 @@
-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;
-    }
-}

+ 37 - 0
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Avalonia.Headless.XUnit;
+
+/// <summary>
+/// Identifies an xunit theory that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+/// </summary>
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer", "Avalonia.Headless.XUnit")]
+public sealed class AvaloniaTheoryAttribute : TheoryAttribute
+{
+}
+
+[EditorBrowsable(EditorBrowsableState.Never)]
+public class AvaloniaTheoryDiscoverer : TheoryDiscoverer
+{
+    public AvaloniaTheoryDiscoverer(IMessageSink diagnosticMessageSink)
+        : base(diagnosticMessageSink)
+    {
+    }
+
+    protected override IEnumerable<IXunitTestCase> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow)
+    {
+        yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow);
+    }
+
+    protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute)
+    {
+        yield return new AvaloniaTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod);
+    }
+}

+ 31 - 0
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs

@@ -0,0 +1,31 @@
+using System;
+using System.ComponentModel;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Avalonia.Headless.XUnit;
+
+internal class AvaloniaTheoryTestCase : XunitTheoryTestCase
+{
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
+    public AvaloniaTheoryTestCase()
+    {
+    }
+
+    public AvaloniaTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod)
+        : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod)
+    {
+    }
+    
+    public override Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
+    {
+        var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
+
+        return AvaloniaTestCaseRunner
+            .RunTest(session, this, DisplayName, SkipReason, constructorArguments,
+                TestMethodArguments, messageBus, aggregator, cancellationTokenSource);
+    }
+}

+ 9 - 0
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@@ -12,7 +12,16 @@
   <Import Project="..\..\..\build\TrimmingEnable.props" />
   <Import Project="..\..\..\build\NullableEnable.props" />
 
+  <ItemGroup>
+    <Compile Remove="..\..\Shared\ModuleInitializer.cs" />
+  </ItemGroup>
+
   <ItemGroup Label="InternalsVisibleTo">
     <InternalsVisibleTo Include="Avalonia.Headless.Vnc, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
   </ItemGroup>
 </Project>

+ 9 - 8
src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@@ -21,20 +21,21 @@ namespace Avalonia.Headless
             private Action? _forceTick; 
             protected override IDisposable StartCore(Action<TimeSpan> tick)
             {
-                bool cancelled = false;
                 var st = Stopwatch.StartNew();
                 _forceTick = () => tick(st.Elapsed);
-                DispatcherTimer.Run(() =>
+
+                var timer = new DispatcherTimer(DispatcherPriority.Render)
                 {
-                    if (cancelled)
-                        return false;
-                    tick(st.Elapsed);
-                    return !cancelled;
-                }, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render);
+                    Interval = TimeSpan.FromSeconds(1.0 / _framesPerSecond),
+                    Tag = "HeadlessRenderTimer"
+                };
+                timer.Tick += (s, e) => tick(st.Elapsed);
+                timer.Start();
+
                 return Disposable.Create(() =>
                 {
                     _forceTick = null;
-                    cancelled = true;
+                    timer.Stop();
                 });
             }
 

+ 27 - 0
src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Avalonia.Headless;
+
+/// <summary>
+/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
+/// </summary>
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
+public sealed class AvaloniaTestApplicationAttribute : Attribute
+{
+    public Type AppBuilderEntryPointType { get; }
+
+    /// <summary>
+    /// Creates instance of <see cref="AvaloniaTestApplicationAttribute"/>. 
+    /// </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 AvaloniaTestApplicationAttribute(
+        [DynamicallyAccessedMembers(HeadlessUnitTestSession.DynamicallyAccessed)]
+        Type appBuilderEntryPointType)
+    {
+        AppBuilderEntryPointType = appBuilderEntryPointType;
+    }
+}

+ 80 - 19
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -18,7 +18,8 @@ namespace Avalonia.Headless
         {
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface())
-                .Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub());
+                .Bind<IFontManagerImpl>().ToConstant(new HeadlessFontManagerStub())
+                .Bind<ITextShaperImpl>().ToConstant(new HeadlessTextShaperStub());
         }
 
         public IEnumerable<string> InstalledFontNames { get; } = new[] { "Tahoma" };
@@ -128,18 +129,30 @@ namespace Avalonia.Headless
             Point baselineOrigin,
             Rect bounds)
         {
-            return new HeadlessGlyphRunStub();
+            return new HeadlessGlyphRunStub(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds);
         }
 
-        private class HeadlessGlyphRunStub : IGlyphRunImpl
+        internal class HeadlessGlyphRunStub : IGlyphRunImpl
         {
-            public Rect Bounds => new Rect(new Size(8, 12));
+            public HeadlessGlyphRunStub(
+                IGlyphTypeface glyphTypeface,
+                double fontRenderingEmSize,
+                Point baselineOrigin,
+                Rect bounds)
+            {
+                GlyphTypeface = glyphTypeface;
+                FontRenderingEmSize = fontRenderingEmSize;
+                BaselineOrigin = baselineOrigin;
+                Bounds =bounds;
+            }
 
-            public Point BaselineOrigin => new Point(0, 8);
+            public Rect Bounds { get; }
 
-            public IGlyphTypeface GlyphTypeface => new HeadlessGlyphTypefaceImpl();
+            public Point BaselineOrigin { get; }
 
-            public double FontRenderingEmSize => 12;           
+            public IGlyphTypeface GlyphTypeface { get; }
+
+            public double FontRenderingEmSize { get; }           
 
             public void Dispose()
             {
@@ -234,8 +247,11 @@ namespace Avalonia.Headless
 
         private class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl
         {
+            private HeadlessStreamingGeometryContextStub _context;
+            
             public HeadlessStreamingGeometryStub() : base(default)
             {
+                _context = new HeadlessStreamingGeometryContextStub(this);
             }
 
             public IStreamGeometryImpl Clone()
@@ -245,13 +261,18 @@ namespace Avalonia.Headless
 
             public IStreamGeometryContextImpl Open()
             {
-                return new HeadlessStreamingGeometryContextStub(this);
+                return _context;
+            }
+
+            public override bool FillContains(Point point)
+            {
+                return _context.FillContains(point);
             }
 
             private class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl
             {
                 private readonly HeadlessStreamingGeometryStub _parent;
-                private double _x1, _y1, _x2, _y2;
+                private List<Point> points = new List<Point>();
                 public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent)
                 {
                     _parent = parent;
@@ -259,19 +280,30 @@ namespace Avalonia.Headless
 
                 private void Track(Point pt)
                 {
-                    if (_x1 > pt.X)
-                        _x1 = pt.X;
-                    if (_x2 < pt.X)
-                        _x2 = pt.X;
-                    if (_y1 > pt.Y)
-                        _y1 = pt.Y;
-                    if (_y2 < pt.Y)
-                        _y2 = pt.Y;
+                    points.Add(pt);
                 }
 
+                public Rect CalculateBounds()
+                {
+                    var left = double.MaxValue;
+                    var right = double.MinValue;
+                    var top = double.MaxValue;
+                    var bottom = double.MinValue;
+
+                    foreach (var p in points)
+                    {
+                        left = Math.Min(p.X, left);
+                        right = Math.Max(p.X, right);
+                        top = Math.Min(p.Y, top);
+                        bottom = Math.Max(p.Y, bottom);
+                    }
+
+                    return new Rect(new Point(left, top), new Point(right, bottom));
+                }
+                
                 public void Dispose()
                 {
-                    _parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1);
+                    _parent.Bounds = CalculateBounds();
                 }
 
                 public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
@@ -303,6 +335,35 @@ namespace Avalonia.Headless
                 {
 
                 }
+                
+                public bool FillContains(Point point)
+                {
+                    // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html
+                    // to determine if the point is in the geometry (since it will always be convex in this situation)
+                    for (int i = 0; i < points.Count; i++)
+                    {
+                        var a = points[i];
+                        var b = points[(i + 1) % points.Count];
+                        var c = points[(i + 2) % points.Count];
+
+                        Vector v0 = c - a;
+                        Vector v1 = b - a;
+                        Vector v2 = point - a;
+
+                        var dot00 = v0 * v0;
+                        var dot01 = v0 * v1;
+                        var dot02 = v0 * v2;
+                        var dot11 = v1 * v1;
+                        var dot12 = v1 * v2;
+
+
+                        var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
+                        var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
+                        var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
+                        if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
+                    }
+                    return false;
+                }
             }
         }
 
@@ -368,7 +429,7 @@ namespace Avalonia.Headless
             }
         }
 
-        private class HeadlessDrawingContextStub : IDrawingContextImpl
+        internal class HeadlessDrawingContextStub : IDrawingContextImpl
         {
             public void Dispose()
             {

+ 85 - 24
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -1,8 +1,10 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.IO;
 using System.Linq;
+using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
@@ -11,6 +13,7 @@ using Avalonia.Input.Platform;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
 using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage.FileIO;
@@ -82,22 +85,22 @@ namespace Avalonia.Headless
     {
         public FontMetrics Metrics => new FontMetrics
         {
-            DesignEmHeight = 1,
-            Ascent = 8,
-            Descent = 4,
+            DesignEmHeight = 10,
+            Ascent = 2,
+            Descent = 10,
+            IsFixedPitch = true,
             LineGap = 0,
             UnderlinePosition = 2,
             UnderlineThickness = 1,
             StrikethroughPosition = 2,
-            StrikethroughThickness = 1,
-            IsFixedPitch = true
+            StrikethroughThickness = 1
         };
 
         public int GlyphCount => 1337;
 
-        public FontSimulations FontSimulations { get; }
+        public FontSimulations FontSimulations => FontSimulations.None;
 
-        public string FamilyName => "Arial";
+        public string FamilyName => "$Default";
 
         public FontWeight Weight => FontWeight.Normal;
 
@@ -111,24 +114,31 @@ namespace Avalonia.Headless
 
         public ushort GetGlyph(uint codepoint)
         {
-            return 1;
+            return (ushort)codepoint;
         }
 
         public bool TryGetGlyph(uint codepoint, out ushort glyph)
         {
-            glyph = 1;
+            glyph = 8;
 
             return true;
         }
 
         public int GetGlyphAdvance(ushort glyph)
         {
-            return 12;
+            return 8;
         }
 
         public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
         {
-            return glyphs.ToArray().Select(x => (int)x).ToArray();
+            var advances = new int[glyphs.Length];
+
+            for (var i = 0; i < advances.Length; i++)
+            {
+                advances[i] = 8;
+            }
+
+            return advances;
         }
 
         public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
@@ -146,8 +156,8 @@ namespace Avalonia.Headless
         {
             metrics = new GlyphMetrics
             {
-                Height = 10,
-                Width = 8
+                Width = 10,
+                Height = 10
             };
 
             return true;
@@ -161,40 +171,81 @@ namespace Avalonia.Headless
             var typeface = options.Typeface;
             var fontRenderingEmSize = options.FontRenderingEmSize;
             var bidiLevel = options.BidiLevel;
+            var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
+            var textSpan = text.Span;
+            var textStartIndex = TextTestHelper.GetStartCharIndex(text);
+
+            for (var i = 0; i < shapedBuffer.Length;)
+            {
+                var glyphCluster = i + textStartIndex;
+
+                var codepoint = Codepoint.ReadAt(textSpan, i, out var count);
+
+                var glyphIndex = typeface.GetGlyph(codepoint);
 
-            return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
+                for (var j = 0; j < count; ++j)
+                {
+                    shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
+                }
+
+                i += count;
+            }
+
+            return shapedBuffer;
         }
     }
 
     internal class HeadlessFontManagerStub : IFontManagerImpl
     {
+        private readonly string _defaultFamilyName;
+
+        public HeadlessFontManagerStub(string defaultFamilyName = "Default")
+        {
+            _defaultFamilyName = defaultFamilyName;
+        }
+
+        public int TryCreateGlyphTypefaceCount { get; private set; }
+
         public string GetDefaultFontFamilyName()
         {
-            return "Arial";
+            return _defaultFamilyName;
         }
 
-        public string[] GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
         {
-            return new string[] { "Arial" };
+            return new[] { _defaultFamilyName };
         }
 
-        public bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, FontStretch stretch, out IGlyphTypeface glyphTypeface)
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
+            FontStretch fontStretch,
+            CultureInfo? culture, out Typeface fontKey)
         {
-            glyphTypeface= new HeadlessGlyphTypefaceImpl();
+            fontKey = new Typeface(_defaultFamilyName);
 
-            return true;
+            return false;
         }
 
-        public bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
+        public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, 
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
         {
-             glyphTypeface = new HeadlessGlyphTypefaceImpl();
+            glyphTypeface = null;
+
+            TryCreateGlyphTypefaceCount++;
+
+            if (familyName == "Unknown")
+            {
+                return false;
+            }
+
+            glyphTypeface = new HeadlessGlyphTypefaceImpl();
 
             return true;
         }
 
-        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, CultureInfo? culture, out Typeface typeface)
+        public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
         {
-            typeface = new Typeface("Arial", fontStyle, fontWeight, fontStretch);
+            glyphTypeface = new HeadlessGlyphTypefaceImpl();
+
             return true;
         }
     }
@@ -249,4 +300,14 @@ namespace Avalonia.Headless
             return ScreenHelper.ScreenFromWindow(window, AllScreens);
         }
     }
+    
+    internal static class TextTestHelper
+    {
+        public static int GetStartCharIndex(ReadOnlyMemory<char> text)
+        {
+            if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
+                throw new InvalidOperationException("text memory should have been a string");
+            return start;
+        }
+    }
 }

+ 214 - 0
src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Avalonia.Controls.Platform;
+using Avalonia.Metadata;
+using Avalonia.Reactive;
+using Avalonia.Rendering;
+using Avalonia.Threading;
+
+namespace Avalonia.Headless;
+
+/// <summary>
+/// Headless unit test session that needs to be used by the actual testing framework.
+/// All UI tests are supposed to be executed from one of the <see cref="Dispatch"/> methods to keep execution flow on the UI thread.
+/// Disposing unit test session stops internal dispatcher loop. 
+/// </summary>
+[Unstable("This API is experimental and might be unstable. Use on your risk. API might or might not be changed in a minor update.")]
+public sealed class HeadlessUnitTestSession : IDisposable
+{
+    private static readonly ConcurrentDictionary<Assembly, HeadlessUnitTestSession> s_session = new();
+
+    private readonly AppBuilder _appBuilder;
+    private readonly CancellationTokenSource _cancellationTokenSource;
+    private readonly BlockingCollection<Action> _queue;
+    private readonly Task _dispatchTask;
+
+    internal const DynamicallyAccessedMemberTypes DynamicallyAccessed =
+        DynamicallyAccessedMemberTypes.PublicMethods |
+        DynamicallyAccessedMemberTypes.NonPublicMethods |
+        DynamicallyAccessedMemberTypes.PublicParameterlessConstructor;
+
+    private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
+        BlockingCollection<Action> queue, Task dispatchTask)
+    {
+        _appBuilder = appBuilder;
+        _cancellationTokenSource = cancellationTokenSource;
+        _queue = queue;
+        _dispatchTask = dispatchTask;
+    }
+
+    /// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
+    public Task Dispatch(Action action, CancellationToken cancellationToken)
+    {
+        return Dispatch(() =>
+        {
+            action();
+            return Task.FromResult(0);
+        }, cancellationToken);
+    }
+
+    /// <inheritdoc cref="Dispatch{TResult}(Func{Task{TResult}}, CancellationToken)"/>
+    public Task<TResult> Dispatch<TResult>(Func<TResult> action, CancellationToken cancellationToken)
+    {
+        return Dispatch(() => Task.FromResult(action()), cancellationToken);
+    }
+
+    /// <summary>
+    /// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance,
+    /// setting app avalonia services, and runs <see cref="action"/> parameter.
+    /// </summary>
+    /// <param name="action">Action to execute on the dispatcher thread with avalonia services.</param>
+    /// <param name="cancellationToken">Cancellation token to cancel execution.</param>
+    /// <exception cref="ObjectDisposedException">
+    /// If global session was already cancelled and thread killed, it's not possible to dispatch any actions again
+    /// </exception>
+    public Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action, CancellationToken cancellationToken)
+    {
+        if (_cancellationTokenSource.IsCancellationRequested)
+        {
+            throw new ObjectDisposedException("Session was already disposed.");
+        }
+
+        var token = _cancellationTokenSource.Token;
+
+        var tcs = new TaskCompletionSource<TResult>();
+        _queue.Add(() =>
+        {
+            using var application = EnsureApplication();
+
+            var cts = new CancellationTokenSource();
+            using var globalCts = token.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true);
+            using var localCts = cancellationToken.Register(s => ((CancellationTokenSource)s!).Cancel(), cts, true);
+
+            try
+            {
+                var task = action();
+                task.ContinueWith((_, s) => ((CancellationTokenSource)s!).Cancel(), cts,
+                    TaskScheduler.FromCurrentSynchronizationContext());
+
+                if (cts.IsCancellationRequested)
+                {
+                    return;
+                }
+
+                var frame = new DispatcherFrame();
+                using var innerCts = cts.Token.Register(() => frame.Continue = false, true);
+                Dispatcher.UIThread.PushFrame(frame);
+
+                var result = task.GetAwaiter().GetResult();
+                tcs.TrySetResult(result);
+            }
+            catch (Exception ex)
+            {
+                tcs.TrySetException(ex);
+            }
+        });
+        return tcs.Task;
+    }
+
+    private IDisposable EnsureApplication()
+    {
+        var scope = AvaloniaLocator.EnterScope();
+        try
+        {
+            Dispatcher.ResetForUnitTests();
+            _appBuilder.SetupUnsafe();
+        }
+        catch
+        {
+            scope.Dispose();
+            throw;
+        }
+
+        return Disposable.Create(() =>
+        {
+            scope.Dispose();
+            Dispatcher.ResetForUnitTests();
+        });
+    }
+
+    public void Dispose()
+    {
+        _cancellationTokenSource.Cancel();
+        _queue.CompleteAdding();
+        _dispatchTask.Wait();
+        _cancellationTokenSource.Dispose();
+    }
+
+    /// <summary>
+    /// Creates instance of <see cref="HeadlessUnitTestSession"/>. 
+    /// </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>
+    public static HeadlessUnitTestSession StartNew(
+        [DynamicallyAccessedMembers(DynamicallyAccessed)]
+        Type entryPointType)
+    {
+        var tcs = new TaskCompletionSource<HeadlessUnitTestSession>();
+        var cancellationTokenSource = new CancellationTokenSource();
+        var queue = new BlockingCollection<Action>();
+
+        Task? task = null;
+        task = Task.Run(() =>
+        {
+            try
+            {
+                var appBuilder = AppBuilder.Configure(entryPointType);
+
+                // If windowing subsystem wasn't initialized by user, force headless with default parameters.
+                if (appBuilder.WindowingSubsystemName != "Headless")
+                {
+                    appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions());
+                }
+
+                // ReSharper disable once AccessToModifiedClosure
+                tcs.SetResult(new HeadlessUnitTestSession(appBuilder, cancellationTokenSource, queue, task!));
+            }
+            catch (Exception e)
+            {
+                tcs.SetException(e);
+                return;
+            }
+
+            while (!cancellationTokenSource.IsCancellationRequested)
+            {
+                try
+                {
+                    var action = queue.Take(cancellationTokenSource.Token);
+                    action();
+                }
+                catch (OperationCanceledException)
+                {
+                }
+            }
+        });
+
+        return tcs.Task.GetAwaiter().GetResult();
+    }
+
+    /// <summary>
+    /// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing.
+    /// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used. 
+    /// </summary>
+    [UnconditionalSuppressMessage("Trimming", "IL2072",
+        Justification = "AvaloniaTestApplicationAttribute attribute should preserve type information.")]
+    public static HeadlessUnitTestSession GetOrStartForAssembly(Assembly? assembly)
+    {
+        return s_session.GetOrAdd(assembly ?? typeof(HeadlessUnitTestSession).Assembly, a =>
+        {
+            var appBuilderEntryPointType = a.GetCustomAttribute<AvaloniaTestApplicationAttribute>()
+                ?.AppBuilderEntryPointType;
+            return appBuilderEntryPointType is not null ?
+                StartNew(appBuilderEntryPointType) :
+                StartNew(typeof(Application));
+        });
+    }
+}

+ 12 - 9
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@@ -44,53 +44,56 @@ public static class HeadlessWindowExtensions
     /// 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);
+        RunJobsOnImpl(topLevel, w => w.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);
+        RunJobsOnImpl(topLevel, w => w.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);
+        RunJobsOnImpl(topLevel, w => w.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);
+        RunJobsOnImpl(topLevel, w => w.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);
+        RunJobsOnImpl(topLevel, w => w.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);
+        RunJobsOnImpl(topLevel, w => w.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);
+        RunJobsOnImpl(topLevel, w => w.DragDrop(point, type, data, effects, modifiers));
 
-    private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel)
+    private static void RunJobsOnImpl(this TopLevel topLevel, Action<IHeadlessWindow> action)
     {
         Dispatcher.UIThread.RunJobs();
-        return GetImpl(topLevel);
+        AvaloniaHeadlessPlatform.ForceRenderTimerTick();
+        Dispatcher.UIThread.RunJobs();
+        action(GetImpl(topLevel));
+        Dispatcher.UIThread.RunJobs();
     }
 
     private static IHeadlessWindow GetImpl(this TopLevel topLevel)

+ 2 - 2
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@@ -41,9 +41,9 @@ namespace Avalonia.Win32
 
         internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
         {
-            if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32()))
+            if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.TryGetValue(wParam.ToInt32(), out var value))
             {
-                s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam);
+                value.WndProc(hWnd, msg, wParam, lParam);
             }
 
             if (msg == WM_TASKBARCREATED)

+ 4 - 3
tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.UnitTests;
 using Xunit;
@@ -27,7 +28,7 @@ namespace Avalonia.Base.UnitTests.Media
         [Fact]
         public void Should_Throw_When_Default_FamilyName_Is_Null()
         {
-            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null))))
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new HeadlessFontManagerStub(null!))))
             {
                 Assert.Throws<InvalidOperationException>(() => FontManager.Current);
             }
@@ -39,7 +40,7 @@ namespace Avalonia.Base.UnitTests.Media
             var options = new FontManagerOptions { DefaultFamilyName = "MyFont" };
 
             using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
-                .With(fontManagerImpl: new MockFontManagerImpl())))
+                .With(fontManagerImpl: new HeadlessFontManagerStub())))
             {
                 AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options);
 
@@ -62,7 +63,7 @@ namespace Avalonia.Base.UnitTests.Media
             };
 
             using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
-                .With(fontManagerImpl: new MockFontManagerImpl())))
+                .With(fontManagerImpl: new HeadlessFontManagerStub())))
             {
                 AvaloniaLocator.CurrentMutable.Bind<FontManagerOptions>().ToConstant(options);
 

+ 3 - 2
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.UnitTests;
@@ -179,13 +180,13 @@ namespace Avalonia.Base.UnitTests.Media
                 glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
             }
 
-            return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
+            return new GlyphRun(new HeadlessGlyphTypefaceImpl(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
         }
 
         private static IDisposable Start()
         {
             return UnitTestApplication.Start(TestServices.StyledWindow.With(
-                renderInterface: new MockPlatformRenderInterface()));
+                renderInterface: new HeadlessPlatformRenderInterface()));
         }
     }
 }

+ 1 - 0
tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using Avalonia.Base.UnitTests.VisualTree;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Shapes;

+ 0 - 285
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@@ -1,285 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.UnitTests;
-using Avalonia.Media.Imaging;
-using Avalonia.Media.TextFormatting;
-
-namespace Avalonia.Base.UnitTests.VisualTree
-{
-    class MockRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext
-    {
-        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
-        {
-            throw new NotImplementedException();
-        }
-
-        public bool IsLost => false;
-
-        public object TryGetFeature(Type featureType) => null;
-
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IStreamGeometryImpl CreateStreamGeometry()
-        {
-            return new MockStreamGeometry();
-        }
-
-        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(Stream stream)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
-            BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
-            BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(string fileName)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, 
-            IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext)
-        {
-            return this;
-        }
-
-        public bool SupportsIndividualRoundRects { get; set; }
-        public AlphaFormat DefaultAlphaFormat { get; }
-        public PixelFormat DefaultPixelFormat { get; }
-        public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true;
-
-        public IFontManagerImpl CreateFontManager()
-        {
-            return new MockFontManagerImpl();
-        }
-
-        public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat fmt, AlphaFormat alphaFormat)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGeometryImpl CreateEllipseGeometry(Rect rect)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGeometryImpl CreateRectangleGeometry(Rect rect)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
-        {
-            throw new NotImplementedException();
-        }
-
-        class MockStreamGeometry : IStreamGeometryImpl
-        {
-            private MockStreamGeometryContext _impl = new MockStreamGeometryContext();
-            public Rect Bounds
-            {
-                get
-                {
-                    throw new NotImplementedException();
-                }
-            }
-
-            public double ContourLength { get; }
-
-            public IStreamGeometryImpl Clone()
-            {
-                return this;
-            }
-
-            public void Dispose()
-            {
-            }
-
-            public bool FillContains(Point point)
-            {
-                return _impl.FillContains(point);
-            }
-
-            public Rect GetRenderBounds(IPen pen)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IGeometryImpl Intersect(IGeometryImpl geometry)
-            {
-                throw new NotImplementedException();
-            }
-
-            public IStreamGeometryContextImpl Open()
-            {
-                return _impl;
-            }
-
-            public bool StrokeContains(IPen pen, Point point)
-            {
-                throw new NotImplementedException();
-            }
-
-            public ITransformedGeometryImpl WithTransform(Matrix transform)
-            {
-                throw new NotImplementedException();
-            }
-
-            public bool TryGetPointAtDistance(double distance, out Point point)
-            {
-                throw new NotImplementedException();
-            }
-
-            public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
-            {
-                throw new NotImplementedException();
-            }
-
-            public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry)
-            {
-                throw new NotImplementedException();
-            }
-
-            class MockStreamGeometryContext : IStreamGeometryContextImpl
-            {
-                private List<Point> points = new List<Point>();
-                public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
-                {
-                    throw new NotImplementedException();
-                }
-
-                public void BeginFigure(Point startPoint, bool isFilled)
-                {
-                    points.Add(startPoint);
-                }
-
-                public void CubicBezierTo(Point point1, Point point2, Point point3)
-                {
-                    throw new NotImplementedException();
-                }
-
-                public void Dispose()
-                {
-                }
-
-                public void EndFigure(bool isClosed)
-                {
-                }
-
-                public void LineTo(Point point)
-                {
-                    points.Add(point);
-                }
-
-                public void QuadraticBezierTo(Point control, Point endPoint)
-                {
-                    throw new NotImplementedException();
-                }
-
-                public void SetFillRule(FillRule fillRule)
-                {
-                }
-
-                public bool FillContains(Point point)
-                {
-                    // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html
-                    // to determine if the point is in the geometry (since it will always be convex in this situation)
-                    for (int i = 0; i < points.Count; i++)
-                    {
-                        var a = points[i];
-                        var b = points[(i + 1) % points.Count];
-                        var c = points[(i + 2) % points.Count];
-
-                        Vector v0 = c - a;
-                        Vector v1 = b - a;
-                        Vector v2 = point - a;
-
-                        var dot00 = v0 * v0;
-                        var dot01 = v0 * v1;
-                        var dot02 = v0 * v2;
-                        var dot11 = v1 * v1;
-                        var dot12 = v1 * v2;
-
-
-                        var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
-                        var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
-                        var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
-                        if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
-                    }
-                    return false;
-                }
-            }
-        }
-
-        public void Dispose()
-        {
-            
-        }
-    }
-
-}

+ 2 - 5
tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Runtime.CompilerServices;
 using Avalonia.Controls;
+using Avalonia.Headless;
 using Avalonia.Threading;
 using Avalonia.UnitTests;
 using BenchmarkDotNet.Attributes;
@@ -15,11 +16,7 @@ namespace Avalonia.Benchmarks.Layout
 
         public ControlsBenchmark()
         {
-            _app = UnitTestApplication.Start(
-                TestServices.StyledWindow.With(
-                    renderInterface: new NullRenderingPlatform(),
-                    dispatcherImpl: new NullThreadingPlatform(),
-                    standardCursorFactory: new NullCursorFactory()));
+            _app = UnitTestApplication.Start(TestServices.StyledWindow);
 
             _root = new TestRoot(true, null)
             {

+ 0 - 17
tests/Avalonia.Benchmarks/NullCursorFactory.cs

@@ -1,17 +0,0 @@
-using System;
-using Avalonia.Input;
-using Avalonia.Platform;
-
-namespace Avalonia.Benchmarks
-{
-    internal class NullCursorFactory : ICursorFactory
-    {
-        public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new NullCursorImpl();
-        ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new NullCursorImpl();
-
-        private class NullCursorImpl : ICursorImpl
-        {
-            public void Dispose() { }
-        }
-    }
-}

+ 0 - 107
tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs

@@ -1,107 +0,0 @@
-using System;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Rendering.SceneGraph;
-using Avalonia.Utilities;
-using Avalonia.Media.Imaging;
-
-namespace Avalonia.Benchmarks
-{
-    internal class NullDrawingContextImpl : IDrawingContextImpl
-    {
-        public void Dispose()
-        {
-        }
-
-        public Matrix Transform { get; set; }
-
-        public RenderOptions RenderOptions { get; set; }
-
-        public void Clear(Color color)
-        {
-        }
-
-        public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
-        {
-        }
-
-        public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
-        {
-        }
-
-        public void DrawLine(IPen pen, Point p1, Point p2)
-        {
-        }
-
-        public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
-        {
-        }
-
-        public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default)
-        {
-        }
-
-        public void DrawEllipse(IBrush brush, IPen pen, Rect rect)
-        {
-        }
-
-        public void DrawGlyphRun(IBrush foreground, IRef<IGlyphRunImpl> glyphRun)
-        {
-        }
-
-        public IDrawingContextLayerImpl CreateLayer(Size size)
-        {
-            return null;
-        }
-
-        public void PushClip(Rect clip)
-        {
-        }
-
-        public void PushClip(RoundedRect clip)
-        {
-        }
-
-        public void PopClip()
-        {
-        }
-
-        public void PushOpacity(double opacity, Rect bounds)
-        {
-        }
-
-        public void PopOpacity()
-        {
-        }
-
-        public void PushOpacityMask(IBrush mask, Rect bounds)
-        {
-        }
-
-        public void PopOpacityMask()
-        {
-        }
-
-        public void PushGeometryClip(IGeometryImpl clip)
-        {
-        }
-
-        public void PopGeometryClip()
-        {
-        }
-
-        public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
-        {
-        }
-
-        public void PopBitmapBlendMode()
-        {
-        }
-
-        public void Custom(ICustomDrawOperation custom)
-        {
-        }
-
-        public object GetFeature(Type t) => null;
-    }
-}

+ 0 - 149
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@@ -1,149 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.UnitTests;
-using Avalonia.Media.Imaging;
-using Avalonia.Media.TextFormatting;
-using Microsoft.Diagnostics.Runtime;
-
-namespace Avalonia.Benchmarks
-{
-    internal class NullRenderingPlatform : IPlatformRenderInterface, IPlatformRenderInterfaceContext
-    {
-        public IGeometryImpl CreateEllipseGeometry(Rect rect)
-        {
-            return new MockStreamGeometryImpl();
-        }
-
-        public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
-        {
-            return new MockStreamGeometryImpl();
-        }
-
-        public IGeometryImpl CreateRectangleGeometry(Rect rect)
-        {
-            return new MockStreamGeometryImpl();
-        }
-
-        public IStreamGeometryImpl CreateStreamGeometry()
-        {
-            return new MockStreamGeometryImpl();
-        }
-
-        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
-        {
-            throw new NotImplementedException();
-        }
-
-        public bool IsLost => false;
-
-        public object TryGetFeature(Type featureType) => null;
-
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(string fileName)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(Stream stream)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
-            BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
-            BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IFontManagerImpl CreateFontManager()
-        {
-            return new MockFontManagerImpl();
-        }
-
-        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
-        {
-            return new MockStreamGeometryImpl();
-        }
-
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, 
-            IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
-        {
-            return new MockGlyphRun(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds);
-        }
-
-        public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext)
-        {
-            return this;
-        }
-
-        public bool SupportsIndividualRoundRects => true;
-
-        public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
-
-        public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
-        public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true;
-
-        public void Dispose()
-        {
-            
-        }
-    }
-}

+ 0 - 27
tests/Avalonia.Benchmarks/NullThreadingPlatform.cs

@@ -1,27 +0,0 @@
-using System;
-using System.Reactive.Disposables;
-using System.Threading;
-using Avalonia.Platform;
-using Avalonia.Threading;
-
-namespace Avalonia.Benchmarks
-{
-    internal class NullThreadingPlatform : IDispatcherImpl
-    {
-        public void Signal()
-        {
-        }
-        
-        public void UpdateTimer(long? dueTimeInMs)
-        {
-        }
-
-        public bool CurrentThreadIsLoopThread => true;
-
-#pragma warning disable CS0067
-        public event Action Signaled;
-        public event Action Timer;
-        public long Now => 0;
-#pragma warning restore CS0067
-    }
-}

+ 3 - 2
tests/Avalonia.Benchmarks/Rendering/ShapeRendering.cs

@@ -1,4 +1,5 @@
 using Avalonia.Controls.Shapes;
+using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.Platform;
 using BenchmarkDotNet.Attributes;
@@ -21,9 +22,9 @@ namespace Avalonia.Benchmarks.Rendering
             _lineFill = new Line { Fill = new SolidColorBrush() };
             _lineFillAndStroke = new Line { Stroke = new SolidColorBrush(), Fill = new SolidColorBrush() };
 
-            _drawingContext = new PlatformDrawingContext(new NullDrawingContextImpl(), true);
+            _drawingContext = new PlatformDrawingContext(new HeadlessPlatformRenderInterface.HeadlessDrawingContextStub(), true);
 
-            AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new NullRenderingPlatform());
+            AvaloniaLocator.CurrentMutable.Bind<IPlatformRenderInterface>().ToConstant(new HeadlessPlatformRenderInterface());
         }
 
         [Benchmark]

+ 1 - 4
tests/Avalonia.Benchmarks/Styling/ControlTheme_Change.cs

@@ -20,10 +20,7 @@ namespace Avalonia.Benchmarks.Styling
 
         public ControlTheme_Change()
         {
-            _app = UnitTestApplication.Start(
-                TestServices.StyledWindow.With(
-                    renderInterface: new NullRenderingPlatform(),
-                    dispatcherImpl: new NullThreadingPlatform()));
+            _app = UnitTestApplication.Start(TestServices.StyledWindow);
 
             // Simulate an application with a lot of styles by creating a tree of nested panels,
             // each with a bunch of styles applied.

+ 1 - 4
tests/Avalonia.Benchmarks/Styling/ResourceBenchmarks.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Controls;
+using Avalonia.Headless;
 using Avalonia.Platform;
 using Avalonia.Styling;
 using Avalonia.UnitTests;
@@ -20,12 +21,8 @@ namespace Avalonia.Benchmarks.Styling
                 assetLoader: new StandardAssetLoader(),
                 globalClock: new MockGlobalClock(),
                 platform: new AppBuilder().RuntimePlatform,
-                renderInterface: new MockPlatformRenderInterface(),
                 standardCursorFactory: Mock.Of<ICursorFactory>(),
                 theme: () => CreateTheme(),
-                dispatcherImpl: new NullThreadingPlatform(),
-                fontManagerImpl: new MockFontManagerImpl(),
-                textShaperImpl: new MockTextShaperImpl(),
                 windowingPlatform: new MockWindowingPlatform());
 
             return UnitTestApplication.Start(services);

+ 1 - 4
tests/Avalonia.Benchmarks/Styling/Style_Apply_Detach_Complex.cs

@@ -17,10 +17,7 @@ namespace Avalonia.Benchmarks.Styling
 
         public Style_Apply_Detach_Complex()
         {
-            _app = UnitTestApplication.Start(
-                TestServices.StyledWindow.With(
-                    renderInterface: new NullRenderingPlatform(),
-                    dispatcherImpl: new NullThreadingPlatform()));
+            _app = UnitTestApplication.Start(TestServices.StyledWindow);
 
             // Simulate an application with a lot of styles by creating a tree of nested panels,
             // each with a bunch of styles applied.

+ 1 - 4
tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

@@ -30,10 +30,7 @@ public class HugeTextLayout : IDisposable
     {
         _manySmallStrings = Enumerable.Range(0, 1000).Select(_ => RandomString(s_rand.Next(2, 15))).ToArray();
 
-        var testServices = TestServices.StyledWindow.With(
-            renderInterface: new NullRenderingPlatform(),
-            dispatcherImpl: new NullThreadingPlatform(),
-            standardCursorFactory: new NullCursorFactory());
+        var testServices = TestServices.StyledWindow;
 
         if (s_useSkia)
         {

+ 0 - 3
tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs

@@ -45,9 +45,6 @@ namespace Avalonia.Benchmarks.Themes
         private static IDisposable CreateApp()
         {
             var services = new TestServices(
-                renderInterface: new NullRenderingPlatform(),
-                dispatcherImpl: new NullThreadingPlatform(),
-                standardCursorFactory: new NullCursorFactory(),
                 theme: () => LoadFluentTheme());
 
             return UnitTestApplication.Start(services);

+ 4 - 3
tests/Avalonia.Controls.UnitTests/DatePickerTests.cs

@@ -2,6 +2,7 @@
 using System.Linq;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
+using Avalonia.Headless;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
@@ -203,10 +204,10 @@ namespace Avalonia.Controls.UnitTests
         }
 
         private static TestServices Services => TestServices.MockThreadingInterface.With(
-            fontManagerImpl: new MockFontManagerImpl(),
+            fontManagerImpl: new HeadlessFontManagerStub(),
             standardCursorFactory: Mock.Of<ICursorFactory>(),
-            textShaperImpl: new MockTextShaperImpl(),
-            renderInterface: new MockPlatformRenderInterface());
+            textShaperImpl: new HeadlessTextShaperStub(),
+            renderInterface: new HeadlessPlatformRenderInterface());
 
         private static IControlTemplate CreateTemplate()
         {

+ 4 - 3
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
+using Avalonia.Headless;
 using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
@@ -1022,12 +1023,12 @@ namespace Avalonia.Controls.UnitTests
             return UnitTestApplication.Start(
                 TestServices.MockThreadingInterface.With(
                     focusManager: new FocusManager(),
-                    fontManagerImpl: new MockFontManagerImpl(),
+                    fontManagerImpl: new HeadlessFontManagerStub(),
                     keyboardDevice: () => new KeyboardDevice(),
                     keyboardNavigation: new KeyboardNavigationHandler(),
                     inputManager: new InputManager(),
-                    renderInterface: new MockPlatformRenderInterface(),
-                    textShaperImpl: new MockTextShaperImpl()));
+                    renderInterface: new HeadlessPlatformRenderInterface(),
+                    textShaperImpl: new HeadlessTextShaperStub()));
         }
 
         private class ItemsControlWithContainer : ItemsControl, IStyleable

+ 7 - 6
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
+using Avalonia.Headless;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Layout;
@@ -891,16 +892,16 @@ namespace Avalonia.Controls.UnitTests
             keyboardDevice: () => new KeyboardDevice(),
             keyboardNavigation: new KeyboardNavigationHandler(),
             inputManager: new InputManager(),
-            renderInterface: new MockPlatformRenderInterface(),
-            fontManagerImpl: new MockFontManagerImpl(),
-            textShaperImpl: new MockTextShaperImpl(),
+            renderInterface: new HeadlessPlatformRenderInterface(),
+            fontManagerImpl: new HeadlessFontManagerStub(),
+            textShaperImpl: new HeadlessTextShaperStub(),
             standardCursorFactory: Mock.Of<ICursorFactory>());
 
         private static TestServices Services => TestServices.MockThreadingInterface.With(
-            renderInterface: new MockPlatformRenderInterface(),
+            renderInterface: new HeadlessPlatformRenderInterface(),
             standardCursorFactory: Mock.Of<ICursorFactory>(),     
-            textShaperImpl: new MockTextShaperImpl(), 
-            fontManagerImpl: new MockFontManagerImpl());
+            textShaperImpl: new HeadlessTextShaperStub(), 
+            fontManagerImpl: new HeadlessFontManagerStub());
 
         private static IControlTemplate CreateTemplate()
         {

+ 4 - 3
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@@ -10,6 +10,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Selection;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
+using Avalonia.Headless;
 using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Styling;
@@ -1347,12 +1348,12 @@ namespace Avalonia.Controls.UnitTests.Primitives
             return UnitTestApplication.Start(
                 TestServices.MockThreadingInterface.With(
                     focusManager: new FocusManager(),
-                    fontManagerImpl: new MockFontManagerImpl(),
+                    fontManagerImpl: new HeadlessFontManagerStub(),
                     keyboardDevice: () => new KeyboardDevice(),
                     keyboardNavigation: new KeyboardNavigationHandler(),
                     inputManager: new InputManager(),
-                    renderInterface: new MockPlatformRenderInterface(),
-                    textShaperImpl: new MockTextShaperImpl()));
+                    renderInterface: new HeadlessPlatformRenderInterface(),
+                    textShaperImpl: new HeadlessTextShaperStub()));
         }
 
         private class TestSelector : SelectingItemsControl

+ 6 - 5
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -6,6 +6,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
+using Avalonia.Headless;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Layout;
@@ -1103,14 +1104,14 @@ namespace Avalonia.Controls.UnitTests
             keyboardNavigation: new KeyboardNavigationHandler(),
             inputManager: new InputManager(),
             standardCursorFactory: Mock.Of<ICursorFactory>(),
-            textShaperImpl: new MockTextShaperImpl(),
-            fontManagerImpl: new MockFontManagerImpl());
+            textShaperImpl: new HeadlessTextShaperStub(),
+            fontManagerImpl: new HeadlessFontManagerStub());
 
         private static TestServices Services => TestServices.MockThreadingInterface.With(
             standardCursorFactory: Mock.Of<ICursorFactory>(),
-            renderInterface: new MockPlatformRenderInterface(),
-            textShaperImpl: new MockTextShaperImpl(), 
-            fontManagerImpl: new MockFontManagerImpl());
+            renderInterface: new HeadlessPlatformRenderInterface(),
+            textShaperImpl: new HeadlessTextShaperStub(), 
+            fontManagerImpl: new HeadlessFontManagerStub());
 
         private IControlTemplate CreateTemplate()
         {

+ 3 - 2
tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
+using Avalonia.Headless;
 using Avalonia.Markup.Data;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
@@ -87,8 +88,8 @@ namespace Avalonia.Controls.UnitTests
 
         private static TestServices Services => TestServices.MockThreadingInterface.With(
             standardCursorFactory: Mock.Of<ICursorFactory>(),
-            textShaperImpl: new MockTextShaperImpl(),
-            fontManagerImpl: new MockFontManagerImpl());
+            textShaperImpl: new HeadlessTextShaperStub(),
+            fontManagerImpl: new HeadlessFontManagerStub());
 
         private static IControlTemplate CreateTemplate()
         {

+ 4 - 3
tests/Avalonia.Controls.UnitTests/TimePickerTests.cs

@@ -2,6 +2,7 @@
 using System.Linq;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
+using Avalonia.Headless;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
@@ -99,10 +100,10 @@ namespace Avalonia.Controls.UnitTests
         }
 
         private static TestServices Services => TestServices.MockThreadingInterface.With(
-            fontManagerImpl: new MockFontManagerImpl(),
+            fontManagerImpl: new HeadlessFontManagerStub(),
             standardCursorFactory: Mock.Of<ICursorFactory>(),
-            textShaperImpl: new MockTextShaperImpl(),
-            renderInterface: new MockPlatformRenderInterface());
+            textShaperImpl: new HeadlessTextShaperStub(),
+            renderInterface: new HeadlessPlatformRenderInterface());
 
         private static IControlTemplate CreateTemplate()
         {

+ 4 - 3
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data.Core;
+using Avalonia.Headless;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Layout;
@@ -1694,12 +1695,12 @@ namespace Avalonia.Controls.UnitTests
             return UnitTestApplication.Start(
                 TestServices.MockThreadingInterface.With(
                     focusManager: new FocusManager(),
-                    fontManagerImpl: new MockFontManagerImpl(),
+                    fontManagerImpl: new HeadlessFontManagerStub(),
                     keyboardDevice: () => new KeyboardDevice(),
                     keyboardNavigation: new KeyboardNavigationHandler(),
                     inputManager: new InputManager(),
-                    renderInterface: new MockPlatformRenderInterface(),
-                    textShaperImpl: new MockTextShaperImpl()));
+                    renderInterface: new HeadlessPlatformRenderInterface(),
+                    textShaperImpl: new HeadlessTextShaperStub()));
         }
 
         private class Node : NotifyingBase

+ 7 - 0
tests/Avalonia.Headless.NUnit.UnitTests/AssemblyInfo.cs

@@ -0,0 +1,7 @@
+global using NUnit.Framework;
+global using Avalonia.Headless.NUnit;
+
+using Avalonia.Headless;
+using Avalonia.Headless.UnitTests;
+
+[assembly: AvaloniaTestApplication(typeof(TestApplication))]

+ 31 - 0
tests/Avalonia.Headless.NUnit.UnitTests/Avalonia.Headless.NUnit.UnitTests.csproj

@@ -0,0 +1,31 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <IsTestProject>true</IsTestProject>
+    <DefineConstants>$(DefineConstants);NUNIT</DefineConstants>
+  </PropertyGroup>
+
+  <Import Project="..\..\build\UnitTests.NetCore.targets" />
+  <Import Project="..\..\build\UnitTests.NetFX.props" />
+  <Import Project="..\..\build\Moq.props" />
+  <Import Project="..\..\build\Rx.props" />
+  <Import Project="..\..\build\SharedVersion.props" />
+
+  <ItemGroup>
+    <PackageReference Include="NUnit" Version="3.13.3" />
+    <PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\Avalonia.Headless.UnitTests\**\*.cs" />
+    <Compile Remove="..\Avalonia.Headless.UnitTests\bin\**\*.cs" />
+    <Compile Remove="..\Avalonia.Headless.UnitTests\obj\**\*.cs" />
+  </ItemGroup>
+  
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
+    <ProjectReference Include="..\..\src\Headless\Avalonia.Headless.NUnit\Avalonia.Headless.NUnit.csproj" />
+    <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
+  </ItemGroup>
+</Project>

+ 50 - 12
tests/Avalonia.Headless.UnitTests/InputTests.cs

@@ -1,16 +1,46 @@
+using System;
+using System.Reactive.Disposables;
+using System.Threading;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Threading;
-using Xunit;
 
 namespace Avalonia.Headless.UnitTests;
 
 public class InputTests
+#if XUNIT
+    : IDisposable
+#endif
 {
-    [Fact]
+    private Window _window;
+    private Application _setupApp;
+
+#if NUNIT
+    [SetUp]
+    public void SetUp()
+#elif XUNIT
+    public InputTests()
+#endif
+    {
+        _setupApp = Application.Current;
+        Dispatcher.UIThread.VerifyAccess();
+        _window = new Window
+        {
+            Width = 100,
+            Height = 100
+        };
+    }
+    
+#if NUNIT
+    [AvaloniaTest, Timeout(10000)]
+#elif XUNIT
+    [AvaloniaFact(Timeout = 10000)]
+#endif
     public void Should_Click_Button_On_Window()
     {
+        Assert.True(_setupApp == Application.Current);
+        
         var buttonClicked = false;
         var button = new Button
         {
@@ -19,18 +49,26 @@ public class InputTests
         };
 
         button.Click += (_, _) => buttonClicked = true;
+        
+        _window.Content = button;
+        _window.Show();
 
-        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);
 
-        window.MouseDown(new Point(50, 50), MouseButton.Left);
-        window.MouseUp(new Point(50, 50), MouseButton.Left);
-        
         Assert.True(buttonClicked);
     }
+
+#if NUNIT
+    [TearDown]
+    public void TearDown()
+#elif XUNIT
+    public void Dispose()
+#endif
+    {
+        Assert.True(_setupApp == Application.Current);
+
+        Dispatcher.UIThread.VerifyAccess();
+        _window.Close();
+    }
 }

+ 5 - 2
tests/Avalonia.Headless.UnitTests/RenderingTests.cs

@@ -2,13 +2,16 @@
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Threading;
-using Xunit;
 
 namespace Avalonia.Headless.UnitTests;
 
 public class RenderingTests
 {
-    [Fact]
+#if NUNIT
+    [AvaloniaTest, Timeout(10000)]
+#elif XUNIT
+    [AvaloniaFact(Timeout = 10000)]
+#endif
     public void Should_Render_Last_Frame_To_Bitmap()
     {
         var window = new Window

+ 0 - 5
tests/Avalonia.Headless.UnitTests/TestApplication.cs

@@ -1,10 +1,5 @@
 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;
 

+ 28 - 7
tests/Avalonia.Headless.UnitTests/ThreadingTests.cs

@@ -2,31 +2,52 @@
 using System.Threading;
 using System.Threading.Tasks;
 using Avalonia.Threading;
-using Xunit;
 
 namespace Avalonia.Headless.UnitTests;
 
 public class ThreadingTests
 {
-    [Fact]
+#if NUNIT
+    [AvaloniaTest, Timeout(10000)]
+#elif XUNIT
+    [AvaloniaFact(Timeout = 10000)]
+#endif
     public void Should_Be_On_Dispatcher_Thread()
     {
         Dispatcher.UIThread.VerifyAccess();
     }
+
+#if NUNIT
+    [AvaloniaTest(Ignore = "This test should always fail, enable to test if it fails")]
+#elif XUNIT
+    [AvaloniaFact(Skip = "This test should always fail, enable to test if it fails")]
+#endif
+    public void Should_Fail_Test_On_Delayed_Post_When_FlushDispatcher()
+    {
+        Dispatcher.UIThread.Post(() => throw new InvalidOperationException(), DispatcherPriority.Default);
+    }
     
-    [Fact]
-    public async Task DispatcherTimer_Works_On_The_Same_Thread()
+#if NUNIT
+    [AvaloniaTheory, Timeout(10000), TestCase(1), TestCase(10), TestCase(100)]
+#elif XUNIT
+    [AvaloniaTheory(Timeout = 10000), InlineData(1), InlineData(10), InlineData(100)]
+#endif
+    public async Task DispatcherTimer_Works_On_The_Same_Thread(int interval)
     {
+        await Task.Delay(100);
+
         var currentThread = Thread.CurrentThread;
         var tcs = new TaskCompletionSource();
+        var hasCompleted = false;
 
         DispatcherTimer.RunOnce(() =>
         {
-            Assert.Equal(currentThread, Thread.CurrentThread);
+            hasCompleted = currentThread == Thread.CurrentThread;
 
             tcs.SetResult();
-        }, TimeSpan.FromTicks(1));
+        }, TimeSpan.FromTicks(interval));
 
-        await tcs.Task;
+        await tcs.Task; 
+        Assert.True(hasCompleted);
     }
 }

+ 7 - 0
tests/Avalonia.Headless.XUnit.UnitTests/AssemblyInfo.cs

@@ -0,0 +1,7 @@
+global using Xunit;
+global using Avalonia.Headless.XUnit;
+using Avalonia.Headless;
+using Avalonia.Headless.UnitTests;
+using Avalonia.Headless.XUnit;
+
+[assembly: AvaloniaTestApplication(typeof(TestApplication))]

+ 6 - 1
tests/Avalonia.Headless.UnitTests/Avalonia.Headless.UnitTests.csproj → tests/Avalonia.Headless.XUnit.UnitTests/Avalonia.Headless.XUnit.UnitTests.csproj

@@ -1,9 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
-
   <PropertyGroup>
     <TargetFramework>net6.0</TargetFramework>
     <IsTestProject>true</IsTestProject>
+    <DefineConstants>$(DefineConstants);XUNIT</DefineConstants>
   </PropertyGroup>
+
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\UnitTests.NetFX.props" />
   <Import Project="..\..\build\Moq.props" />
@@ -11,6 +12,10 @@
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\SharedVersion.props" />
 
+  <ItemGroup>
+    <Compile Include="..\Avalonia.Headless.UnitTests\**\*.cs" />
+  </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
     <ProjectReference Include="..\..\src\Headless\Avalonia.Headless.XUnit\Avalonia.Headless.XUnit.csproj" />

+ 2 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@@ -402,6 +402,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
         [Fact]
         public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater()
         {
+            GC.KeepAlive(typeof(ItemsRepeater));
+            
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
                 var xaml = @"

+ 2 - 1
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.UnitTests;
 using SkiaSharp;
@@ -90,7 +91,7 @@ namespace Avalonia.Skia.UnitTests.Media
         [Fact]
         public void Should_Only_Try_To_Create_GlyphTypeface_Once()
         {
-            var fontManagerImpl = new MockFontManagerImpl();
+            var fontManagerImpl = new HeadlessFontManagerStub();
 
             using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: fontManagerImpl)))
             {

+ 1 - 0
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Runtime.InteropServices;
+using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Media.TextFormatting.Unicode;

+ 1 - 0
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Runtime.InteropServices;
+using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.UnitTests;

+ 1 - 0
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@@ -10,6 +10,7 @@
     <EmbeddedResource Include="..\Avalonia.UnitTests\Assets\*.ttf" />
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Headless\Avalonia.Headless\Avalonia.Headless.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />

+ 0 - 62
tests/Avalonia.UnitTests/ImmediateDispatcher.cs

@@ -1,62 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Avalonia.Threading;
-
-namespace Avalonia.UnitTests
-{
-    /// <summary>
-    /// Immediately invokes dispatched jobs on the current thread.
-    /// </summary>
-    public class ImmediateDispatcher : IDispatcher
-    {
-        /// <inheritdoc/>
-        public bool CheckAccess()
-        {
-            return true;
-        }
-
-        /// <inheritdoc/>
-        public void Post(Action action, DispatcherPriority priority)
-        {
-            action();
-        }
-
-        /// <inheritdoc/>
-        public void Post(SendOrPostCallback action, object arg, DispatcherPriority priority)
-        {
-            action(arg);
-        }
-
-        /// <inheritdoc/>
-        public Task InvokeAsync(Action action, DispatcherPriority priority)
-        {
-            action();
-            return Task.CompletedTask;
-        }
-
-        /// <inheritdoc/>
-        public Task<TResult> InvokeAsync<TResult>(Func<TResult> function, DispatcherPriority priority)
-        {
-            var result = function();
-            return Task.FromResult(result);
-        }
-
-        /// <inheritdoc/>
-        public Task InvokeAsync(Func<Task> function, DispatcherPriority priority)
-        {
-            return function();
-        }
-        
-        /// <inheritdoc/>
-        public Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, DispatcherPriority priority)
-        {
-            return function();
-        }
-        
-        /// <inheritdoc/>
-        public void VerifyAccess()
-        {
-        }
-    }
-}

+ 0 - 63
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@@ -1,63 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.IO;
-using Avalonia.Media;
-using Avalonia.Platform;
-
-namespace Avalonia.UnitTests
-{
-    public class MockFontManagerImpl : IFontManagerImpl
-    {
-        private readonly string _defaultFamilyName;
-
-        public MockFontManagerImpl(string defaultFamilyName = "Default")
-        {
-            _defaultFamilyName = defaultFamilyName;
-        }
-
-        public int TryCreateGlyphTypefaceCount { get; private set; }
-
-        public string GetDefaultFontFamilyName()
-        {
-            return _defaultFamilyName;
-        }
-
-        string[] IFontManagerImpl.GetInstalledFontFamilyNames(bool checkForUpdates)
-        {
-            return new[] { _defaultFamilyName };
-        }
-
-        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
-            FontStretch fontStretch,
-            CultureInfo culture, out Typeface fontKey)
-        {
-            fontKey = new Typeface(_defaultFamilyName);
-
-            return false;
-        }
-
-        public virtual bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight, 
-            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface glyphTypeface)
-        {
-            glyphTypeface = null;
-
-            TryCreateGlyphTypefaceCount++;
-
-            if (familyName == "Unknown")
-            {
-                return false;
-            }
-
-            glyphTypeface = new MockGlyphTypeface();
-
-            return true;
-        }
-
-        public virtual bool TryCreateGlyphTypeface(Stream stream, out IGlyphTypeface glyphTypeface)
-        {
-            glyphTypeface = new MockGlyphTypeface();
-
-            return true;
-        }
-    }
-}

+ 0 - 34
tests/Avalonia.UnitTests/MockGlyphRun.cs

@@ -1,34 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Media;
-using Avalonia.Platform;
-
-namespace Avalonia.UnitTests
-{
-    public class MockGlyphRun : IGlyphRunImpl
-    {
-        public MockGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, Point baselineOrigin, Rect bounds)
-        {
-            GlyphTypeface = glyphTypeface;
-            FontRenderingEmSize = fontRenderingEmSize;
-            BaselineOrigin = baselineOrigin;
-            Bounds =bounds;
-        }
-
-        public IGlyphTypeface GlyphTypeface { get; }
-
-        public double FontRenderingEmSize { get; }
-
-        public Point BaselineOrigin { get; }
-
-        public Rect Bounds { get; }
-
-        public void Dispose()
-        {
-           
-        }
-
-        public IReadOnlyList<float> GetIntersections(float lowerLimit, float upperLimit) 
-            => Array.Empty<float>();
-    }
-}

+ 0 - 81
tests/Avalonia.UnitTests/MockGlyphTypeface.cs

@@ -1,81 +0,0 @@
-using System;
-using Avalonia.Media;
-
-namespace Avalonia.UnitTests
-{
-    public class MockGlyphTypeface : IGlyphTypeface
-    {
-        public FontMetrics Metrics => new FontMetrics
-        {
-            DesignEmHeight = 10,
-            Ascent = 2,
-            Descent = 10,
-            IsFixedPitch = true
-        };
-
-        public int GlyphCount => 1337;
-
-        public FontSimulations FontSimulations => throw new NotImplementedException();
-
-        public string FamilyName => "$Default";
-
-        public FontWeight Weight { get; }
-
-        public FontStyle Style { get; }
-
-        public FontStretch Stretch { get; }
-
-        public ushort GetGlyph(uint codepoint)
-        {
-            return (ushort)codepoint;
-        }
-
-        public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
-        {
-            return new ushort[codepoints.Length];
-        }
-
-        public int GetGlyphAdvance(ushort glyph)
-        {
-            return 8;
-        }
-
-        public bool TryGetGlyph(uint codepoint, out ushort glyph)
-        {
-            glyph = 8;
-
-            return true;
-        }
-
-        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
-        {
-            var advances = new int[glyphs.Length];
-
-            for (var i = 0; i < advances.Length; i++)
-            {
-                advances[i] = 8;
-            }
-
-            return advances;
-        }
-
-        public void Dispose() { }
-
-        public bool TryGetTable(uint tag, out byte[] table)
-        {
-            table = null;
-            return false;
-        }
-
-        public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics)
-        {
-            metrics = new GlyphMetrics
-            {
-                Width = 10,
-                Height = 10
-            };
-
-            return true;
-        }
-    }
-}

+ 0 - 174
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@@ -1,174 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using Avalonia.Media;
-using Avalonia.Platform;
-using Avalonia.Media.Imaging;
-using Avalonia.Media.TextFormatting;
-using Moq;
-
-namespace Avalonia.UnitTests
-{
-    public class MockPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext
-    {
-        public IGeometryImpl CreateEllipseGeometry(Rect rect)
-        {
-            return Mock.Of<IGeometryImpl>();
-        }
-
-        public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
-        {
-            return Mock.Of<IGeometryImpl>();
-        }
-
-        public IGeometryImpl CreateRectangleGeometry(Rect rect)
-        {
-            return Mock.Of<IGeometryImpl>(x => x.Bounds == rect);
-        }
-
-        class MockRenderTarget : IRenderTarget
-        {
-            public void Dispose()
-            {
-                
-            }
-
-            public IDrawingContextImpl CreateDrawingContext()
-            {
-                var m = new Mock<IDrawingContextImpl>();
-                m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
-                    .Returns(() =>
-                        {
-                            var r = new Mock<IDrawingContextLayerImpl>();
-                            r.Setup(r => r.CreateDrawingContext())
-                                .Returns(CreateDrawingContext());
-                            return r.Object;
-                        }
-                    );
-                return m.Object;
-
-            }
-
-            public bool IsCorrupted => false;
-        }
-        
-        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
-        {
-            return new MockRenderTarget();
-        }
-
-        public bool IsLost => false;
-
-        public object TryGetFeature(Type featureType) => null;
-
-        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
-        {
-            return Mock.Of<IRenderTargetBitmapImpl>();
-        }
-
-        public IStreamGeometryImpl CreateStreamGeometry()
-        {
-            return new MockStreamGeometryImpl();
-        }
-
-        public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children)
-        {
-            return Mock.Of<IGeometryImpl>();
-        }
-
-        public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2)
-        {
-            return Mock.Of<IGeometryImpl>();
-        }
-
-        public IWriteableBitmapImpl CreateWriteableBitmap(
-            PixelSize size,
-            Vector dpi,
-            PixelFormat format,
-            AlphaFormat alphaFormat)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(Stream stream)
-        {
-            return Mock.Of<IBitmapImpl>();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmapToWidth(Stream stream, int width,
-            BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmapToHeight(Stream stream, int height,
-            BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmap(string fileName)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IWriteableBitmapImpl LoadWriteableBitmap(Stream stream)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IBitmapImpl LoadBitmap(string fileName)
-        {
-            return Mock.Of<IBitmapImpl>();
-        }
-
-        public IBitmapImpl LoadBitmapToWidth(Stream stream, int width, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            return Mock.Of<IBitmapImpl>();
-        }
-
-        public IBitmapImpl LoadBitmapToHeight(Stream stream, int height, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            return Mock.Of<IBitmapImpl>();
-        }
-
-        public IBitmapImpl ResizeBitmap(IBitmapImpl bitmapImpl, PixelSize destinationSize, BitmapInterpolationMode interpolationMode = BitmapInterpolationMode.HighQuality)
-        {
-            return Mock.Of<IBitmapImpl>();
-        }
-
-        public IBitmapImpl LoadBitmap(
-            PixelFormat format,
-            AlphaFormat alphaFormat,
-            IntPtr data,
-            PixelSize size,
-            Vector dpi,
-            int stride)
-        {
-            throw new NotImplementedException();
-        }
-
-        public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin, Rect bounds)
-        {
-            return new MockGlyphRun(glyphTypeface, fontRenderingEmSize, baselineOrigin, bounds);
-        }
-
-        public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this;
-
-        public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
-        {
-            return Mock.Of<IGeometryImpl>();
-        }
-
-        public bool SupportsIndividualRoundRects { get; set; }
-
-        public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
-
-        public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
-        public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true;
-
-        public void Dispose()
-        {
-        }
-    }
-}

+ 0 - 179
tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs

@@ -1,179 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.Media;
-using Avalonia.Platform;
-
-namespace Avalonia.UnitTests
-{
-    public class MockStreamGeometryImpl : IStreamGeometryImpl, ITransformedGeometryImpl
-    {
-        private MockStreamGeometryContext _context;
-
-        public MockStreamGeometryImpl()
-        {
-            Transform = Matrix.Identity;
-            _context = new MockStreamGeometryContext();
-        }
-
-        public MockStreamGeometryImpl(Matrix transform)
-        {
-            Transform = transform;
-            _context = new MockStreamGeometryContext();
-        }
-
-        private MockStreamGeometryImpl(Matrix transform, MockStreamGeometryContext context)
-        {
-            Transform = transform;
-            _context = context;
-        }
-
-        public IGeometryImpl SourceGeometry { get; }
-
-        public Rect Bounds => _context.CalculateBounds();
-        
-        public double ContourLength { get; }
-
-        public Matrix Transform { get; }
-
-        public IStreamGeometryImpl Clone()
-        {
-            return this;
-        }
-
-        public void Dispose()
-        {
-        }
-
-        public bool FillContains(Point point)
-        {
-            return _context.FillContains(point);
-        }
-
-        public bool StrokeContains(IPen pen, Point point)
-        {
-            return false;
-        }
-
-        public Rect GetRenderBounds(IPen pen) => Bounds;
-
-        public IGeometryImpl Intersect(IGeometryImpl geometry)
-        {
-            return new MockStreamGeometryImpl(Transform);
-        }
-
-        public IStreamGeometryContextImpl Open()
-        {
-            return _context;
-        }
-
-        public ITransformedGeometryImpl WithTransform(Matrix transform)
-        {
-            return new MockStreamGeometryImpl(transform, _context);
-        }
-
-        public bool TryGetPointAtDistance(double distance, out Point point)
-        {
-            point = new Point();
-            return false;
-        }
-
-        public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
-        {
-            point = new Point();
-            tangent = new Point();
-            return false;
-        }
-
-        public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry)
-        {
-            segmentGeometry = null;
-            return false;
-        }
-
-        class MockStreamGeometryContext : IStreamGeometryContextImpl
-        {
-            private List<Point> points = new List<Point>();
-            public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
-            {
-            }
-
-            public void BeginFigure(Point startPoint, bool isFilled)
-            {
-                points.Add(startPoint);
-            }
-
-            public Rect CalculateBounds()
-            {
-                var left = double.MaxValue;
-                var right = double.MinValue;
-                var top = double.MaxValue;
-                var bottom = double.MinValue;
-
-                foreach (var p in points)
-                {
-                    left = Math.Min(p.X, left);
-                    right = Math.Max(p.X, right);
-                    top = Math.Min(p.Y, top);
-                    bottom = Math.Max(p.Y, bottom);
-                }
-
-                return new Rect(new Point(left, top), new Point(right, bottom));
-            }
-
-            public void CubicBezierTo(Point point1, Point point2, Point point3)
-            {
-            }
-
-            public void Dispose()
-            {
-            }
-
-            public void EndFigure(bool isClosed)
-            {
-            }
-
-            public void LineTo(Point point)
-            {
-                points.Add(point);
-            }
-
-            public void QuadraticBezierTo(Point control, Point endPoint)
-            {
-                throw new NotImplementedException();
-            }
-
-            public void SetFillRule(FillRule fillRule)
-            {
-            }
-
-            public bool FillContains(Point point)
-            {
-                // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html
-                // to determine if the point is in the geometry (since it will always be convex in this situation)
-                for (int i = 0; i < points.Count; i++)
-                {
-                    var a = points[i];
-                    var b = points[(i + 1) % points.Count];
-                    var c = points[(i + 2) % points.Count];
-
-                    Vector v0 = c - a;
-                    Vector v1 = b - a;
-                    Vector v2 = point - a;
-
-                    var dot00 = v0 * v0;
-                    var dot01 = v0 * v1;
-                    var dot02 = v0 * v2;
-                    var dot11 = v1 * v1;
-                    var dot12 = v1 * v2;
-
-
-                    var invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
-                    var u = (dot11 * dot02 - dot01 * dot12) * invDenom;
-                    var v = (dot00 * dot12 - dot01 * dot02) * invDenom;
-                    if ((u >= 0) && (v >= 0) && (u + v < 1)) return true;
-                }
-                return false;
-            }
-        }
-    }
-}

+ 0 - 38
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@@ -1,38 +0,0 @@
-using System;
-using Avalonia.Media.TextFormatting;
-using Avalonia.Media.TextFormatting.Unicode;
-using Avalonia.Platform;
-
-namespace Avalonia.UnitTests
-{
-    public class MockTextShaperImpl : ITextShaperImpl
-    {
-        public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
-        {
-            var typeface = options.Typeface;
-            var fontRenderingEmSize = options.FontRenderingEmSize;
-            var bidiLevel = options.BidiLevel;
-            var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
-            var textSpan = text.Span;
-            var textStartIndex = TextTestHelper.GetStartCharIndex(text);
-
-            for (var i = 0; i < shapedBuffer.Length;)
-            {
-                var glyphCluster = i + textStartIndex;
-
-                var codepoint = Codepoint.ReadAt(textSpan, i, out var count);
-
-                var glyphIndex = typeface.GetGlyph(codepoint);
-
-                for (var j = 0; j < count; ++j)
-                {
-                    shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10);
-                }
-
-                i += count;
-            }
-
-            return shapedBuffer;
-        }
-    }
-}

+ 14 - 20
tests/Avalonia.UnitTests/TestServices.cs

@@ -13,6 +13,7 @@ using System.Collections.Generic;
 using Avalonia.Controls;
 using System.Reflection;
 using Avalonia.Animation;
+using Avalonia.Headless;
 using Avalonia.Threading;
 
 namespace Avalonia.UnitTests
@@ -22,25 +23,25 @@ namespace Avalonia.UnitTests
         public static readonly TestServices StyledWindow = new TestServices(
             assetLoader: new StandardAssetLoader(),
             platform: new StandardRuntimePlatform(),
-            renderInterface: new MockPlatformRenderInterface(),
-            standardCursorFactory: Mock.Of<ICursorFactory>(),
+            renderInterface: new HeadlessPlatformRenderInterface(),
+            standardCursorFactory: new HeadlessCursorFactoryStub(),
             theme: () => CreateSimpleTheme(),
-            dispatcherImpl: Mock.Of<IDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true),
-            fontManagerImpl: new MockFontManagerImpl(),
-            textShaperImpl: new MockTextShaperImpl(),
+            dispatcherImpl: new NullDispatcherImpl(),
+            fontManagerImpl: new HeadlessFontManagerStub(),
+            textShaperImpl: new HeadlessTextShaperStub(),
             windowingPlatform: new MockWindowingPlatform());
 
         public static readonly TestServices MockPlatformRenderInterface = new TestServices(
             assetLoader: new StandardAssetLoader(),
-            renderInterface: new MockPlatformRenderInterface(),
-            fontManagerImpl: new MockFontManagerImpl(),
-            textShaperImpl: new MockTextShaperImpl());
+            renderInterface: new HeadlessPlatformRenderInterface(),
+            fontManagerImpl: new HeadlessFontManagerStub(),
+            textShaperImpl: new HeadlessTextShaperStub());
 
         public static readonly TestServices MockPlatformWrapper = new TestServices(
             platform: Mock.Of<IRuntimePlatform>());
 
         public static readonly TestServices MockThreadingInterface = new TestServices(
-            dispatcherImpl: Mock.Of<IDispatcherImpl>(x => x.CurrentThreadIsLoopThread == true));
+            dispatcherImpl: new NullDispatcherImpl());
 
         public static readonly TestServices MockWindowingPlatform = new TestServices(
             windowingPlatform: new MockWindowingPlatform());
@@ -51,13 +52,13 @@ namespace Avalonia.UnitTests
             keyboardNavigation: new KeyboardNavigationHandler(),
             inputManager: new InputManager(),
             assetLoader: new StandardAssetLoader(),
-            renderInterface: new MockPlatformRenderInterface(),
-            fontManagerImpl: new MockFontManagerImpl(),
-            textShaperImpl: new MockTextShaperImpl());
+            renderInterface: new HeadlessPlatformRenderInterface(),
+            fontManagerImpl: new HeadlessFontManagerStub(),
+            textShaperImpl: new HeadlessTextShaperStub());
 
         public static readonly TestServices TextServices = new TestServices(
             assetLoader: new StandardAssetLoader(),
-            renderInterface: new MockPlatformRenderInterface(),
+            renderInterface: new HeadlessPlatformRenderInterface(),
             fontManagerImpl: new HarfBuzzFontManagerImpl(),
             textShaperImpl: new HarfBuzzTextShaperImpl());
         
@@ -158,12 +159,5 @@ namespace Avalonia.UnitTests
         {
             return new SimpleTheme();
         }
-
-        private static IPlatformRenderInterface CreateRenderInterfaceMock()
-        {
-            return Mock.Of<IPlatformRenderInterface>(x =>
-                x.CreateStreamGeometry() == Mock.Of<IStreamGeometryImpl>(
-                    y => y.Open() == Mock.Of<IStreamGeometryContextImpl>()));
-        }
     }
 }

+ 0 - 15
tests/Avalonia.UnitTests/TextTestHelper.cs

@@ -1,15 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace Avalonia.UnitTests
-{
-    public static class TextTestHelper
-    {
-        public static int GetStartCharIndex(ReadOnlyMemory<char> text)
-        {
-            if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
-                throw new InvalidOperationException("text memory should have been a string");
-            return start;
-        }
-    }
-}