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.Desktop.slnf b/Avalonia.Desktop.slnf
index 73e38f8cb9..92c924f107 100644
--- a/Avalonia.Desktop.slnf
+++ b/Avalonia.Desktop.slnf
@@ -3,6 +3,7 @@
"path": "Avalonia.sln",
"projects": [
"packages\\Avalonia\\Avalonia.csproj",
+ "samples\\AppWithoutLifetime\\AppWithoutLifetime.csproj",
"samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj",
"samples\\ControlCatalog\\ControlCatalog.csproj",
"samples\\GpuInterop\\GpuInterop.csproj",
diff --git a/Avalonia.sln b/Avalonia.sln
index 16de32d661..9670327d67 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -246,12 +246,11 @@ 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}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppWithoutLifetime", "samples\AppWithoutLifetime\AppWithoutLifetime.csproj", "{F8928267-688E-4A51-989C-612A72446D33}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo", "samples\SafeAreaDemo\SafeAreaDemo.csproj", "{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SafeAreaDemo.Android", "samples\SafeAreaDemo.Android\SafeAreaDemo.Android.csproj", "{22E3BC08-EAF7-4889-BDC4-B4D3046C4E2D}"
@@ -264,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
@@ -597,18 +602,22 @@ 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
{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F8928267-688E-4A51-989C-612A72446D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F8928267-688E-4A51-989C-612A72446D33}.Release|Any CPU.Build.0 = Release|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B60A970-D5D2-49C2-8BAB-F9C7973B74B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -629,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
@@ -706,15 +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/samples/AppWithoutLifetime/App.axaml b/samples/AppWithoutLifetime/App.axaml
new file mode 100644
index 0000000000..5f86b8be93
--- /dev/null
+++ b/samples/AppWithoutLifetime/App.axaml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/samples/AppWithoutLifetime/App.axaml.cs b/samples/AppWithoutLifetime/App.axaml.cs
new file mode 100644
index 0000000000..9cc99929c4
--- /dev/null
+++ b/samples/AppWithoutLifetime/App.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia;
+using Avalonia.Markup.Xaml;
+
+namespace AppWithoutLifetime;
+
+public partial class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+}
diff --git a/samples/AppWithoutLifetime/AppWithoutLifetime.csproj b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj
new file mode 100644
index 0000000000..fce12af298
--- /dev/null
+++ b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj
@@ -0,0 +1,21 @@
+
+
+ WinExe
+ net6.0
+ enable
+ app.manifest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/AppWithoutLifetime/MainWindow.axaml b/samples/AppWithoutLifetime/MainWindow.axaml
new file mode 100644
index 0000000000..3f31cb7fae
--- /dev/null
+++ b/samples/AppWithoutLifetime/MainWindow.axaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/samples/AppWithoutLifetime/MainWindow.axaml.cs b/samples/AppWithoutLifetime/MainWindow.axaml.cs
new file mode 100644
index 0000000000..d27d5d5653
--- /dev/null
+++ b/samples/AppWithoutLifetime/MainWindow.axaml.cs
@@ -0,0 +1,30 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+
+namespace AppWithoutLifetime;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ protected override void OnLoaded()
+ {
+ this.AttachDevTools();
+ base.OnLoaded();
+ }
+
+ public void Open(object sender, RoutedEventArgs e)
+ {
+ new Sub().Show(this);
+ }
+}
diff --git a/samples/AppWithoutLifetime/Program.cs b/samples/AppWithoutLifetime/Program.cs
new file mode 100644
index 0000000000..9404b9f01a
--- /dev/null
+++ b/samples/AppWithoutLifetime/Program.cs
@@ -0,0 +1,28 @@
+using Avalonia;
+using Avalonia.Controls;
+using System;
+
+namespace AppWithoutLifetime;
+
+class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args)
+ {
+ BuildAvaloniaApp().Start(AppMain, args);
+ }
+
+ private static void AppMain(Application app, string[] args)
+ {
+ app.Run(new MainWindow());
+ }
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace();
+}
diff --git a/samples/AppWithoutLifetime/Sub.axaml b/samples/AppWithoutLifetime/Sub.axaml
new file mode 100644
index 0000000000..d0061c432c
--- /dev/null
+++ b/samples/AppWithoutLifetime/Sub.axaml
@@ -0,0 +1,9 @@
+
+ Welcome to Avalonia Sub!
+
diff --git a/samples/AppWithoutLifetime/Sub.axaml.cs b/samples/AppWithoutLifetime/Sub.axaml.cs
new file mode 100644
index 0000000000..50c770b3a2
--- /dev/null
+++ b/samples/AppWithoutLifetime/Sub.axaml.cs
@@ -0,0 +1,24 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace AppWithoutLifetime;
+
+public partial class Sub : Window
+{
+ public Sub()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ protected override void OnLoaded()
+ {
+ this.AttachDevTools();
+ base.OnLoaded();
+ }
+}
diff --git a/samples/AppWithoutLifetime/app.manifest b/samples/AppWithoutLifetime/app.manifest
new file mode 100644
index 0000000000..d653ddd4b4
--- /dev/null
+++ b/samples/AppWithoutLifetime/app.manifest
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs
index 39497f1811..7130b3602a 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml.cs
+++ b/samples/IntegrationTestApp/MainWindow.axaml.cs
@@ -202,7 +202,7 @@ namespace IntegrationTestApp
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
- foreach (var window in lifetime.Windows)
+ foreach (var window in lifetime.Windows.ToArray())
{
window.Activate();
}
@@ -212,7 +212,7 @@ namespace IntegrationTestApp
{
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
- foreach (var window in lifetime.Windows)
+ foreach (var window in lifetime.Windows.ToArray())
{
window.Show();
if (window.WindowState == WindowState.Minimized)
diff --git a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
index 5307ea4b9a..88cd1f38af 100644
--- a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
+++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
@@ -35,7 +35,12 @@ namespace Avalonia.Data.Converters
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
value = Inner?.Convert(value, targetType, parameter, culture) ?? value;
- return string.Format(culture, Format, value);
+ var format = Format!;
+ if (!format.Contains('{'))
+ {
+ format = $"{{0:{format}}}";
+ }
+ return string.Format(culture, format, value);
}
///
diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs
index 2bf666af44..c8de7267ca 100644
--- a/src/Avalonia.Base/Input/FocusManager.cs
+++ b/src/Avalonia.Base/Input/FocusManager.cs
@@ -122,6 +122,11 @@ namespace Avalonia.Input
{
scope = scope ?? throw new ArgumentNullException(nameof(scope));
+ if (element is not null && !CanFocus(element))
+ {
+ return;
+ }
+
if (_focusScopes.TryGetValue(scope, out var existingElement))
{
if (element != existingElement)
@@ -242,6 +247,6 @@ namespace Avalonia.Input
}
}
- private static bool IsVisible(IInputElement e) => (e as Visual)?.IsVisible ?? true;
+ private static bool IsVisible(IInputElement e) => (e as Visual)?.IsEffectivelyVisible ?? true;
}
}
diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs
index 962c7aa334..33ddbaedf9 100644
--- a/src/Avalonia.Base/Input/InputElement.cs
+++ b/src/Avalonia.Base/Input/InputElement.cs
@@ -647,6 +647,10 @@ namespace Avalonia.Input
{
PseudoClasses.Set(":focus-within", change.GetNewValue());
}
+ else if (change.Property == IsVisibleProperty && !change.GetNewValue() && IsFocused)
+ {
+ FocusManager.Instance?.Focus(null);
+ }
}
///
diff --git a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
index 9b5668bf98..1c61334888 100644
--- a/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
+++ b/src/Avalonia.Base/Input/TextInput/InputMethodManager.cs
@@ -48,9 +48,9 @@ namespace Avalonia.Input.TextInput
}
_transformTracker.SetVisual(_client?.TextViewVisual);
- UpdateCursorRect();
_im?.SetClient(_client);
+ UpdateCursorRect();
}
else
{
diff --git a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
index 608f924808..55aac6f3fa 100644
--- a/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
+++ b/src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
@@ -50,7 +50,8 @@ internal static class StorageProviderHelpers
}
}
- public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
+ [return: NotNullIfNotNull(nameof(path))]
+ public static string? NameWithExtension(string? path, string? defaultExtension, FilePickerFileType? filter)
{
var name = Path.GetFileName(path);
if (name != null && !Path.HasExtension(name))
diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
index 8ecc0028ce..3991300fb5 100644
--- a/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Expressions/TokenParser.cs
@@ -243,7 +243,7 @@ namespace Avalonia.Rendering.Composition.Expressions
else if (ch == '-')
{
if (len != 0)
- return false;
+ break;
len = c + 1;
}
else
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/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
index 9cc36e6a81..77f3b93efa 100644
--- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
+++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
+using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Interactivity;
@@ -16,7 +17,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
private int _exitCode;
private CancellationTokenSource? _cts;
private bool _isShuttingDown;
- private readonly HashSet _windows = new();
+ private readonly AvaloniaList _windows = new();
private static ClassicDesktopStyleApplicationLifetime? s_activeLifetime;
@@ -35,7 +36,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
private static void OnWindowOpened(object? sender, RoutedEventArgs e)
{
- s_activeLifetime?._windows.Add((Window)sender!);
+ var window = (Window)sender!;
+ if (s_activeLifetime is not null && !s_activeLifetime._windows.Contains(window))
+ {
+ s_activeLifetime._windows.Add(window);
+ }
}
public ClassicDesktopStyleApplicationLifetime()
@@ -67,7 +72,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
public Window? MainWindow { get; set; }
///
- public IReadOnlyList Windows => _windows.ToArray();
+ public IReadOnlyList Windows => _windows;
private void HandleWindowClosed(Window? window)
{
@@ -161,7 +166,7 @@ namespace Avalonia.Controls.ApplicationLifetimes
// When an OS shutdown request is received, try to close all non-owned windows. Windows can cancel
// shutdown by setting e.Cancel = true in the Closing event. Owned windows will be shutdown by their
// owners.
- foreach (var w in Windows)
+ foreach (var w in Windows.ToArray())
{
if (w.Owner is null)
{
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 9c4bacbedf..48761ca8b8 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -15,9 +15,9 @@
+
-
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index dadda4e0ec..f41c00662d 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -170,8 +170,15 @@ namespace Avalonia.Controls
UpdateFlowDirection();
}
- protected internal override Control CreateContainerForItemOverride() => new ComboBoxItem();
- protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ComboBoxItem;
+ protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
+ {
+ return new ComboBoxItem();
+ }
+
+ protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
+ {
+ return NeedsContainer(item, out recycleKey);
+ }
///
protected override void OnKeyDown(KeyEventArgs e)
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.Controls/DesktopApplicationExtensions.cs b/src/Avalonia.Controls/DesktopApplicationExtensions.cs
index ddaed6bb0d..ce246f0532 100644
--- a/src/Avalonia.Controls/DesktopApplicationExtensions.cs
+++ b/src/Avalonia.Controls/DesktopApplicationExtensions.cs
@@ -1,6 +1,5 @@
using System;
using System.Threading;
-using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs
index a2ce93ee6d..1e5f43cf41 100644
--- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs
+++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs
@@ -24,9 +24,8 @@ namespace Avalonia.Controls
///
/// Defines the property
///
- public static readonly DirectProperty ItemTemplateProperty =
- AvaloniaProperty.RegisterDirect(nameof(ItemTemplate),
- x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
+ public static readonly StyledProperty ItemTemplateProperty =
+ AvaloniaProperty.Register(nameof(ItemTemplate));
///
/// Defines the property.
@@ -59,8 +58,8 @@ namespace Avalonia.Controls
///
public IDataTemplate? ItemTemplate
{
- get => _itemTemplate;
- set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
+ get => GetValue(ItemTemplateProperty);
+ set => SetValue(ItemTemplateProperty, value);
}
///
@@ -82,7 +81,6 @@ namespace Avalonia.Controls
}
private Classes? _classes;
- private IDataTemplate? _itemTemplate;
protected override Control CreatePresenter()
{
diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
index d27479af18..91e8dae0a1 100644
--- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
+++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
@@ -10,34 +10,44 @@ namespace Avalonia.Controls.Generators
/// When creating a container for an item from a , the following
/// process should be followed:
///
- /// - should first be called if the item is
- /// derived from the class. If this method returns true then the
- /// item itself should be used as the container.
- /// - If returns false then
- /// should be called to create a new container.
+ /// - should first be called to
+ /// determine whether the item needs a container. This method will return true if the item
+ /// should be wrapped in a container control, or false if the item itself can be used as a
+ /// container.
+ /// - If returns true then the
+ /// method should be called to create a new container, passing
+ /// the recycle key returned from .
+ /// - If the panel supports recycling and the recycle key is non-null then the recycle key
+ /// should be recorded for the container (e.g. in an attached property or the realized
+ /// container list).
/// - method should be called for the
/// container.
/// - The container should then be added to the panel using
///
/// - Finally, should be called.
///
- /// NOTE: If in the first step above returns true
- /// then the above steps should be carried out a single time; the first time the item is
- /// displayed. Otherwise the steps should be carried out each time a new container is realized
- /// for an item.
+ /// NOTE: If in the first step above
+ /// returns false then the above steps should be carried out a single time: the first time the
+ /// item is displayed. Otherwise the steps should be carried out each time a new container is
+ /// realized for an item.
///
/// When unrealizing a container, the following process should be followed:
///
- /// - If for the item returned true then the item
- /// cannot be unrealized or recycled.
+ /// - If for the item returned false
+ /// then the item cannot be unrealized or recycled.
/// - Otherwise, should be called for the container
- /// - If recycling is supported then the container should be added to a recycle pool.
- /// - It is assumed that recyclable containers will not be removed from the panel but instead
- /// hidden from view using e.g. `container.IsVisible = false`.
+ /// - If recycling is supported by the panel and the container then the container should be
+ /// added to a recycle pool keyed on the recycle key returned from
+ /// . It is assumed that recycled
+ /// containers will not be removed from the panel but instead hidden from view using
+ /// e.g. `container.IsVisible = false`.
+ /// - If recycling is not supported then the container should be removed from the panel.
///
/// When recycling an unrealized container, the following process should be followed:
///
- /// - An element should be taken from the recycle pool.
+ /// - should be called to determine
+ /// whether the item needs a container, and if so, the recycle key.
+ /// - A container should be taken from the recycle pool keyed on the returned recycle key.
/// - The container should be made visible.
/// - method should be called for the
/// container.
@@ -55,28 +65,43 @@ namespace Avalonia.Controls.Generators
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
///
- /// Creates a new container control.
+ /// Determines whether the specified item needs to be wrapped in a container control.
///
- /// The newly created container control.
- ///
- /// Before calling this method, should be
- /// called to determine whether the item itself should be used as a container. After
- /// calling this method, should
- /// be called to prepare the container to display the specified item.
- ///
- public Control CreateContainer() => _owner.CreateContainerForItemOverride();
+ /// The item to display.
+ /// The index of the item.
+ ///
+ /// When the method returns, contains a key that can be used to locate a previously
+ /// recycled container of the correct type, or null if the item cannot be recycled.
+ ///
+ ///
+ /// true if the item needs a container; otherwise false if the item can itself be used
+ /// as a container.
+ ///
+ public bool NeedsContainer(object? item, int index, out object? recycleKey) =>
+ _owner.NeedsContainerOverride(item, index, out recycleKey);
///
- /// Determines whether the specified item is (or is eligible to be) its own container.
+ /// Creates a new container control.
///
- /// The item.
- /// true if the item is its own container, otherwise false.
+ /// The item to display.
+ /// The index of the item.
+ ///
+ /// The recycle key returned from
+ ///
+ /// The newly created container control.
///
- /// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
- /// control items may be; the caller is responsible for checking if each item is a control
- /// and calling this method before creating a new container.
+ /// Before calling this method,
+ /// should be called to determine whether the item itself should be used as a container.
+ /// After calling this method,
+ /// must be called to prepare the container to display the specified item.
+ ///
+ /// If the panel supports recycling then the returned recycle key should be stored alongside
+ /// the container and when container becomes eligible for recycling the container should
+ /// be placed in a recycle pool using this key. If the returned recycle key is null then
+ /// the container cannot be recycled.
///
- public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
+ public Control CreateContainer(object? item, int index, object? recycleKey)
+ => _owner.CreateContainerForItemOverride(item, index, recycleKey);
///
/// Prepares the specified element as the container for the corresponding item.
@@ -85,10 +110,10 @@ namespace Avalonia.Controls.Generators
/// The item to display.
/// The index of the item to display.
///
- /// If is true for an item, then this method
- /// must only be called a single time, otherwise this method must be called after the
- /// container is created, and each subsequent time the container is recycled to display a
- /// new item.
+ /// If is false for an
+ /// item, then this method must only be called a single time; otherwise this method must
+ /// be called after the container is created, and each subsequent time the container is
+ /// recycled to display a new item.
///
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
@@ -104,8 +129,8 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container has been fully prepared and added
/// to the logical and visual trees, but may be called before a layout pass has completed.
/// It must be called regardless of the result of
- /// but if that method returned true then
- /// must be called only a single time.
+ /// but if that method returned
+ /// false then must be called only a single time.
///
public void ItemContainerPrepared(Control container, object? item, int index) =>
_owner.ItemContainerPrepared(container, item, index);
@@ -128,7 +153,7 @@ namespace Avalonia.Controls.Generators
/// This method must be called when a container is unrealized. The container must have
/// already have been removed from the virtualizing panel's list of realized containers before
/// this method is called. This method must not be called if
- /// returned true for the item.
+ /// returned false for the item.
///
public void ClearItemContainer(Control container) => _owner.ClearItemContainer(container);
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index 4a0b3c367e..064716fa9b 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -307,6 +307,12 @@ namespace Avalonia.Controls
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
+ ///
+ /// Gets a default recycle key that can be used when an supports
+ /// a single container type.
+ ///
+ protected static object DefaultRecycleKey { get; } = new object();
+
///
/// Returns the container for the item at the specified index.
///
@@ -362,7 +368,10 @@ namespace Avalonia.Controls
///
/// Creates or a container that can be used to display an item.
///
- protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
+ protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
+ {
+ return new ContentPresenter();
+ }
///
/// Prepares the specified element to display the specified item.
@@ -495,11 +504,52 @@ namespace Avalonia.Controls
}
///
- /// Determines whether the specified item is (or is eligible to be) its own container.
+ /// Determines whether the specified item can be its own container.
///
/// The item to check.
- /// true if the item is (or is eligible to be) its own container; otherwise, false.
- protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
+ /// The index of the item.
+ ///
+ /// When the method returns, contains a key that can be used to locate a previously
+ /// recycled container of the correct type, or null if the item cannot be recycled.
+ /// If the item is its own container then by definition it cannot be recycled, so
+ /// shoud be set to null.
+ ///
+ ///
+ /// true if the item needs a container; otherwise false if the item can itself be used
+ /// as a container.
+ ///
+ protected internal virtual bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
+ {
+ return NeedsContainer(item, out recycleKey);
+ }
+
+ ///
+ /// A default implementation of
+ /// that returns true and sets the recycle key to if the item
+ /// is not a .
+ ///
+ /// The container type.
+ /// The item.
+ ///
+ /// When the method returns, contains if
+ /// is not of type ; otherwise null.
+ ///
+ ///
+ /// true if is of type ; otherwise false.
+ ///
+ protected bool NeedsContainer(object? item, out object? recycleKey) where T : Control
+ {
+ if (item is T)
+ {
+ recycleKey = null;
+ return false;
+ }
+ else
+ {
+ recycleKey = DefaultRecycleKey;
+ return true;
+ }
+ }
///
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs
index e5f0b50555..2586333f20 100644
--- a/src/Avalonia.Controls/ListBox.cs
+++ b/src/Avalonia.Controls/ListBox.cs
@@ -58,6 +58,9 @@ namespace Avalonia.Controls
static ListBox()
{
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
+ KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
+ typeof(ListBox),
+ KeyboardNavigationMode.Once);
}
///
@@ -108,8 +111,15 @@ namespace Avalonia.Controls
///
public void UnselectAll() => Selection.Clear();
- protected internal override Control CreateContainerForItemOverride() => new ListBoxItem();
- protected internal override bool IsItemItsOwnContainerOverride(Control item) => item is ListBoxItem;
+ protected internal override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
+ {
+ return new ListBoxItem();
+ }
+
+ protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
+ {
+ return NeedsContainer(item, out recycleKey);
+ }
///
protected override void OnGotFocus(GotFocusEventArgs e)
diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs
index ac0262c9b2..4df423dee8 100644
--- a/src/Avalonia.Controls/Menu.cs
+++ b/src/Avalonia.Controls/Menu.cs
@@ -35,6 +35,9 @@ namespace Avalonia.Controls
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
+ KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
+ typeof(Menu),
+ KeyboardNavigationMode.Once);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue
public Func? FrameworkAssetPathResolver { get; set; }
+
+ ///
+ /// Defines if the service worker used by Avalonia should be registered.
+ /// If registered, service worker can work as a save file picker fallback on the browsers that don't support native implementation.
+ /// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
+ ///
+ public bool RegisterAvaloniaServiceWorker { get; set; }
+
+ ///
+ /// If is enabled, it is possible to redefine scope for the worker.
+ /// By default, current domain root is used as a scope.
+ ///
+ public string? AvaloniaServiceWorkerScope { get; set; }
+
+ ///
+ /// Avalonia uses "native-file-system-adapter" polyfill for the file dialogs.
+ /// If native implementation is available, by default it is used.
+ /// This property forces polyfill to be always used.
+ /// For more details, see https://github.com/jimmywarting/native-file-system-adapter#a-note-when-downloading-with-the-polyfilled-version.
+ ///
+ public bool PreferFileDialogPolyfill { get; set; }
}
public static class BrowserAppBuilder
diff --git a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
index 394f191dab..cb7aabbc39 100644
--- a/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
+++ b/src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
@@ -25,6 +25,15 @@ internal static partial class AvaloniaModule
public static Task ImportStorage() => s_importStorage.Value;
+ public static string ResolveServiceWorkerPath()
+ {
+ var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions();
+ return options.FrameworkAssetPathResolver!("sw.js");
+ }
+
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();
+
+ [JSImport("registerServiceWorker", AvaloniaModule.MainModuleName)]
+ public static partial void RegisterServiceWorker(string path, string? scope);
}
diff --git a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
index c56023c0f7..d95d4405ba 100644
--- a/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
+++ b/src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
@@ -9,15 +9,15 @@ internal static partial class StorageHelper
public static partial bool HasNativeFilePicker();
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
- public static partial Task SelectFolderDialog(JSObject? startIn);
+ public static partial Task SelectFolderDialog(JSObject? startIn, bool preferPolyfill);
[JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task OpenFileDialog(JSObject? startIn, bool multiple,
- [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption);
+ [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)]
public static partial Task SaveFileDialog(JSObject? startIn, string? suggestedName,
- [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption);
+ [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption, bool preferPolyfill);
[JSImport("StorageItem.createWellKnownDirectory", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateWellKnownDirectory(string wellKnownDirectory);
diff --git a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
index e6849f2fcc..a28fd4cbde 100644
--- a/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
+++ b/src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
@@ -6,20 +6,22 @@ using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage;
+using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Browser.Storage;
-internal record FilePickerAcceptType(string Description, IReadOnlyDictionary> Accept);
-
internal class BrowserStorageProvider : IStorageProvider
{
internal const string PickerCancelMessage = "The user aborted a request";
internal const string NoPermissionsMessage = "Permissions denied";
public bool CanOpen => true;
- public bool CanSave => StorageHelper.HasNativeFilePicker();
+ public bool CanSave => true;
public bool CanPickFolder => true;
+ private bool PreferPolyfill =>
+ AvaloniaLocator.Current.GetService()?.PreferFileDialogPolyfill ?? false;
+
public async Task> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await AvaloniaModule.ImportStorage();
@@ -29,7 +31,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
- using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll);
+ using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, excludeAll, PreferPolyfill);
if (items is null)
{
return Array.Empty();
@@ -63,7 +65,9 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
- var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, excludeAll);
+ var suggestedName =
+ StorageProviderHelpers.NameWithExtension(options.SuggestedFileName, options.DefaultExtension, null);
+ var item = await StorageHelper.SaveFileDialog(startIn, suggestedName, types, excludeAll, PreferPolyfill);
return item is not null ? new JSStorageFile(item) : null;
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
@@ -89,7 +93,7 @@ internal class BrowserStorageProvider : IStorageProvider
try
{
- var item = await StorageHelper.SelectFolderDialog(startIn);
+ var item = await StorageHelper.SelectFolderDialog(startIn, PreferPolyfill);
return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty();
}
catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal))
diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs
index be6e28f5cb..a33738079c 100644
--- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs
+++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading;
+using Avalonia.Browser.Interop;
using Avalonia.Browser.Skia;
using Avalonia.Input;
using Avalonia.Input.Platform;
@@ -46,6 +47,13 @@ namespace Avalonia.Browser
.Bind().ToConstant(new BrowserSkiaGraphics())
.Bind().ToSingleton()
.Bind().ToSingleton();
+
+ if (AvaloniaLocator.Current.GetService() is { } options
+ && options.RegisterAvaloniaServiceWorker)
+ {
+ var swPath = AvaloniaModule.ResolveServiceWorkerPath();
+ AvaloniaModule.RegisterServiceWorker(swPath, options.AvaloniaServiceWorkerScope);
+ }
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
diff --git a/src/Browser/Avalonia.Browser/webapp/build.js b/src/Browser/Avalonia.Browser/webapp/build.js
index c1cbc84709..e8e49554cd 100644
--- a/src/Browser/Avalonia.Browser/webapp/build.js
+++ b/src/Browser/Avalonia.Browser/webapp/build.js
@@ -1,7 +1,8 @@
require("esbuild").build({
entryPoints: [
"./modules/avalonia.ts",
- "./modules/storage.ts"
+ "./modules/storage.ts",
+ "./modules/sw.ts"
],
outdir: "../wwwroot",
bundle: true,
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
index 80faca7a50..2b69254cf2 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
@@ -7,6 +7,12 @@ import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper";
import { GeneralHelpers } from "./avalonia/generalHelpers";
+async function registerServiceWorker(path: string, scope: string | undefined) {
+ if ("serviceWorker" in navigator) {
+ await globalThis.navigator.serviceWorker.register(path, scope ? { scope } : undefined);
+ }
+}
+
export {
Caniuse,
Canvas,
@@ -17,5 +23,6 @@ export {
StreamHelper,
NativeControlHost,
NavigationHelper,
- GeneralHelpers
+ GeneralHelpers,
+ registerServiceWorker
};
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
index 7c7769ea36..2e160ec618 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
+++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
@@ -18,12 +18,7 @@ export class StreamHelper {
const array = new Uint8Array(span.byteLength);
span.copyTo(array);
- const data = {
- type: "write",
- data: array
- };
-
- return await stream.write(data);
+ return await stream.write(array);
}
public static byteLength(stream: Blob) {
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
index 7a29992674..8c79f225ee 100644
--- a/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
+++ b/src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
@@ -1,6 +1,6 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem";
-import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter";
+import { showOpenFilePicker, showDirectoryPicker, showSaveFilePicker, FileSystemFileHandle } from "native-file-system-adapter";
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
@@ -12,10 +12,12 @@ declare global {
export class StorageProvider {
public static async selectFolderDialog(
- startIn: StorageItem | null): Promise {
+ startIn: StorageItem | null,
+ preferPolyfill: boolean): Promise {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options = {
- startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined)
+ startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
+ _preferPolyfill: preferPolyfill
};
const handle = await showDirectoryPicker(options as any);
@@ -24,12 +26,14 @@ export class StorageProvider {
public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean,
- types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise {
+ types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
+ preferPolyfill: boolean): Promise {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
multiple,
excludeAcceptAllOption,
- types: (types ?? undefined)
+ types: (types ?? undefined),
+ _preferPolyfill: preferPolyfill
};
const handles = await showOpenFilePicker(options);
@@ -38,16 +42,17 @@ export class StorageProvider {
public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null,
- types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise {
+ types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean,
+ preferPolyfill: boolean): Promise {
const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption,
- types: (types ?? undefined)
+ types: (types ?? undefined),
+ _preferPolyfill: preferPolyfill
};
- // Always prefer native save file picker, as polyfill solutions are not reliable.
- const handle = await (globalThis as any).showSaveFilePicker(options);
+ const handle = await showSaveFilePicker(options);
return StorageItem.createFromHandle(handle);
}
diff --git a/src/Browser/Avalonia.Browser/webapp/modules/sw.ts b/src/Browser/Avalonia.Browser/webapp/modules/sw.ts
new file mode 100644
index 0000000000..2d08ae2c15
--- /dev/null
+++ b/src/Browser/Avalonia.Browser/webapp/modules/sw.ts
@@ -0,0 +1,78 @@
+const WRITE = 0;
+const PULL = 0;
+const ERROR = 1;
+const ABORT = 1;
+const CLOSE = 2;
+
+class MessagePortSource implements UnderlyingSource {
+ private controller?: ReadableStreamController;
+
+ constructor (private readonly port: MessagePort) {
+ this.port.onmessage = evt => this.onMessage(evt.data);
+ }
+
+ start (controller: ReadableStreamController) {
+ this.controller = controller;
+ }
+
+ cancel (reason: Error) {
+ // Firefox can notify a cancel event, chrome can't
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=638494
+ this.port.postMessage({ type: ERROR, reason: reason.message });
+ this.port.close();
+ }
+
+ onMessage (message: { type: number; chunk: Uint8Array; reason: any }) {
+ // enqueue() will call pull() if needed when there's no backpressure
+ if (!this.controller) {
+ return;
+ }
+ if (message.type === WRITE) {
+ this.controller.enqueue(message.chunk);
+ this.port.postMessage({ type: PULL });
+ }
+ if (message.type === ABORT) {
+ this.controller.error(message.reason);
+ this.port.close();
+ }
+ if (message.type === CLOSE) {
+ this.controller.close();
+ this.port.close();
+ }
+ }
+}
+
+self.addEventListener("install", () => {
+ (self as any).skipWaiting();
+});
+
+self.addEventListener("activate", event /* ExtendableEvent */ => {
+ (event as any).waitUntil((self as any).clients.claim());
+});
+
+const map = new Map();
+
+// This should be called once per download
+// Each event has a dataChannel that the data will be piped through
+globalThis.addEventListener("message", evt => {
+ const data = evt.data;
+ if (data.url && data.readablePort) {
+ data.rs = new ReadableStream(
+ new MessagePortSource(evt.data.readablePort),
+ new CountQueuingStrategy({ highWaterMark: 4 })
+ );
+ map.set(data.url, data);
+ }
+});
+
+globalThis.addEventListener("fetch", evt => {
+ const url = (evt as any).request.url;
+ const data = map.get(url);
+ if (!data) return null;
+ map.delete(url);
+ (evt as any).respondWith(new Response(data.rs, {
+ headers: data.headers
+ }));
+});
+
+export {};
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/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
index afc7b569a8..b6a4b599fa 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
@@ -36,13 +36,29 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
}
- if (valueNode.Manipulation is not XamlObjectInitializationNode
- {
- Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
- })
+ if (valueNode.Manipulation is not XamlObjectInitializationNode initializationNode)
{
throw new XamlDocumentParseException(context.CurrentDocument,
- $"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
+ $"Invalid \"{nodeTypeName}\" node initialization.", valueNode);
+ }
+
+ var additionalProperties = new List();
+ if (initializationNode.Manipulation is not XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty)
+ {
+ if (initializationNode.Manipulation is XamlManipulationGroupNode manipulationGroup
+ && manipulationGroup.Children.OfType()
+ .FirstOrDefault(p => p.Property.Name == "Source") is { } sourceProperty2)
+ {
+ sourceProperty = sourceProperty2;
+ // We need to copy some additional properties from ResourceInclude to ResourceDictionary except the Source one.
+ // If there is any missing properties, then XAML compiler will throw an error in the emitter code.
+ additionalProperties = manipulationGroup.Children.Where(c => c != sourceProperty2).ToList();
+ }
+ else
+ {
+ throw new XamlDocumentParseException(context.CurrentDocument,
+ $"Source property must be set on the \"{nodeTypeName}\" node.", valueNode);
+ }
}
var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false);
@@ -65,12 +81,12 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
{
if (targetDocument.BuildMethod is not null)
{
- return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+ return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
if (targetDocument.ClassType is not null)
{
- return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+ return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@@ -95,11 +111,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
if (buildMethod is not null)
{
- return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+ return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
else if (assetAssembly.FindType(fullTypeName) is { } type)
{
- return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+ return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly, additionalProperties);
}
return context.ParseError(
@@ -108,7 +124,8 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li,
- IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
+ IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
+ IEnumerable manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(type))
{
@@ -116,15 +133,17 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
$"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
li, fallbackNode);
}
-
+
IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
+ ((XamlAstObjectNode)newObjNode).Children.AddRange(manipulationNodes);
newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
return new NewObjectTransformer().Transform(context, newObjNode);
}
private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li,
- IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
+ IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly,
+ IEnumerable manipulationNodes)
{
if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
{
@@ -134,8 +153,11 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
}
var sp = context.Configuration.TypeMappings.ServiceProvider;
- return new XamlStaticOrTargetedReturnMethodCallNode(li, method,
- new[] { new NewServiceProviderNode(sp, li) });
+
+ return new XamlValueWithManipulationNode(li,
+ new XamlStaticOrTargetedReturnMethodCallNode(li, method,
+ new[] { new NewServiceProviderNode(sp, li) }),
+ new XamlManipulationGroupNode(li, manipulationNodes));
}
internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude(
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs
index 05df8be1b6..256a7472f3 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlThemeVariantProviderTransformer.cs
@@ -10,7 +10,8 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
- var type = context.GetAvaloniaTypes().IThemeVariantProvider;
+ var avTypes = context.GetAvaloniaTypes();
+ var type = avTypes.IThemeVariantProvider;
if (!(node is XamlAstObjectNode on
&& type.IsAssignableFrom(on.Type.GetClrType())))
return node;
@@ -21,6 +22,13 @@ internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransform
if (keyDirective is null)
return node;
+ var themeDictionariesColl = avTypes.IDictionaryT.MakeGenericType(avTypes.ThemeVariant, avTypes.IThemeVariantProvider);
+ if (context.ParentNodes().FirstOrDefault() is not XamlAstXamlPropertyValueNode propertyValueNode
+ || !themeDictionariesColl.IsAssignableFrom(propertyValueNode.Property.GetClrProperty().Getter.ReturnType))
+ {
+ return node;
+ }
+
var keyProp = type.Properties.First(p => p.Name == "Key");
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective,
new XamlAstClrProperty(keyDirective, keyProp, context.Configuration),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index b5c0c7734d..c5c3cdd123 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -65,6 +65,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType Int { get; }
public IXamlType Long { get; }
public IXamlType Uri { get; }
+ public IXamlType IDictionaryT { get; }
public IXamlType FontFamily { get; }
public IXamlConstructor FontFamilyConstructorUriName { get; }
public IXamlType Thickness { get; }
@@ -194,6 +195,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
Int = cfg.TypeSystem.GetType("System.Int32");
Long = cfg.TypeSystem.GetType("System.Int64");
Uri = cfg.TypeSystem.GetType("System.Uri");
+ IDictionaryT = cfg.TypeSystem.GetType("System.Collections.Generic.IDictionary`2");
FontFamily = cfg.TypeSystem.GetType("Avalonia.Media.FontFamily");
FontFamilyConstructorUriName = FontFamily.GetConstructor(new List { Uri, XamlIlTypes.String });
ThemeVariant = cfg.TypeSystem.GetType("Avalonia.Styling.ThemeVariant");
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
index 9c7b6deb7c..37a028d7b1 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
@@ -701,7 +701,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
.GetConstructor(new() { objectType, context.Configuration.TypeSystem.GetType("System.IntPtr") }));
}
- if (_dependsOnProperties is { Count:> 1 })
+ if (_dependsOnProperties is { Count:> 0 })
{
using var dependsOnPropertiesArray = context.GetLocalOfType(context.Configuration.WellKnownTypes.String.MakeArrayType(1));
codeGen
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/Composition/CompositionAnimationParserTests.cs b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationParserTests.cs
new file mode 100644
index 0000000000..fc6ea969d8
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Composition/CompositionAnimationParserTests.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using Avalonia.Animation.Easings;
+using Avalonia.Base.UnitTests.Rendering;
+using Avalonia.Rendering;
+using Avalonia.Rendering.Composition;
+using Avalonia.Rendering.Composition.Expressions;
+using Avalonia.Rendering.Composition.Server;
+using Avalonia.Threading;
+using Avalonia.UnitTests;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Avalonia.Base.UnitTests.Composition;
+
+public class CompositionAnimationParserTests
+{
+ [Theory,
+ InlineData("Vector3(0.5+(4.0-0.5)* -2, 1, 0).X", -6.5)]
+ public void EvaluatesExpressionCorrectly(string expression, double value)
+ {
+ var expr = ExpressionParser.Parse(expression);
+ var ctx = new ExpressionEvaluationContext
+ {
+ ForeignFunctionInterface = BuiltInExpressionFfi.Instance,
+ };
+ var res = expr.Evaluate(ref ctx);
+ double doubleRes;
+ if (res.Type == VariantType.Scalar)
+ doubleRes = res.Scalar;
+ else if (res.Type == VariantType.Double)
+ doubleRes = res.Double;
+ else
+ throw new Exception("Invalid result type: " + res.Type);
+ Assert.Equal(value, doubleRes);
+ }
+}
\ No newline at end of file
diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
index e36ce21009..ac1547d09f 100644
--- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
@@ -26,6 +26,138 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Same(target, FocusManager.Instance.Current);
}
}
+
+ [Fact]
+ public void Invisible_Controls_Should_Not_Receive_Focus()
+ {
+ Button target;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = target = new Button() { IsVisible = false}
+ };
+
+ Assert.Null(FocusManager.Instance.Current);
+
+ target.Focus();
+
+ Assert.False(target.IsFocused);
+ Assert.False(target.IsKeyboardFocusWithin);
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
+
+ [Fact]
+ public void Effectively_Invisible_Controls_Should_Not_Receive_Focus()
+ {
+ var target = new Button();
+ Panel container;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = container = new Panel
+ {
+ IsVisible = false,
+ Children = { target }
+ }
+ };
+
+ Assert.Null(FocusManager.Instance.Current);
+
+ target.Focus();
+
+ Assert.False(target.IsFocused);
+ Assert.False(target.IsKeyboardFocusWithin);
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
+
+ [Fact]
+ public void Trying_To_Focus_Invisible_Control_Should_Not_Change_Focus()
+ {
+ Button first;
+ Button second;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ (first = new Button()),
+ (second = new Button() { IsVisible = false}),
+ }
+ }
+ };
+
+ first.Focus();
+
+ Assert.Same(first, FocusManager.Instance.Current);
+
+ second.Focus();
+
+ Assert.Same(first, FocusManager.Instance.Current);
+ }
+ }
+
+ [Fact]
+ public void Disabled_Controls_Should_Not_Receive_Focus()
+ {
+ Button target;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = target = new Button() { IsEnabled = false }
+ };
+
+ Assert.Null(FocusManager.Instance.Current);
+
+ target.Focus();
+
+ Assert.False(target.IsFocused);
+ Assert.False(target.IsKeyboardFocusWithin);
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
+
+ [Fact]
+ public void Effectively_Disabled_Controls_Should_Not_Receive_Focus()
+ {
+ var target = new Button();
+ Panel container;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = container = new Panel
+ {
+ IsEnabled = false,
+ Children = { target }
+ }
+ };
+
+ Assert.Null(FocusManager.Instance.Current);
+
+ target.Focus();
+
+ Assert.False(target.IsFocused);
+ Assert.False(target.IsKeyboardFocusWithin);
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
[Fact]
public void Focus_Should_Not_Get_Restored_To_Enabled_Control()
@@ -54,6 +186,90 @@ namespace Avalonia.Base.UnitTests.Input
}
}
+ [Fact]
+ public void Focus_Should_Be_Cleared_When_Control_Is_Hidden()
+ {
+ Button target;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = target = new Button()
+ };
+
+ target.Focus();
+ target.IsVisible = false;
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
+
+ [Fact(Skip = "Need to implement IsEffectivelyVisible change notifications.")]
+ public void Focus_Should_Be_Cleared_When_Control_Is_Effectively_Hidden()
+ {
+ Border container;
+ Button target;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = container = new Border
+ {
+ Child = target = new Button(),
+ }
+ };
+
+ target.Focus();
+ container.IsVisible = false;
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
+
+ [Fact]
+ public void Focus_Should_Be_Cleared_When_Control_Is_Disabled()
+ {
+ Button target;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = target = new Button()
+ };
+
+ target.Focus();
+ target.IsEnabled = false;
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
+
+ [Fact]
+ public void Focus_Should_Be_Cleared_When_Control_Is_Effectively_Disabled()
+ {
+ Border container;
+ Button target;
+
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var root = new TestRoot
+ {
+ Child = container = new Border
+ {
+ Child = target = new Button(),
+ }
+ };
+
+ target.Focus();
+ container.IsEnabled = false;
+
+ Assert.Null(FocusManager.Instance.Current);
+ }
+ }
+
[Fact]
public void Focus_Should_Be_Cleared_When_Control_Is_Removed_From_VisualTree()
{
@@ -78,8 +294,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
- var target1 = new Decorator();
- var target2 = new Decorator();
+ var target1 = new Decorator { Focusable = true };
+ var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@@ -115,8 +331,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
- var target1 = new Decorator();
- var target2 = new Decorator();
+ var target1 = new Decorator { Focusable = true };
+ var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@@ -157,8 +373,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
- var target1 = new Decorator();
- var target2 = new Decorator();
+ var target1 = new Decorator { Focusable = true };
+ var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@@ -190,8 +406,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
- var target1 = new Decorator();
- var target2 = new Decorator();
+ var target1 = new Decorator { Focusable = true };
+ var target2 = new Decorator { Focusable = true };
var panel1 = new Panel { Children = { target1 } };
var panel2 = new Panel { Children = { target2 } };
var root = new TestRoot
@@ -245,8 +461,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
- var target1 = new Decorator();
- var target2 = new Decorator();
+ var target1 = new Decorator { Focusable = true };
+ var target2 = new Decorator { Focusable = true };
var root = new TestRoot
{
Child = new StackPanel
@@ -290,8 +506,8 @@ namespace Avalonia.Base.UnitTests.Input
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
- var target1 = new Decorator();
- var target2 = new Decorator();
+ var target1 = new Decorator { Focusable = true };
+ var target2 = new Decorator { Focusable = true };
var root1 = new TestRoot
{
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