diff --git a/.editorconfig b/.editorconfig
index d07618df6c..62a533e468 100644
--- a/.editorconfig
+++ b/.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
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index 875161d336..d2f2ee36d5 100644
--- a/.nuke/build.schema.json
+++ b/.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}'",
diff --git a/Avalonia.sln b/Avalonia.sln
index 40df3c8742..9670327d67 100644
--- a/Avalonia.sln
+++ b/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}
diff --git a/dirs.proj b/dirs.proj
index f1eaae8a4a..d29aa61fcb 100644
--- a/dirs.proj
+++ b/dirs.proj
@@ -9,10 +9,11 @@
-
+
-
+
+
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index e17bad28d7..87e1e86bf9 100644
--- a/nukebuild/Build.cs
+++ b/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(x => x.Package)
+ ? Execute(x => x.RunToolsTests)
: Execute(x => x.RunTests);
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index e56e32a5ff..a55a47fa53 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -729,6 +729,8 @@ namespace Avalonia.Controls
RowDetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsTemplateChanged(e));
RowDetailsVisibilityModeProperty.Changed.AddClassHandler((x, e) => x.OnRowDetailsVisibilityModeChanged(e));
AutoGenerateColumnsProperty.Changed.AddClassHandler((x, e) => x.OnAutoGenerateColumnsChanged(e));
+
+ FocusableProperty.OverrideDefaultValue(true);
}
///
@@ -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;
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
index dd802678d4..599bea056b 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
@@ -33,6 +33,8 @@ namespace Avalonia.Controls
{
PointerPressedEvent.AddClassHandler(
(x,e) => x.DataGridCell_PointerPressed(e), handledEventsToo: true);
+ FocusableProperty.OverrideDefaultValue(true);
+ IsTabStopProperty.OverrideDefaultValue(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();
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
index 5250f80f77..ef1e84c745 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
@@ -72,6 +72,7 @@ namespace Avalonia.Controls
{
AreSeparatorsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreSeparatorsVisibleChanged(e));
PressedMixin.Attach();
+ IsTabStopProperty.OverrideDefaultValue(false);
}
///
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
index 703bc0d9c3..4056b78bfe 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
+++ b/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)
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index ea9b2fe972..dfda7d6e4f 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
@@ -128,6 +128,7 @@ namespace Avalonia.Controls
DetailsTemplateProperty.Changed.AddClassHandler((x, e) => x.OnDetailsTemplateChanged(e));
AreDetailsVisibleProperty.Changed.AddClassHandler((x, e) => x.OnAreDetailsVisibleChanged(e));
PointerPressedEvent.AddClassHandler((x, e) => x.DataGridRow_PointerPressed(e), handledEventsToo: true);
+ IsTabStopProperty.OverrideDefaultValue(false);
}
///
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
index 10efded58a..e51c2526b1 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
@@ -106,6 +106,7 @@ namespace Avalonia.Controls
{
SublevelIndentProperty.Changed.AddClassHandler((x,e) => x.OnSublevelIndentChanged(e));
PressedMixin.Attach();
+ IsTabStopProperty.OverrideDefaultValue(false);
}
///
@@ -301,8 +302,7 @@ namespace Avalonia.Controls
}
else
{
- //if (!e.Handled && OwningGrid.IsTabStop)
- if (!e.Handled)
+ if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
index 00e035270c..44079d24d0 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs
+++ b/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)
diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
index e4642c1453..082eac60be 100644
--- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
+++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
@@ -82,7 +82,6 @@
-
-
@@ -268,7 +266,6 @@
-
@@ -310,7 +307,6 @@
-
@@ -408,7 +404,6 @@
-
@@ -433,7 +428,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
- Focusable="False"
+ IsTabStop="False"
Foreground="{TemplateBinding Foreground}" />
+
-
True if the currently focused element is within the visual tree of the parent
internal static bool ContainsFocusedElement(this Visual element)
{
- return (element == null) ? false : element.ContainsChild(FocusManager.Instance.Current as Visual);
+ return element is InputElement { IsKeyboardFocusWithin: true };
}
}
}
diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs
index 9af50180dd..77cc9d4dcb 100644
--- a/src/Avalonia.Controls/AppBuilder.cs
+++ b/src/Avalonia.Controls/AppBuilder.cs
@@ -288,17 +288,26 @@ namespace Avalonia
}
s_setupWasAlreadyCalled = true;
+ SetupUnsafe();
+ }
+
+ ///
+ /// Setup method that doesn't check for input initalizers being set.
+ /// Nor
+ ///
+ 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();
}
}
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index d4226d0013..48761ca8b8 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -18,7 +18,6 @@
-
diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs
index 621bdd1017..fcd607a707 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/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)
{
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs b/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
index 4768c88f75..d20f4bdc5d 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
+++ b/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);
}
}
diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
index 3401da2d4a..31b65dcc02 100644
--- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
+++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
index 8b2b38bb82..c7552823d4 100644
--- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs
+++ b/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 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 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)
{
/*
0: No preference
1: Prefer dark appearance
2: Prefer light appearance
*/
- 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]));
}
}
diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
index 20583dd6ac..cd6f829d7a 100644
--- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs
+++ b/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
{
diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs
index c0263b3518..8966dd4206 100644
--- a/src/Avalonia.Remote.Protocol/MetsysBson.cs
+++ b/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(string name)
diff --git a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
new file mode 100644
index 0000000000..49f1de31f2
--- /dev/null
+++ b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
@@ -0,0 +1,19 @@
+
+
+ netstandard2.0;net6.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs
new file mode 100644
index 0000000000..94b75cf849
--- /dev/null
+++ b/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;
+
+///
+/// Identifies a nunit test that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+///
+[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);
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTestMethodCommand.cs
new file mode 100644
index 0000000000..bd3f41de6a
--- /dev/null
+++ b/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 _beforeTest;
+ private readonly List _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 beforeTest,
+ List 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(), new List());
+ }
+
+ private static TestCommand ProcessCommand(HeadlessUnitTestSession session, TestCommand command, List before, List after)
+ {
+ if (command is BeforeAndAfterTestCommand beforeAndAfterTestCommand)
+ {
+ if (s_beforeTest.GetValue(beforeAndAfterTestCommand) is Action beforeTest)
+ {
+ Action beforeAction = c => before.Add(() => beforeTest(c));
+ s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction);
+ }
+ if (s_afterTest.GetValue(beforeAndAfterTestCommand) is Action afterTest)
+ {
+ Action 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 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;
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTheory.cs
new file mode 100644
index 0000000000..85ed67dbd2
--- /dev/null
+++ b/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;
+
+///
+/// Identifies a nunit theory that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+///
+[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);
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
index c2c58b4f94..4ab70eb07d 100644
--- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
+++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
@@ -1,12 +1,12 @@
- net6.0
- enable
- enable
+ netstandard2.0;net6.0
+ false
-
+
+
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs
new file mode 100644
index 0000000000..f501fc7a56
--- /dev/null
+++ b/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;
+
+///
+/// Identifies an xunit test that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+///
+[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);
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs
new file mode 100644
index 0000000000..4b1cf84914
--- /dev/null
+++ b/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 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 RunTestCollectionAsync(
+ IMessageBus messageBus,
+ ITestCollection testCollection,
+ IEnumerable 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 testCases,
+ IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer,
+ ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection,
+ testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
+ {
+ _session = session;
+ }
+
+ protected override Task RunTestClassAsync(
+ ITestClass testClass,
+ IReflectionTypeInfo @class,
+ IEnumerable 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 testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus,
+ ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator,
+ CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings) :
+ base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator,
+ cancellationTokenSource, collectionFixtureMappings)
+ {
+ _session = session;
+ }
+
+ protected override Task RunTestMethodAsync(
+ ITestMethod testMethod,
+ IReflectionMethodInfo method,
+ IEnumerable 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 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 RunTestCaseAsync(IXunitTestCase testCase)
+ {
+ return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason,
+ _constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator,
+ _cancellationTokenSource);
+ }
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
new file mode 100644
index 0000000000..092662745c
--- /dev/null
+++ b/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 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);
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
new file mode 100644
index 0000000000..97fcfa2521
--- /dev/null
+++ b/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 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 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 beforeAfterAttributes, ExceptionAggregator aggregator,
+ CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments,
+ testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
+ {
+ _onAfterTestInvoked = onAfterTestInvoked;
+ }
+
+ protected override Task 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 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);
+ }
+ }
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
index 21086fa946..aa9b3e7e18 100644
--- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
+++ b/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 : XunitTestFramework
+internal class AvaloniaTestFramework : XunitTestFramework
{
public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink)
{
@@ -26,8 +27,7 @@ internal class AvaloniaTestFramework : XunitTestFramework
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
{
- executionOptions.SetValue("xunit.execution.DisableParallelization", false);
- using (var assemblyRunner = new AvaloniaTestRunner(
+ using (var assemblyRunner = new AvaloniaTestAssemblyRunner(
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
executionOptions)) await assemblyRunner.RunAsync();
}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
index 3eace30805..bdd8f3b0ea 100644
--- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
+++ b/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;
///
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
///
+///
+/// It is an alternative to using [AvaloniaFact] or [AvaloniaTheory] attributes on every test method.
+///
[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute
{
- ///
- /// Creates instance of .
- ///
- ///
- /// Parameter from which should be created.
- /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
- ///
- public AvaloniaTestFrameworkAttribute(
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
- Type appBuilderEntryPointType) { }
}
///
@@ -38,8 +33,6 @@ public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
///
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);
}
}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
deleted file mode 100644
index 42604adf46..0000000000
--- a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Avalonia.Threading;
-using Xunit.Abstractions;
-using Xunit.Sdk;
-
-namespace Avalonia.Headless.XUnit;
-
-internal class AvaloniaTestRunner : XunitTestAssemblyRunner
-{
- private CancellationTokenSource? _cancellationTokenSource;
-
- public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable testCases,
- IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink,
- ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink,
- executionMessageSink, executionOptions)
- {
- }
-
- protected override void SetupSyncContext(int maxParallelThreads)
- {
- _cancellationTokenSource?.Dispose();
- _cancellationTokenSource = new CancellationTokenSource();
- SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result);
- }
-
- public override void Dispose()
- {
- _cancellationTokenSource?.Cancel();
- base.Dispose();
- }
-
- internal static Task InitNewApplicationContext(CancellationToken cancellationToken)
- {
- var tcs = new TaskCompletionSource();
-
- new Thread(() =>
- {
- try
- {
- var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry));
-
- // If windowing subsystem wasn't initialized by user, force headless with default parameters.
- if (appBuilder.WindowingSubsystemName != "Headless")
- {
- appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions());
- }
-
- appBuilder.SetupWithoutStarting();
-
- tcs.SetResult(SynchronizationContext.Current!);
- }
- catch (Exception e)
- {
- tcs.SetException(e);
- }
-
- Dispatcher.UIThread.MainLoop(cancellationToken);
- }) { IsBackground = true }.Start();
-
- return tcs.Task;
- }
-}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs
new file mode 100644
index 0000000000..53c997f08f
--- /dev/null
+++ b/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;
+
+///
+/// Identifies an xunit theory that starts on Avalonia Dispatcher
+/// such that awaited expressions resume on the test's "main thread".
+///
+[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 CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow)
+ {
+ yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow);
+ }
+
+ protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute)
+ {
+ yield return new AvaloniaTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod);
+ }
+}
diff --git a/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs b/src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs
new file mode 100644
index 0000000000..ea7e7abee4
--- /dev/null
+++ b/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 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);
+ }
+}
diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
index b626eaeb68..893cb0074c 100644
--- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
+++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
@@ -12,7 +12,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
index cefb6772c9..8202dab874 100644
--- a/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
+++ b/src/Headless/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
@@ -21,20 +21,21 @@ namespace Avalonia.Headless
private Action? _forceTick;
protected override IDisposable StartCore(Action 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();
});
}
diff --git a/src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs b/src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs
new file mode 100644
index 0000000000..9159657ec4
--- /dev/null
+++ b/src/Headless/Avalonia.Headless/AvaloniaTestApplicationAttribute.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Avalonia.Headless;
+
+///
+/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
+///
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
+public sealed class AvaloniaTestApplicationAttribute : Attribute
+{
+ public Type AppBuilderEntryPointType { get; }
+
+ ///
+ /// Creates instance of .
+ ///
+ ///
+ /// Parameter from which should be created.
+ /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
+ ///
+ public AvaloniaTestApplicationAttribute(
+ [DynamicallyAccessedMembers(HeadlessUnitTestSession.DynamicallyAccessed)]
+ Type appBuilderEntryPointType)
+ {
+ AppBuilderEntryPointType = appBuilderEntryPointType;
+ }
+}
diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 04e1ea99a5..ab157f8062 100644
--- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -18,7 +18,8 @@ namespace Avalonia.Headless
{
AvaloniaLocator.CurrentMutable
.Bind().ToConstant(new HeadlessPlatformRenderInterface())
- .Bind().ToConstant(new HeadlessFontManagerStub());
+ .Bind().ToConstant(new HeadlessFontManagerStub())
+ .Bind().ToConstant(new HeadlessTextShaperStub());
}
public IEnumerable 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 points = new List();
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()
{
diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
index 769fea7c6e..471ea9a6d0 100644
--- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
+++ b/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 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 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 text)
+ {
+ if (!MemoryMarshal.TryGetString(text, out _, out var start, out _))
+ throw new InvalidOperationException("text memory should have been a string");
+ return start;
+ }
+ }
}
diff --git a/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs b/src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
new file mode 100644
index 0000000000..1610f6796c
--- /dev/null
+++ b/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;
+
+///
+/// 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 methods to keep execution flow on the UI thread.
+/// Disposing unit test session stops internal dispatcher loop.
+///
+[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 s_session = new();
+
+ private readonly AppBuilder _appBuilder;
+ private readonly CancellationTokenSource _cancellationTokenSource;
+ private readonly BlockingCollection _queue;
+ private readonly Task _dispatchTask;
+
+ internal const DynamicallyAccessedMemberTypes DynamicallyAccessed =
+ DynamicallyAccessedMemberTypes.PublicMethods |
+ DynamicallyAccessedMemberTypes.NonPublicMethods |
+ DynamicallyAccessedMemberTypes.PublicParameterlessConstructor;
+
+ private HeadlessUnitTestSession(AppBuilder appBuilder, CancellationTokenSource cancellationTokenSource,
+ BlockingCollection queue, Task dispatchTask)
+ {
+ _appBuilder = appBuilder;
+ _cancellationTokenSource = cancellationTokenSource;
+ _queue = queue;
+ _dispatchTask = dispatchTask;
+ }
+
+ ///
+ public Task Dispatch(Action action, CancellationToken cancellationToken)
+ {
+ return Dispatch(() =>
+ {
+ action();
+ return Task.FromResult(0);
+ }, cancellationToken);
+ }
+
+ ///
+ public Task Dispatch(Func action, CancellationToken cancellationToken)
+ {
+ return Dispatch(() => Task.FromResult(action()), cancellationToken);
+ }
+
+ ///
+ /// Dispatch method queues an async operation on the dispatcher thread, creates a new application instance,
+ /// setting app avalonia services, and runs parameter.
+ ///
+ /// Action to execute on the dispatcher thread with avalonia services.
+ /// Cancellation token to cancel execution.
+ ///
+ /// If global session was already cancelled and thread killed, it's not possible to dispatch any actions again
+ ///
+ public Task Dispatch(Func> action, CancellationToken cancellationToken)
+ {
+ if (_cancellationTokenSource.IsCancellationRequested)
+ {
+ throw new ObjectDisposedException("Session was already disposed.");
+ }
+
+ var token = _cancellationTokenSource.Token;
+
+ var tcs = new TaskCompletionSource();
+ _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();
+ }
+
+ ///
+ /// Creates instance of .
+ ///
+ ///
+ /// Parameter from which should be created.
+ /// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
+ ///
+ public static HeadlessUnitTestSession StartNew(
+ [DynamicallyAccessedMembers(DynamicallyAccessed)]
+ Type entryPointType)
+ {
+ var tcs = new TaskCompletionSource();
+ var cancellationTokenSource = new CancellationTokenSource();
+ var queue = new BlockingCollection();
+
+ 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();
+ }
+
+ ///
+ /// Creates a session from AvaloniaTestApplicationAttribute attribute or reuses any existing.
+ /// If AvaloniaTestApplicationAttribute doesn't exist, empty application is used.
+ ///
+ [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()
+ ?.AppBuilderEntryPointType;
+ return appBuilderEntryPointType is not null ?
+ StartNew(appBuilderEntryPointType) :
+ StartNew(typeof(Application));
+ });
+ }
+}
diff --git a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
index 7d4b7f5477..61659dee2b 100644
--- a/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
@@ -44,53 +44,56 @@ public static class HeadlessWindowExtensions
/// Simulates keyboard press on the headless window/toplevel.
///
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
- RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers);
+ RunJobsOnImpl(topLevel, w => w.KeyPress(key, modifiers));
///
/// Simulates keyboard release on the headless window/toplevel.
///
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
- RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers);
+ RunJobsOnImpl(topLevel, w => w.KeyRelease(key, modifiers));
///
/// Simulates mouse down on the headless window/toplevel.
///
public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
- RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers);
+ RunJobsOnImpl(topLevel, w => w.MouseDown(point, button, modifiers));
///
/// Simulates mouse move on the headless window/toplevel.
///
public static void MouseMove(this TopLevel topLevel, Point point,
RawInputModifiers modifiers = RawInputModifiers.None) =>
- RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers);
+ RunJobsOnImpl(topLevel, w => w.MouseMove(point, modifiers));
///
/// Simulates mouse up on the headless window/toplevel.
///
public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button,
RawInputModifiers modifiers = RawInputModifiers.None) =>
- RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers);
+ RunJobsOnImpl(topLevel, w => w.MouseUp(point, button, modifiers));
///
/// Simulates mouse wheel on the headless window/toplevel.
///
public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta,
RawInputModifiers modifiers = RawInputModifiers.None) =>
- RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers);
+ RunJobsOnImpl(topLevel, w => w.MouseWheel(point, delta, modifiers));
///
/// Simulates drag'n'drop target on the headless window/toplevel.
///
public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data,
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) =>
- 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 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)
diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
index 8f9fc5fa80..d541e6b436 100644
--- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs
+++ b/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)
diff --git a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
index 3ccec872d2..adb5431ce6 100644
--- a/tests/Avalonia.Base.UnitTests/Media/FontManagerTests.cs
+++ b/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(() => 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().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().ToConstant(options);
diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
index 84ce341e98..c273cc6489 100644
--- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
+++ b/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()));
}
}
}
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
index 27bb0355e6..7cd02d2907 100644
--- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorHitTestingTests.cs
+++ b/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;
diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
deleted file mode 100644
index 37adb03628..0000000000
--- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
+++ /dev/null
@@ -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