diff --git a/Avalonia.sln b/Avalonia.sln
index a216229a5b..1e2a3c6027 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -39,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
+ src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
@@ -55,8 +56,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.AndroidTestApplication", "src\Android\Avalonia.AndroidTestApplication\Avalonia.AndroidTestApplication.csproj", "{FF69B927-C545-49AE-8E16-3D14D621AA12}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}"
@@ -113,6 +112,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\SharedVersion.props = build\SharedVersion.props
build\SharpDX.props = build\SharpDX.props
build\SkiaSharp.props = build\SkiaSharp.props
+ build\SourceGenerators.props = build\SourceGenerators.props
build\SourceLink.props = build\SourceLink.props
build\System.Drawing.Common.props = build\System.Drawing.Common.props
build\System.Memory.props = build\System.Memory.props
@@ -209,10 +209,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
EndProject
Global
@@ -629,22 +631,6 @@ Global
{7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU
{7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhone.ActiveCfg = Release|Any CPU
{7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhone.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhone.ActiveCfg = Debug|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Build.0 = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Deploy.0 = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhone.ActiveCfg = Release|Any CPU
- {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
@@ -1929,6 +1915,30 @@ Global
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.Build.0 = Release|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
@@ -1970,7 +1980,6 @@ Global
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
- {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}
{E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props
index 28959dbd47..14e4f6a563 100644
--- a/build/DevAnalyzers.props
+++ b/build/DevAnalyzers.props
@@ -1,6 +1,6 @@
-
+
+
+
+
+
diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm
index 79175d9ff1..05b129baca 100644
--- a/native/Avalonia.Native/src/OSX/app.mm
+++ b/native/Avalonia.Native/src/OSX/app.mm
@@ -73,6 +73,11 @@ ComPtr _events;
_isHandlingSendEvent = true;
@try {
[super sendEvent: event];
+ if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand))
+ {
+ [[self keyWindow] sendEvent:event];
+ }
+
} @finally {
_isHandlingSendEvent = oldHandling;
}
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index d16c466fe6..4426e7fdff 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -457,7 +457,7 @@ public:
}
point = ConvertPointY(point);
- NSRect convertRect = [Window convertRectToScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)];
+ NSRect convertRect = [Window convertRectFromScreen:NSMakeRect(point.X, point.Y, 0.0, 0.0)];
auto viewPoint = NSMakePoint(convertRect.origin.x, convertRect.origin.y);
*ret = [View translateLocalPoint:ToAvnPoint(viewPoint)];
diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index 9fcb9d6b7f..f0f677b844 100644
--- a/nukebuild/Build.cs
+++ b/nukebuild/Build.cs
@@ -221,6 +221,7 @@ partial class Build : NukeBuild
RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
+ RunCoreTest("Avalonia.PlatformSupport.UnitTests");
});
Target RunRenderTests => _ => _
diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
index 8d2ead054c..04c67e84e8 100644
--- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
+++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj
@@ -39,49 +39,9 @@
-
+
-
- {7b92af71-6287-4693-9dcb-bd5b6e927e23}
- Avalonia.Android
-
-
- {b09b78d8-9b26-48b0-9149-d64a2f120f3f}
- Avalonia.Base
-
-
- {d2221c82-4a25-4583-9b43-d791e3f6820c}
- Avalonia.Controls
-
-
- {7062ae20-5dcc-4442-9645-8195bdece63e}
- Avalonia.Diagnostics
-
-
- {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}
- Avalonia.Themes.Default
-
-
- {3e53a01a-b331-47f3-b828-4a5717e77a24}
- Avalonia.Markup.Xaml
-
-
- {6417e941-21bc-467b-a771-0de389353ce6}
- Avalonia.Markup
-
-
- {7d2d3083-71dd-4cc9-8907-39a0d86fb322}
- Avalonia.Skia
-
-
- {d0a739b9-3c68-4ba6-a328-41606954b6bd}
- ControlCatalog
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index d1b657722c..2b45ac1508 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -6,7 +6,7 @@
true
-
+
true
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json
7.0.0-*
@@ -22,12 +22,11 @@
-
+
-
diff --git a/samples/ControlCatalog.NetCore/rd.xml b/samples/ControlCatalog.NetCore/rd.xml
deleted file mode 100644
index 27db7f34ca..0000000000
--- a/samples/ControlCatalog.NetCore/rd.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
index d4f10b3580..513ac44f83 100644
--- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
+++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
@@ -7,59 +7,10 @@
True
iossimulator-x64
-
+
-
-
-
-
-
-
-
-
- {4488ad85-1495-4809-9aa4-ddfe0a48527e}
- Avalonia.iOS
-
-
- {3E53A01A-B331-47F3-B828-4A5717E77A24}
- Avalonia.Markup.Xaml
-
-
- {6417E941-21BC-467B-A771-0DE389353CE6}
- Avalonia.Markup
-
-
- {B09B78D8-9B26-48B0-9149-D64A2F120F3F}
- Avalonia.Base
-
-
- {D2221C82-4A25-4583-9B43-D791E3F6820C}
- Avalonia.Controls
-
-
- {7062AE20-5DCC-4442-9645-8195BDECE63E}
- Avalonia.Diagnostics
-
-
- {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}
- Avalonia.Themes.Default
-
-
- {7d2d3083-71dd-4cc9-8907-39a0d86fb322}
- Avalonia.Skia
-
-
- {d0a739b9-3c68-4ba6-a328-41606954b6bd}
- ControlCatalog
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
deleted file mode 100644
index ee41b9f354..0000000000
--- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
- net6.0-android
- 21
- Exe
- enable
- com.Avalonia.AndroidTestApplication
- 1
- 1.0
- apk
- true
- portable
-
-
-
-
- Resources\drawable\Icon.png
-
-
-
-
- True
- True
- True
- True
-
-
-
-
-
-
- True
-
-
-
- True
-
-
-
-
-
-
-
-
- {7b92af71-6287-4693-9dcb-bd5b6e927e23}
- Avalonia.Android
-
-
- {3e53a01a-b331-47f3-b828-4a5717e77a24}
- Avalonia.Markup.Xaml
-
-
- {b09b78d8-9b26-48b0-9149-d64a2f120f3f}
- Avalonia.Base
-
-
- {d2221c82-4a25-4583-9b43-d791e3f6820c}
- Avalonia.Controls
-
-
- {7062ae20-5dcc-4442-9645-8195bdece63e}
- Avalonia.Diagnostics
-
-
- {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}
- Avalonia.Themes.Default
-
-
- {7d2d3083-71dd-4cc9-8907-39a0d86fb322}
- Avalonia.Skia
-
-
-
-
-
-
-
-
-
diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
deleted file mode 100644
index 8f4beb2737..0000000000
--- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs
+++ /dev/null
@@ -1,104 +0,0 @@
-using System;
-using Android.App;
-using Android.Content.PM;
-using Avalonia.Android;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Input.TextInput;
-using Avalonia.Markup.Xaml;
-using Avalonia.Media;
-using Avalonia.Styling;
-using Avalonia.Themes.Default;
-
-namespace Avalonia.AndroidTestApplication
-{
- [Activity(Label = "Main",
- MainLauncher = true,
- Icon = "@drawable/icon",
- Theme = "@style/Theme.AppCompat.NoActionBar",
- ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize,
- LaunchMode = LaunchMode.SingleInstance/*,
- ScreenOrientation = ScreenOrientation.Landscape*/)]
- public class MainActivity : AvaloniaActivity
- {
- protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
- {
- return base.CustomizeAppBuilder(builder);
- }
- }
-
- public class App : Application
- {
- public override void Initialize()
- {
- Styles.Add(new SimpleTheme(new Uri("avares://Avalonia.AndroidTestApplication")));
- }
-
- public override void OnFrameworkInitializationCompleted()
- {
- if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
- {
- singleViewLifetime.MainView = CreateSimpleWindow();
- }
-
- base.OnFrameworkInitializationCompleted();
- }
-
- // This provides a simple UI tree for testing input handling, drawing, etc
- public static ContentControl CreateSimpleWindow()
- {
- ContentControl window = new ContentControl()
- {
- Background = Brushes.Red,
- Content = new StackPanel
- {
- Margin = new Thickness(30),
- Background = Brushes.Yellow,
- Children =
- {
- new TextBlock
- {
- Text = "TEXT BLOCK",
- Width = 300,
- Height = 40,
- Background = Brushes.White,
- Foreground = Brushes.Black
- },
-
- new Button
- {
- Content = "BUTTON",
- Width = 150,
- Height = 40,
- Background = Brushes.LightGreen,
- Foreground = Brushes.Black
- },
-
- CreateTextBox(TextInputContentType.Normal),
- CreateTextBox(TextInputContentType.Password),
- CreateTextBox(TextInputContentType.Email),
- CreateTextBox(TextInputContentType.Url),
- CreateTextBox(TextInputContentType.Digits),
- CreateTextBox(TextInputContentType.Number),
- }
- }
- };
-
- return window;
- }
-
- private static TextBox CreateTextBox(TextInputContentType contentType)
- {
- var textBox = new TextBox()
- {
- Margin = new Thickness(20, 10),
- Watermark = contentType.ToString(),
- BorderThickness = new Thickness(3),
- FontSize = 20,
- [TextInputOptions.ContentTypeProperty] = contentType
- };
-
- return textBox;
- }
- }
-}
diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
deleted file mode 100644
index ad8134f628..0000000000
--- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt b/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt
deleted file mode 100644
index 194ae28a59..0000000000
--- a/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-Images, layout descriptions, binary blobs and string dictionaries can be included
-in your application as resource files. Various Android APIs are designed to
-operate on the resource IDs instead of dealing with images, strings or binary blobs
-directly.
-
-For example, a sample Android app that contains a user interface layout (main.xml),
-an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
-would keep its resources in the "Resources" directory of the application:
-
-Resources/
- drawable-hdpi/
- icon.png
-
- drawable-ldpi/
- icon.png
-
- drawable-mdpi/
- icon.png
-
- layout/
- main.xml
-
- values/
- strings.xml
-
-In order to get the build system to recognize Android resources, set the build action to
-"AndroidResource". The native Android APIs do not operate directly with filenames, but
-instead operate on resource IDs. When you compile an Android application that uses resources,
-the build system will package the resources for distribution and generate a class called
-"Resource" that contains the tokens for each one of the resources included. For example,
-for the above Resources layout, this is what the Resource class would expose:
-
-public class Resource {
- public class drawable {
- public const int icon = 0x123;
- }
-
- public class layout {
- public const int main = 0x456;
- }
-
- public class strings {
- public const int first_string = 0xabc;
- public const int second_string = 0xbcd;
- }
-}
-
-You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main
-to reference the layout/main.xml file, or Resource.strings.first_string to reference the first
-string in the dictionary file values/strings.xml.
\ No newline at end of file
diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png b/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png
deleted file mode 100644
index 8074c4c571..0000000000
Binary files a/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png and /dev/null differ
diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml b/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml
deleted file mode 100644
index c8dca13c35..0000000000
--- a/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
- Hello World, Click Me!
- Avalonia.AndroidTestApplication
-
\ No newline at end of file
diff --git a/src/Avalonia.Base/Animation/Easings/Easing.cs b/src/Avalonia.Base/Animation/Easings/Easing.cs
index c721772f3e..d4f817ccc3 100644
--- a/src/Avalonia.Base/Animation/Easings/Easing.cs
+++ b/src/Avalonia.Base/Animation/Easings/Easing.cs
@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using System.Linq;
+using Avalonia.SourceGenerator;
namespace Avalonia.Animation.Easings
{
@@ -10,14 +11,15 @@ namespace Avalonia.Animation.Easings
/// Base class for all Easing classes.
///
[TypeConverter(typeof(EasingTypeConverter))]
- public abstract class Easing : IEasing
+ public abstract partial class Easing : IEasing
{
///
public abstract double Ease(double progress);
- static Dictionary? _easingTypes;
+ private const string Namespace = "Avalonia.Animation.Easings";
- static readonly Type s_thisType = typeof(Easing);
+ [SubtypesFactory(typeof(Easing), Namespace)]
+ private static partial bool TryCreateEasingInstance(string type, [NotNullWhen(true)] out Easing? instance);
///
/// Parses a Easing type string.
@@ -26,34 +28,18 @@ namespace Avalonia.Animation.Easings
/// Returns the instance of the parsed type.
public static Easing Parse(string e)
{
+#if NETSTANDARD2_0
+ if (e.Contains(","))
+#else
if (e.Contains(','))
+#endif
{
return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
}
- if (_easingTypes == null)
- {
- _easingTypes = new Dictionary();
-
- // Fetch the built-in easings.
- var derivedTypes = typeof(Easing).Assembly.GetTypes()
- .Where(p => p.Namespace == s_thisType.Namespace)
- .Where(p => p.IsSubclassOf(s_thisType))
- .Select(p => p);
-
- foreach (var easingType in derivedTypes)
- _easingTypes.Add(easingType.Name, easingType);
- }
-
- if (_easingTypes.ContainsKey(e))
- {
- var type = _easingTypes[e];
- return (Easing)Activator.CreateInstance(type)!;
- }
- else
- {
- throw new FormatException($"Easing \"{e}\" was not found in {s_thisType.Namespace} namespace.");
- }
+ return TryCreateEasingInstance(e, out var easing)
+ ? easing
+ : throw new FormatException($"Easing \"{e}\" was not found in {Namespace} namespace.");
}
}
}
diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt
index 4701a83175..7f378d2f65 100644
--- a/src/Avalonia.Base/ApiCompatBaseline.txt
+++ b/src/Avalonia.Base/ApiCompatBaseline.txt
@@ -1,3 +1,4 @@
Compat issues with assembly Avalonia.Base:
+MembersMustExist : Member 'public System.Int32 System.Int32 Avalonia.Threading.DispatcherPriority.value__' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post(System.Action, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract.
-Total Issues: 1
+Total Issues: 2
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 5b79d3cd7b..8e4755b4b7 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -16,4 +16,5 @@
+
diff --git a/src/Avalonia.Base/Input/IInputRoot.cs b/src/Avalonia.Base/Input/IInputRoot.cs
index 3e2b8cc477..98e8699573 100644
--- a/src/Avalonia.Base/Input/IInputRoot.cs
+++ b/src/Avalonia.Base/Input/IInputRoot.cs
@@ -1,5 +1,3 @@
-using JetBrains.Annotations;
-
namespace Avalonia.Input
{
///
@@ -30,7 +28,6 @@ namespace Avalonia.Input
///
/// Gets associated mouse device
///
- [CanBeNull]
IMouseDevice? MouseDevice { get; }
}
}
diff --git a/src/Avalonia.Base/Input/IKeyboardDevice.cs b/src/Avalonia.Base/Input/IKeyboardDevice.cs
index 9506dc36fb..d0e84e5ad0 100644
--- a/src/Avalonia.Base/Input/IKeyboardDevice.cs
+++ b/src/Avalonia.Base/Input/IKeyboardDevice.cs
@@ -50,12 +50,6 @@ namespace Avalonia.Input
KeyboardMask = Alt | Control | Shift | Meta
}
- internal static class KeyModifiersUtils
- {
- public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) =>
- (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
- }
-
public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged
{
IInputElement? FocusedElement { get; }
diff --git a/src/Avalonia.Base/Input/IMouseDevice.cs b/src/Avalonia.Base/Input/IMouseDevice.cs
index 272d1eb8d7..6b7f0e76e5 100644
--- a/src/Avalonia.Base/Input/IMouseDevice.cs
+++ b/src/Avalonia.Base/Input/IMouseDevice.cs
@@ -13,8 +13,10 @@ namespace Avalonia.Input
[Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; }
+ [Obsolete]
void TopLevelClosed(IInputRoot root);
+ [Obsolete]
void SceneInvalidated(IInputRoot root, Rect rect);
}
}
diff --git a/src/Avalonia.Base/Input/IPointerDevice.cs b/src/Avalonia.Base/Input/IPointerDevice.cs
index 1f82cb1ed7..0096bb77bf 100644
--- a/src/Avalonia.Base/Input/IPointerDevice.cs
+++ b/src/Avalonia.Base/Input/IPointerDevice.cs
@@ -1,17 +1,31 @@
using System;
using Avalonia.VisualTree;
+using Avalonia.Input.Raw;
namespace Avalonia.Input
{
public interface IPointerDevice : IInputDevice
{
+ ///
[Obsolete("Use IPointer")]
IInputElement? Captured { get; }
-
+
+ ///
[Obsolete("Use IPointer")]
void Capture(IInputElement? control);
+ ///
[Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo);
+
+ ///
+ /// Gets a pointer for specific event args.
+ ///
+ ///
+ /// If pointer doesn't exist or wasn't yet created this method will return null.
+ ///
+ /// Raw pointer event args associated with the pointer.
+ /// The pointer.
+ IPointer? TryGetPointer(RawPointerEventArgs ev);
}
}
diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs
index 0adbe73263..3b7a828b86 100644
--- a/src/Avalonia.Base/Input/KeyGesture.cs
+++ b/src/Avalonia.Base/Input/KeyGesture.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
+using Avalonia.Utilities;
namespace Avalonia.Input
{
@@ -155,7 +156,7 @@ namespace Avalonia.Input
if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv))
return rv;
- return (Key)Enum.Parse(typeof(Key), key, true);
+ return EnumHelper.Parse(key, true);
}
private static KeyModifiers ParseModifier(ReadOnlySpan modifier)
@@ -172,7 +173,7 @@ namespace Avalonia.Input
return KeyModifiers.Meta;
}
- return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true);
+ return EnumHelper.Parse(modifier.ToString(), true);
}
private Key ResolveNumPadOperationKey(Key key)
diff --git a/src/Avalonia.Base/Input/KeyboardDevice.cs b/src/Avalonia.Base/Input/KeyboardDevice.cs
index 3df717b8c4..0600b54618 100644
--- a/src/Avalonia.Base/Input/KeyboardDevice.cs
+++ b/src/Avalonia.Base/Input/KeyboardDevice.cs
@@ -188,7 +188,7 @@ namespace Avalonia.Input
RoutedEvent = routedEvent,
Device = this,
Key = keyInput.Key,
- KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers),
+ KeyModifiers = keyInput.Modifiers.ToKeyModifiers(),
Source = element,
};
diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs
index a5d54bb047..5f8ab24b79 100644
--- a/src/Avalonia.Base/Input/MouseDevice.cs
+++ b/src/Avalonia.Base/Input/MouseDevice.cs
@@ -21,27 +21,17 @@ namespace Avalonia.Input
private readonly Pointer _pointer;
private bool _disposed;
- private PixelPoint? _position;
+ private PixelPoint? _position;
+ private MouseButton _lastMouseDownButton;
public MouseDevice(Pointer? pointer = null)
{
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
}
-
- ///
- /// Gets the control that is currently capturing by the mouse, if any.
- ///
- ///
- /// When an element captures the mouse, it receives mouse input whether the cursor is
- /// within the control's bounds or not. To set the mouse capture, call the
- /// method.
- ///
+
[Obsolete("Use IPointer instead")]
public IInputElement? Captured => _pointer.Captured;
- ///
- /// Gets the mouse position, in screen coordinates.
- ///
[Obsolete("Use events instead")]
public PixelPoint Position
{
@@ -49,15 +39,7 @@ namespace Avalonia.Input
protected set => _position = value;
}
- ///
- /// Captures mouse input to the specified control.
- ///
- /// The control.
- ///
- /// When an element captures the mouse, it receives mouse input whether the cursor is
- /// within the control's bounds or not. The current mouse capture control is exposed
- /// by the property.
- ///
+ [Obsolete("Use IPointer instead")]
public void Capture(IInputElement? control)
{
_pointer.Capture(control);
@@ -90,39 +72,6 @@ namespace Avalonia.Input
ProcessRawEvent(margs);
}
- public void TopLevelClosed(IInputRoot root)
- {
- ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None);
- }
-
- public void SceneInvalidated(IInputRoot root, Rect rect)
- {
- // Pointer is outside of the target area
- if (_position == null )
- {
- if (root.PointerOverElement != null)
- ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None);
- return;
- }
-
-
- var clientPoint = root.PointToClient(_position.Value);
-
- if (rect.Contains(clientPoint))
- {
- if (_pointer.Captured == null)
- {
- SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint,
- PointerPointProperties.None, KeyModifiers.None);
- }
- else
- {
- SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured,
- PointerPointProperties.None, KeyModifiers.None);
- }
- }
- }
-
int ButtonCount(PointerPointProperties props)
{
var rv = 0;
@@ -138,7 +87,7 @@ namespace Avalonia.Input
rv++;
return rv;
}
-
+
private void ProcessRawEvent(RawPointerEventArgs e)
{
e = e ?? throw new ArgumentNullException(nameof(e));
@@ -147,15 +96,14 @@ namespace Avalonia.Input
if(mouse._disposed)
return;
- if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return;
-
_position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e);
- var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers);
+ var keyModifiers = e.InputModifiers.ToKeyModifiers();
switch (e.Type)
{
case RawPointerEventType.LeaveWindow:
- LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers);
+ case RawPointerEventType.NonClientLeftButtonDown:
+ LeaveWindow();
break;
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
@@ -163,10 +111,9 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1)
- e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
else
- e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
- props, keyModifiers);
+ e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp:
@@ -174,82 +121,50 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0)
- e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
else
- e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers);
+ e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Move:
- e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
break;
case RawPointerEventType.Wheel:
- e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers);
+ e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Magnify:
- e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
+ e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Rotate:
- e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
+ e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
case RawPointerEventType.Swipe:
- e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers);
+ e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
break;
}
}
- private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties,
- KeyModifiers inputModifiers)
+ private void LeaveWindow()
{
- device = device ?? throw new ArgumentNullException(nameof(device));
- root = root ?? throw new ArgumentNullException(nameof(root));
-
_position = null;
- ClearPointerOver(this, timestamp, root, properties, inputModifiers);
}
-
PointerPointProperties CreateProperties(RawPointerEventArgs args)
{
-
- var kind = PointerUpdateKind.Other;
-
- if (args.Type == RawPointerEventType.LeftButtonDown)
- kind = PointerUpdateKind.LeftButtonPressed;
- if (args.Type == RawPointerEventType.MiddleButtonDown)
- kind = PointerUpdateKind.MiddleButtonPressed;
- if (args.Type == RawPointerEventType.RightButtonDown)
- kind = PointerUpdateKind.RightButtonPressed;
- if (args.Type == RawPointerEventType.XButton1Down)
- kind = PointerUpdateKind.XButton1Pressed;
- if (args.Type == RawPointerEventType.XButton2Down)
- kind = PointerUpdateKind.XButton2Pressed;
- if (args.Type == RawPointerEventType.LeftButtonUp)
- kind = PointerUpdateKind.LeftButtonReleased;
- if (args.Type == RawPointerEventType.MiddleButtonUp)
- kind = PointerUpdateKind.MiddleButtonReleased;
- if (args.Type == RawPointerEventType.RightButtonUp)
- kind = PointerUpdateKind.RightButtonReleased;
- if (args.Type == RawPointerEventType.XButton1Up)
- kind = PointerUpdateKind.XButton1Released;
- if (args.Type == RawPointerEventType.XButton2Up)
- kind = PointerUpdateKind.XButton2Released;
-
- return new PointerPointProperties(args.InputModifiers, kind);
+ return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind());
}
- private MouseButton _lastMouseDownButton;
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
PointerPointProperties properties,
- KeyModifiers inputModifiers)
+ KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
- var hit = HitTest(root, p);
+ var source = _pointer.Captured ?? root.InputHitTest(p);
- if (hit != null)
+ if (source != null)
{
- _pointer.Capture(hit);
- var source = GetSource(hit);
+ _pointer.Capture(source);
if (source != null)
{
var settings = AvaloniaLocator.Current.GetService();
@@ -275,23 +190,14 @@ namespace Avalonia.Input
return false;
}
- private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
- KeyModifiers inputModifiers, Lazy?>? intermediatePoints)
+ private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
+ PointerPointProperties properties, KeyModifiers inputModifiers, Lazy?>? intermediatePoints,
+ IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
- IInputElement? source;
-
- if (_pointer.Captured == null)
- {
- source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers);
- }
- else
- {
- SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers);
- source = _pointer.Captured;
- }
+ var source = _pointer.Captured ?? hitTest;
if (source is object)
{
@@ -306,13 +212,12 @@ namespace Avalonia.Input
}
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
- KeyModifiers inputModifiers)
+ KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
- var hit = HitTest(root, p);
- var source = GetSource(hit);
+ var source = _pointer.Captured ?? hitTest;
if (source is not null)
{
@@ -329,13 +234,12 @@ namespace Avalonia.Input
private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props,
- Vector delta, KeyModifiers inputModifiers)
+ Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
- var hit = HitTest(root, p);
- var source = GetSource(hit);
+ var source = _pointer.Captured ?? hitTest;
// KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform.
// If Shift-Key is pressed and X is close to 0 we swap the Vector.
@@ -356,16 +260,15 @@ namespace Avalonia.Input
}
private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
- PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
+ PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
- var hit = HitTest(root, p);
+ var source = _pointer.Captured ?? hitTest;
- if (hit != null)
+ if (source != null)
{
- var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
@@ -377,16 +280,15 @@ namespace Avalonia.Input
}
private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
- PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
+ PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
- var hit = HitTest(root, p);
+ var source = _pointer.Captured ?? hitTest;
- if (hit != null)
+ if (source != null)
{
- var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
@@ -398,16 +300,15 @@ namespace Avalonia.Input
}
private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
- PointerPointProperties props, Vector delta, KeyModifiers inputModifiers)
+ PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest)
{
device = device ?? throw new ArgumentNullException(nameof(device));
root = root ?? throw new ArgumentNullException(nameof(root));
- var hit = HitTest(root, p);
+ var source = _pointer.Captured ?? hitTest;
- if (hit != null)
+ if (source != null)
{
- var source = GetSource(hit);
var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source,
_pointer, root, p, timestamp, props, inputModifiers, delta);
@@ -418,154 +319,27 @@ namespace Avalonia.Input
return false;
}
- private IInteractive? GetSource(IVisual? hit)
- {
- if (hit is null)
- return null;
-
- return _pointer.Captured ??
- (hit as IInteractive) ??
- hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault();
- }
-
- private IInputElement? HitTest(IInputElement root, Point p)
- {
- root = root ?? throw new ArgumentNullException(nameof(root));
-
- return _pointer.Captured ?? root.InputHitTest(p);
- }
-
- PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source,
- PointerPointProperties properties,
- KeyModifiers inputModifiers)
- {
- return new PointerEventArgs(ev, source, _pointer, null, default,
- timestamp, properties, inputModifiers);
- }
-
- private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root,
- PointerPointProperties properties,
- KeyModifiers inputModifiers)
- {
- device = device ?? throw new ArgumentNullException(nameof(device));
- root = root ?? throw new ArgumentNullException(nameof(root));
-
- var element = root.PointerOverElement;
- var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers);
-
- if (element!=null && !element.IsAttachedToVisualTree)
- {
- // element has been removed from visual tree so do top down cleanup
- if (root.IsPointerOver)
- ClearChildrenPointerOver(e, root,true);
- }
- while (element != null)
- {
- e.Source = element;
- e.Handled = false;
- element.RaiseEvent(e);
- element = (IInputElement?)element.VisualParent;
- }
-
- root.PointerOverElement = null;
- }
-
- private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot)
+ public void Dispose()
{
- foreach (IInputElement el in element.VisualChildren)
- {
- if (el.IsPointerOver)
- {
- ClearChildrenPointerOver(e, el, true);
- break;
- }
- }
- if(clearRoot)
- {
- e.Source = element;
- e.Handled = false;
- element.RaiseEvent(e);
- }
+ _disposed = true;
+ _pointer?.Dispose();
}
- private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p,
- PointerPointProperties properties,
- KeyModifiers inputModifiers)
+ [Obsolete]
+ public void TopLevelClosed(IInputRoot root)
{
- device = device ?? throw new ArgumentNullException(nameof(device));
- root = root ?? throw new ArgumentNullException(nameof(root));
-
- var element = root.InputHitTest(p);
-
- if (element != root.PointerOverElement)
- {
- if (element != null)
- {
- SetPointerOver(device, timestamp, root, element, properties, inputModifiers);
- }
- else
- {
- ClearPointerOver(device, timestamp, root, properties, inputModifiers);
- }
- }
-
- return element;
+ // no-op
}
- private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element,
- PointerPointProperties properties,
- KeyModifiers inputModifiers)
+ [Obsolete]
+ public void SceneInvalidated(IInputRoot root, Rect rect)
{
- device = device ?? throw new ArgumentNullException(nameof(device));
- root = root ?? throw new ArgumentNullException(nameof(root));
- element = element ?? throw new ArgumentNullException(nameof(element));
-
- IInputElement? branch = null;
-
- IInputElement? el = element;
-
- while (el != null)
- {
- if (el.IsPointerOver)
- {
- branch = el;
- break;
- }
- el = (IInputElement?)el.VisualParent;
- }
-
- el = root.PointerOverElement;
-
- var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers);
- if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
- {
- ClearChildrenPointerOver(e,branch,false);
- }
-
- while (el != null && el != branch)
- {
- e.Source = el;
- e.Handled = false;
- el.RaiseEvent(e);
- el = (IInputElement?)el.VisualParent;
- }
-
- el = root.PointerOverElement = element;
- e.RoutedEvent = InputElement.PointerEnterEvent;
-
- while (el != null && el != branch)
- {
- e.Source = el;
- e.Handled = false;
- el.RaiseEvent(e);
- el = (IInputElement?)el.VisualParent;
- }
+ // no-op
}
- public void Dispose()
+ public IPointer? TryGetPointer(RawPointerEventArgs ev)
{
- _disposed = true;
- _pointer?.Dispose();
+ return _pointer;
}
}
}
diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs
index 0604d09dc4..5495802920 100644
--- a/src/Avalonia.Base/Input/PointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/PointerEventArgs.cs
@@ -63,6 +63,8 @@ namespace Avalonia.Input
}
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
+
+ public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer;
}
public IPointer Pointer { get; }
diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
new file mode 100644
index 0000000000..d22252893d
--- /dev/null
+++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
@@ -0,0 +1,209 @@
+using System;
+using Avalonia.Input.Raw;
+
+namespace Avalonia.Input
+{
+ internal class PointerOverPreProcessor : IObserver
+ {
+ private IPointerDevice? _lastActivePointerDevice;
+ private (IPointer pointer, PixelPoint position)? _lastPointer;
+
+ private readonly IInputRoot _inputRoot;
+
+ public PointerOverPreProcessor(IInputRoot inputRoot)
+ {
+ _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
+ }
+
+ public void OnCompleted()
+ {
+ ClearPointerOver();
+ }
+
+ public void OnError(Exception error)
+ {
+ }
+
+ public void OnNext(RawInputEventArgs value)
+ {
+ if (value is RawPointerEventArgs args
+ && args.Root == _inputRoot
+ && value.Device is IPointerDevice pointerDevice)
+ {
+ if (pointerDevice != _lastActivePointerDevice)
+ {
+ ClearPointerOver();
+
+ // Set last active device before processing input, because ClearPointerOver might be called and clear last device.
+ _lastActivePointerDevice = pointerDevice;
+ }
+
+ if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
+ && _lastPointer is (var lastPointer, var lastPosition))
+ {
+ _lastPointer = null;
+ ClearPointerOver(lastPointer, args.Root, 0, args.Root.PointToClient(lastPosition),
+ new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
+ args.InputModifiers.ToKeyModifiers());
+ }
+ else if (pointerDevice.TryGetPointer(args) is IPointer pointer
+ && pointer.Type != PointerType.Touch)
+ {
+ var element = pointer.Captured ?? args.InputHitTestResult;
+
+ SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position,
+ new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
+ args.InputModifiers.ToKeyModifiers());
+ }
+ }
+ }
+
+ public void SceneInvalidated(Rect dirtyRect)
+ {
+ if (_lastPointer is (var pointer, var position))
+ {
+ var clientPoint = _inputRoot.PointToClient(position);
+
+ if (dirtyRect.Contains(clientPoint))
+ {
+ SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
+ }
+ else if (!_inputRoot.Bounds.Contains(clientPoint))
+ {
+ ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None);
+ }
+ }
+ }
+
+ private void ClearPointerOver()
+ {
+ if (_lastPointer is (var pointer, var _))
+ {
+ ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None);
+ }
+ _lastPointer = null;
+ _lastActivePointerDevice = null;
+ }
+
+ private void ClearPointerOver(IPointer pointer, IInputRoot root,
+ ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
+ {
+ var element = root.PointerOverElement;
+ if (element is null)
+ {
+ return;
+ }
+
+ // Do not pass rootVisual, when we have unknown (negative) position,
+ // so GetPosition won't return invalid values.
+ var hasPosition = position.X >= 0 && position.Y >= 0;
+ var e = new PointerEventArgs(InputElement.PointerLeaveEvent, element, pointer,
+ hasPosition ? root : null, hasPosition ? position : default,
+ timestamp, properties, inputModifiers);
+
+ if (element != null && !element.IsAttachedToVisualTree)
+ {
+ // element has been removed from visual tree so do top down cleanup
+ if (root.IsPointerOver)
+ {
+ ClearChildrenPointerOver(e, root, true);
+ }
+ }
+ while (element != null)
+ {
+ e.Source = element;
+ e.Handled = false;
+ element.RaiseEvent(e);
+ element = (IInputElement?)element.VisualParent;
+ }
+
+ root.PointerOverElement = null;
+ _lastActivePointerDevice = null;
+ _lastPointer = null;
+ }
+
+ private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot)
+ {
+ foreach (IInputElement el in element.VisualChildren)
+ {
+ if (el.IsPointerOver)
+ {
+ ClearChildrenPointerOver(e, el, true);
+ break;
+ }
+ }
+ if (clearRoot)
+ {
+ e.Source = element;
+ e.Handled = false;
+ element.RaiseEvent(e);
+ }
+ }
+
+ private void SetPointerOver(IPointer pointer, IInputRoot root, IInputElement? element,
+ ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
+ {
+ var pointerOverElement = root.PointerOverElement;
+
+ if (element != pointerOverElement)
+ {
+ if (element != null)
+ {
+ SetPointerOverToElement(pointer, root, element, timestamp, position, properties, inputModifiers);
+ }
+ else
+ {
+ ClearPointerOver(pointer, root, timestamp, position, properties, inputModifiers);
+ }
+ }
+ }
+
+ private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element,
+ ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
+ {
+ IInputElement? branch = null;
+
+ IInputElement? el = element;
+
+ while (el != null)
+ {
+ if (el.IsPointerOver)
+ {
+ branch = el;
+ break;
+ }
+ el = (IInputElement?)el.VisualParent;
+ }
+
+ el = root.PointerOverElement;
+
+ var e = new PointerEventArgs(InputElement.PointerLeaveEvent, el, pointer, root, position,
+ timestamp, properties, inputModifiers);
+ if (el != null && branch != null && !el.IsAttachedToVisualTree)
+ {
+ ClearChildrenPointerOver(e, branch, false);
+ }
+
+ while (el != null && el != branch)
+ {
+ e.Source = el;
+ e.Handled = false;
+ el.RaiseEvent(e);
+ el = (IInputElement?)el.VisualParent;
+ }
+
+ el = root.PointerOverElement = element;
+ _lastPointer = (pointer, root.PointToScreen(position));
+
+ e.RoutedEvent = InputElement.PointerEnterEvent;
+
+ while (el != null && el != branch)
+ {
+ e.Source = el;
+ e.Handled = false;
+ el.RaiseEvent(e);
+ el = (IInputElement?)el.VisualParent;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Input/Raw/RawDragEvent.cs b/src/Avalonia.Base/Input/Raw/RawDragEvent.cs
index 6e9ce20ff1..652bad7115 100644
--- a/src/Avalonia.Base/Input/Raw/RawDragEvent.cs
+++ b/src/Avalonia.Base/Input/Raw/RawDragEvent.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Input.Raw
Location = location;
Data = data;
Effects = effects;
- KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers);
+ KeyModifiers = modifiers.ToKeyModifiers();
#pragma warning disable CS0618 // Type or member is obsolete
Modifiers = (InputModifiers)modifiers;
#pragma warning restore CS0618 // Type or member is obsolete
diff --git a/src/Avalonia.Base/Input/Raw/RawInputHelpers.cs b/src/Avalonia.Base/Input/Raw/RawInputHelpers.cs
new file mode 100644
index 0000000000..9d329bae59
--- /dev/null
+++ b/src/Avalonia.Base/Input/Raw/RawInputHelpers.cs
@@ -0,0 +1,27 @@
+using Avalonia.Input.Raw;
+
+namespace Avalonia.Input
+{
+ internal static class RawInputHelpers
+ {
+ public static KeyModifiers ToKeyModifiers(this RawInputModifiers modifiers) =>
+ (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
+
+ public static PointerUpdateKind ToUpdateKind(this RawPointerEventType type) => type switch
+ {
+ RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed,
+ RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased,
+ RawPointerEventType.RightButtonDown => PointerUpdateKind.RightButtonPressed,
+ RawPointerEventType.RightButtonUp => PointerUpdateKind.RightButtonReleased,
+ RawPointerEventType.MiddleButtonDown => PointerUpdateKind.MiddleButtonPressed,
+ RawPointerEventType.MiddleButtonUp => PointerUpdateKind.MiddleButtonReleased,
+ RawPointerEventType.XButton1Down => PointerUpdateKind.XButton1Pressed,
+ RawPointerEventType.XButton1Up => PointerUpdateKind.XButton1Released,
+ RawPointerEventType.XButton2Down => PointerUpdateKind.XButton2Pressed,
+ RawPointerEventType.XButton2Up => PointerUpdateKind.XButton2Released,
+ RawPointerEventType.TouchBegin => PointerUpdateKind.LeftButtonPressed,
+ RawPointerEventType.TouchEnd => PointerUpdateKind.LeftButtonReleased,
+ _ => PointerUpdateKind.Other
+ };
+ }
+}
diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
index c157fa059c..8b9d7c161d 100644
--- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
@@ -120,6 +120,8 @@ namespace Avalonia.Input.Raw
/// only valid for Move and TouchUpdate
///
public Lazy?>? IntermediatePoints { get; set; }
+
+ internal IInputElement? InputHitTestResult { get; set; }
}
public struct RawPointerPoint
diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs
index 20cafb9e8e..54dcc4051e 100644
--- a/src/Avalonia.Base/Input/TouchDevice.cs
+++ b/src/Avalonia.Base/Input/TouchDevice.cs
@@ -3,24 +3,26 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.Platform;
+using Avalonia.VisualTree;
namespace Avalonia.Input
{
///
/// Handles raw touch events
+ ///
///
/// This class is supposed to be used on per-toplevel basis, don't use a shared one
///
- ///
- public class TouchDevice : IInputDevice, IDisposable
+ public class TouchDevice : IPointerDevice, IDisposable
{
private readonly Dictionary _pointers = new Dictionary();
private bool _disposed;
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
- KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) =>
- (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask);
+ private Pointer? _lastPointer;
+
+ IInputElement? IPointerDevice.Captured => _lastPointer?.Captured;
RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown)
{
@@ -30,6 +32,10 @@ namespace Avalonia.Input
return rv;
}
+ void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control);
+
+ Point IPointerDevice.GetPosition(IVisual relativeTo) => default;
+
public void ProcessRawEvent(RawInputEventArgs ev)
{
if (ev.Handled || _disposed)
@@ -39,15 +45,18 @@ namespace Avalonia.Input
{
if (args.Type == RawPointerEventType.TouchEnd)
return;
- var hit = args.Root.InputHitTest(args.Position);
+ var hit = args.InputHitTestResult;
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
-
+ _lastPointer = pointer;
var target = pointer.Captured ?? args.Root;
+ var updateKind = args.Type.ToUpdateKind();
+ var keyModifier = args.InputModifiers.ToKeyModifiers();
+
if (args.Type == RawPointerEventType.TouchBegin)
{
if (_pointers.Count > 1)
@@ -73,9 +82,8 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
- new PointerPointProperties(GetModifiers(args.InputModifiers, true),
- PointerUpdateKind.LeftButtonPressed),
- GetKeyModifiers(args.InputModifiers), _clickCount));
+ new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
+ keyModifier, _clickCount));
}
if (args.Type == RawPointerEventType.TouchEnd)
@@ -85,10 +93,10 @@ namespace Avalonia.Input
{
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
- new PointerPointProperties(GetModifiers(args.InputModifiers, false),
- PointerUpdateKind.LeftButtonReleased),
- GetKeyModifiers(args.InputModifiers), MouseButton.Left));
+ new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind),
+ keyModifier, MouseButton.Left));
}
+ _lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchCancel)
@@ -96,18 +104,16 @@ namespace Avalonia.Input
_pointers.Remove(args.TouchPointId);
using (pointer)
pointer.Capture(null);
+ _lastPointer = null;
}
if (args.Type == RawPointerEventType.TouchUpdate)
{
- var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp,
- new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other),
- GetKeyModifiers(args.InputModifiers), args.IntermediatePoints));
+ new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind),
+ keyModifier, args.IntermediatePoints));
}
-
-
}
public void Dispose()
@@ -121,5 +127,12 @@ namespace Avalonia.Input
p.Dispose();
}
+ public IPointer? TryGetPointer(RawPointerEventArgs ev)
+ {
+ return ev is RawTouchEventArgs args
+ && _pointers.TryGetValue(args.TouchPointId, out var pointer)
+ ? pointer
+ : null;
+ }
}
}
diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs
index eaa886ccbd..cb90404f6d 100644
--- a/src/Avalonia.Base/Media/Color.cs
+++ b/src/Avalonia.Base/Media/Color.cs
@@ -166,7 +166,10 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
+
+ if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@@ -175,7 +178,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@@ -185,7 +188,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@@ -229,7 +232,10 @@ namespace Avalonia.Media
// At this point all parsing uses strings
var str = s.ToString();
- if (s.Length > 5 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible CSS format is "rbg(0,0,0)", Length = 10.
+
+ if (s.Length >= 10 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
@@ -238,7 +244,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
@@ -248,7 +254,7 @@ namespace Avalonia.Media
return true;
}
- if (s.Length > 5 &&
+ if (s.Length >= 10 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
@@ -271,6 +277,9 @@ namespace Avalonia.Media
return false;
}
+ ///
+ /// Parses the given span of characters representing a hex color value into a new .
+ ///
private static bool TryParseHexFormat(ReadOnlySpan s, out Color color)
{
static bool TryParseCore(ReadOnlySpan input, ref Color color)
@@ -325,8 +334,13 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
+ ///
+ /// Parses the given string representing a CSS color value into a new .
+ ///
private static bool TryParseCssFormat(string s, out Color color)
{
+ bool prefixMatched = false;
+
color = default;
if (s is null)
@@ -342,27 +356,35 @@ namespace Avalonia.Media
return false;
}
- if (workingString.Length > 6 &&
+ if (workingString.Length >= 11 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
+ prefixMatched = true;
}
- if (workingString.Length > 5 &&
+ if (prefixMatched == false &&
+ workingString.Length >= 10 &&
workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
+ prefixMatched = true;
+ }
+
+ if (prefixMatched == false)
+ {
+ return false;
}
string[] components = workingString.Split(',');
if (components.Length == 3) // RGB
{
- if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
- byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
- byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue))
+ if (InternalTryParseByte(components[0], out byte red) &&
+ InternalTryParseByte(components[1], out byte green) &&
+ InternalTryParseByte(components[2], out byte blue))
{
color = new Color(0xFF, red, green, blue);
return true;
@@ -370,18 +392,45 @@ namespace Avalonia.Media
}
else if (components.Length == 4) // RGBA
{
- if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
- byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
- byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) &&
- TryInternalParse(components[3], out double alpha))
+ if (InternalTryParseByte(components[0], out byte red) &&
+ InternalTryParseByte(components[1], out byte green) &&
+ InternalTryParseByte(components[2], out byte blue) &&
+ InternalTryParseDouble(components[3], out double alpha))
{
- color = new Color((byte)(alpha * 255), red, green, blue);
+ color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue);
return true;
}
}
+ // Local function to specially parse a byte value with an optional percentage sign
+ bool InternalTryParseByte(string inString, out byte outByte)
+ {
+ // The percent sign, if it exists, must be at the end of the number
+ int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outByte = (byte)Math.Round((percentage / 100.0) * 255.0);
+ return result;
+ }
+ else
+ {
+ return byte.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outByte);
+ }
+ }
+
// Local function to specially parse a double value with an optional percentage sign
- bool TryInternalParse(string inString, out double outDouble)
+ bool InternalTryParseDouble(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs
index e27a4f3106..e8a4d6f94f 100644
--- a/src/Avalonia.Base/Media/HslColor.cs
+++ b/src/Avalonia.Base/Media/HslColor.cs
@@ -12,6 +12,7 @@ namespace Avalonia.Media
{
///
/// Defines a color using the hue/saturation/lightness (HSL) model.
+ /// This uses a cylindrical-coordinate representation of a color.
///
#if !BUILDTASK
public
@@ -98,24 +99,53 @@ namespace Avalonia.Media
}
///
- /// Gets the Alpha (transparency) component in the range from 0..1.
+ /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully transparent.
+ /// - 1 is fully opaque.
+ ///
+ ///
public double A { get; }
///
- /// Gets the Hue component in the range from 0..360.
+ /// Gets the Hue component in the range from 0..360 (degrees).
+ /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
///
+ ///
+ ///
+ /// - 0/360 degrees is Red.
+ /// - 60 degrees is Yellow.
+ /// - 120 degrees is Green.
+ /// - 180 degrees is Cyan.
+ /// - 240 degrees is Blue.
+ /// - 300 degrees is Magenta.
+ ///
+ ///
public double H { get; }
///
- /// Gets the Saturation component in the range from 0..1.
+ /// Gets the Saturation component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is a shade of gray (no color).
+ /// - 1 is the full color.
+ ///
+ ///
public double S { get; }
///
- /// Gets the Lightness component in the range from 0..1.
+ /// Gets the Lightness component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully black.
+ /// - 1 is fully white.
+ ///
+ ///
public double L { get; }
///
@@ -226,6 +256,8 @@ namespace Avalonia.Media
/// True if parsing was successful; otherwise, false.
public static bool TryParse(string s, out HslColor hslColor)
{
+ bool prefixMatched = false;
+
hslColor = default;
if (s is null)
@@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
- if (workingString.Length > 6 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible format is "hsl(0,0,0)", Length = 10.
+
+ if (workingString.Length >= 11 &&
workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
+ prefixMatched = true;
}
- if (workingString.Length > 5 &&
+ if (prefixMatched == false &&
+ workingString.Length >= 10 &&
workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
+ prefixMatched = true;
+ }
+
+ if (prefixMatched == false)
+ {
+ return false;
}
string[] components = workingString.Split(',');
diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs
index 164aeb1df1..924ef4778b 100644
--- a/src/Avalonia.Base/Media/HsvColor.cs
+++ b/src/Avalonia.Base/Media/HsvColor.cs
@@ -12,6 +12,7 @@ namespace Avalonia.Media
{
///
/// Defines a color using the hue/saturation/value (HSV) model.
+ /// This uses a cylindrical-coordinate representation of a color.
///
#if !BUILDTASK
public
@@ -98,24 +99,53 @@ namespace Avalonia.Media
}
///
- /// Gets the Alpha (transparency) component in the range from 0..1.
+ /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully transparent.
+ /// - 1 is fully opaque.
+ ///
+ ///
public double A { get; }
///
- /// Gets the Hue component in the range from 0..360.
+ /// Gets the Hue component in the range from 0..360 (degrees).
+ /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
///
+ ///
+ ///
+ /// - 0/360 degrees is Red.
+ /// - 60 degrees is Yellow.
+ /// - 120 degrees is Green.
+ /// - 180 degrees is Cyan.
+ /// - 240 degrees is Blue.
+ /// - 300 degrees is Magenta.
+ ///
+ ///
public double H { get; }
///
- /// Gets the Saturation component in the range from 0..1.
+ /// Gets the Saturation component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is a shade of gray (no color).
+ /// - 1 is the full color.
+ ///
+ ///
public double S { get; }
///
- /// Gets the Value component in the range from 0..1.
+ /// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage).
///
+ ///
+ ///
+ /// - 0 is fully black and shows no color.
+ /// - 1 is the brightest and shows full color.
+ ///
+ ///
public double V { get; }
///
@@ -226,6 +256,8 @@ namespace Avalonia.Media
/// True if parsing was successful; otherwise, false.
public static bool TryParse(string s, out HsvColor hsvColor)
{
+ bool prefixMatched = false;
+
hsvColor = default;
if (s is null)
@@ -241,18 +273,29 @@ namespace Avalonia.Media
return false;
}
- if (workingString.Length > 6 &&
+ // Note: The length checks are also an important optimization.
+ // The shortest possible format is "hsv(0,0,0)", Length = 10.
+
+ if (workingString.Length >= 11 &&
workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
+ prefixMatched = true;
}
- if (workingString.Length > 5 &&
+ if (prefixMatched == false &&
+ workingString.Length >= 10 &&
workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
+ prefixMatched = true;
+ }
+
+ if (prefixMatched == false)
+ {
+ return false;
}
string[] components = workingString.Split(',');
diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs
index 4d7f487e00..a0560924e7 100644
--- a/src/Avalonia.Base/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs
@@ -21,6 +21,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
diff --git a/src/Avalonia.Base/RelativePoint.cs b/src/Avalonia.Base/RelativePoint.cs
index 4550dbd54b..e1fd0093b6 100644
--- a/src/Avalonia.Base/RelativePoint.cs
+++ b/src/Avalonia.Base/RelativePoint.cs
@@ -1,7 +1,8 @@
using System;
using System.Globalization;
-
+#if !BUILDTASK
using Avalonia.Animation.Animators;
+#endif
using Avalonia.Utilities;
namespace Avalonia
@@ -10,7 +11,10 @@ namespace Avalonia
/// Defines the reference point units of an or
/// .
///
- public enum RelativeUnit
+#if !BUILDTASK
+ public
+#endif
+ enum RelativeUnit
{
///
/// The point is expressed as a fraction of the containing element's size.
@@ -26,7 +30,10 @@ namespace Avalonia
///
/// Defines a point that may be defined relative to a containing element.
///
- public readonly struct RelativePoint : IEquatable
+#if !BUILDTASK
+ public
+#endif
+ readonly struct RelativePoint : IEquatable
{
///
/// A point at the top left of the containing element.
@@ -49,7 +56,9 @@ namespace Avalonia
static RelativePoint()
{
+#if !BUILDTASK
Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType));
+#endif
}
///
diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs
index 397826df53..6423d86e7c 100644
--- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs
+++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs
@@ -46,7 +46,7 @@ namespace Avalonia.Threading
{
composite.Add(action(this, state));
}
- }, DispatcherPriority.DataBind);
+ }, DispatcherPriority.Background);
composite.Add(cancellation);
diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs
index 49cee441d0..2eb2e7c01f 100644
--- a/src/Avalonia.Base/Threading/Dispatcher.cs
+++ b/src/Avalonia.Base/Threading/Dispatcher.cs
@@ -83,42 +83,42 @@ namespace Avalonia.Threading
_jobRunner.HasJobsWithPriority(minimumPriority);
///
- public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Action action, DispatcherPriority priority = default)
{
_ = action ?? throw new ArgumentNullException(nameof(action));
return _jobRunner.InvokeAsync(action, priority);
}
///
- public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Func function, DispatcherPriority priority = default)
{
_ = function ?? throw new ArgumentNullException(nameof(function));
return _jobRunner.InvokeAsync(function, priority);
}
///
- public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Func function, DispatcherPriority priority = default)
{
_ = function ?? throw new ArgumentNullException(nameof(function));
return _jobRunner.InvokeAsync(function, priority).Unwrap();
}
///
- public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Func> function, DispatcherPriority priority = default)
{
_ = function ?? throw new ArgumentNullException(nameof(function));
return _jobRunner.InvokeAsync(function, priority).Unwrap();
}
///
- public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
+ public void Post(Action action, DispatcherPriority priority = default)
{
_ = action ?? throw new ArgumentNullException(nameof(action));
_jobRunner.Post(action, priority);
}
///
- public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal)
+ public void Post(Action action, T arg, DispatcherPriority priority = default)
{
_ = action ?? throw new ArgumentNullException(nameof(action));
_jobRunner.Post(action, arg, priority);
diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs
index a2b4b86bac..a93e4f406d 100644
--- a/src/Avalonia.Base/Threading/DispatcherPriority.cs
+++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs
@@ -1,74 +1,121 @@
+using System;
+
namespace Avalonia.Threading
{
///
/// Defines the priorities with which jobs can be invoked on a .
///
- // TODO: These are copied from WPF - many won't apply to Avalonia.
- public enum DispatcherPriority
+ public readonly struct DispatcherPriority : IEquatable, IComparable
{
+ ///
+ /// The integer value of the priority
+ ///
+ public int Value { get; }
+
+ private DispatcherPriority(int value)
+ {
+ Value = value;
+ }
+
///
/// Minimum possible priority
///
- MinValue = 1,
-
+ public static readonly DispatcherPriority MinValue = new(0);
+
///
/// The job will be processed when the system is idle.
///
- SystemIdle = 1,
+ [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue;
///
/// The job will be processed when the application is idle.
///
- ApplicationIdle = 2,
+ [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue;
///
/// The job will be processed after background operations have completed.
///
- ContextIdle = 3,
+ [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue;
///
- /// The job will be processed after other non-idle operations have completed.
+ /// The job will be processed with normal priority.
///
- Background = 4,
+ public static readonly DispatcherPriority Normal = MinValue;
///
- /// The job will be processed with the same priority as input.
+ /// The job will be processed after other non-idle operations have completed.
///
- Input = 5,
+ public static readonly DispatcherPriority Background = new(1);
///
- /// The job will be processed after layout and render but before input.
+ /// The job will be processed with the same priority as input.
///
- Loaded = 6,
+ public static readonly DispatcherPriority Input = new(2);
///
- /// The job will be processed with the same priority as render.
+ /// The job will be processed after layout and render but before input.
///
- Render = 7,
+ public static readonly DispatcherPriority Loaded = new(3);
///
/// The job will be processed with the same priority as render.
///
- Layout = 8,
-
+ public static readonly DispatcherPriority Render = new(5);
+
///
- /// The job will be processed with the same priority as data binding.
+ /// The job will be processed with the same priority as render.
///
- DataBind = 9,
+ public static readonly DispatcherPriority Layout = new(6);
///
- /// The job will be processed with normal priority.
+ /// The job will be processed with the same priority as data binding.
///
- Normal = 10,
+ [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue;
///
/// The job will be processed before other asynchronous operations.
///
- Send = 11,
-
+ public static readonly DispatcherPriority Send = new(7);
+
///
/// Maximum possible priority
///
- MaxValue = 11
+ public static readonly DispatcherPriority MaxValue = Send;
+
+ // Note: unlike ctor this one is validating
+ public static DispatcherPriority FromValue(int value)
+ {
+ if (value < MinValue.Value || value > MaxValue.Value)
+ throw new ArgumentOutOfRangeException(nameof(value));
+ return new DispatcherPriority(value);
+ }
+
+ public static implicit operator int(DispatcherPriority priority) => priority.Value;
+
+ public static implicit operator DispatcherPriority(int value) => FromValue(value);
+
+ ///
+ public bool Equals(DispatcherPriority other) => Value == other.Value;
+
+ ///
+ public override bool Equals(object? obj) => obj is DispatcherPriority other && Equals(other);
+
+ ///
+ public override int GetHashCode() => Value.GetHashCode();
+
+ public static bool operator ==(DispatcherPriority left, DispatcherPriority right) => left.Value == right.Value;
+
+ public static bool operator !=(DispatcherPriority left, DispatcherPriority right) => left.Value != right.Value;
+
+ public static bool operator <(DispatcherPriority left, DispatcherPriority right) => left.Value < right.Value;
+
+ public static bool operator >(DispatcherPriority left, DispatcherPriority right) => left.Value > right.Value;
+
+ public static bool operator <=(DispatcherPriority left, DispatcherPriority right) => left.Value <= right.Value;
+
+ public static bool operator >=(DispatcherPriority left, DispatcherPriority right) => left.Value >= right.Value;
+
+ ///
+ public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value);
}
-}
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs
index 93023b90a5..0c25d89722 100644
--- a/src/Avalonia.Base/Threading/DispatcherTimer.cs
+++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs
@@ -123,7 +123,7 @@ namespace Avalonia.Threading
/// The interval at which to tick.
/// The priority to use.
/// An used to cancel the timer.
- public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal)
+ public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default)
{
var timer = new DispatcherTimer(priority) { Interval = interval };
@@ -152,7 +152,7 @@ namespace Avalonia.Threading
public static IDisposable RunOnce(
Action action,
TimeSpan interval,
- DispatcherPriority priority = DispatcherPriority.Normal)
+ DispatcherPriority priority = default)
{
interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1);
diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs
index cd5add70d4..eccd42bd4e 100644
--- a/src/Avalonia.Base/Threading/IDispatcher.cs
+++ b/src/Avalonia.Base/Threading/IDispatcher.cs
@@ -24,7 +24,7 @@ namespace Avalonia.Threading
///
/// The method.
/// The priority with which to invoke the method.
- void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal);
+ void Post(Action action, DispatcherPriority priority = default);
///
/// Posts an action that will be invoked on the dispatcher thread.
@@ -33,7 +33,7 @@ namespace Avalonia.Threading
/// The method to call.
/// The argument of method to call.
/// The priority with which to invoke the method.
- void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal);
+ void Post(Action action, T arg, DispatcherPriority priority = default);
///
/// Invokes a action on the dispatcher thread.
@@ -41,7 +41,7 @@ namespace Avalonia.Threading
/// The method.
/// The priority with which to invoke the method.
/// A task that can be used to track the method's execution.
- Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal);
+ Task InvokeAsync(Action action, DispatcherPriority priority = default);
///
/// Invokes a method on the dispatcher thread.
@@ -49,7 +49,7 @@ namespace Avalonia.Threading
/// The method.
/// The priority with which to invoke the method.
/// A task that can be used to track the method's execution.
- Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal);
+ Task InvokeAsync(Func function, DispatcherPriority priority = default);
///
/// Queues the specified work to run on the dispatcher thread and returns a proxy for the
@@ -58,7 +58,7 @@ namespace Avalonia.Threading
/// The work to execute asynchronously.
/// The priority with which to invoke the method.
/// A task that represents a proxy for the task returned by .
- Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal);
+ Task InvokeAsync(Func function, DispatcherPriority priority = default);
///
/// Queues the specified work to run on the dispatcher thread and returns a proxy for the
@@ -67,6 +67,6 @@ namespace Avalonia.Threading
/// The work to execute asynchronously.
/// The priority with which to invoke the method.
/// A task that represents a proxy for the task returned by .
- Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal);
+ Task InvokeAsync(Func> function, DispatcherPriority priority = default);
}
}
diff --git a/src/Avalonia.Base/Utilities/EnumHelper.cs b/src/Avalonia.Base/Utilities/EnumHelper.cs
new file mode 100644
index 0000000000..c857033ef1
--- /dev/null
+++ b/src/Avalonia.Base/Utilities/EnumHelper.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Avalonia.Utilities
+{
+ internal class EnumHelper
+ {
+#if NET6_0_OR_GREATER
+ public static T Parse(ReadOnlySpan key, bool ignoreCase) where T : struct
+ {
+ return Enum.Parse(key, ignoreCase);
+ }
+#else
+ public static T Parse(string key, bool ignoreCase) where T : struct
+ {
+ return (T)Enum.Parse(typeof(T), key, ignoreCase);
+ }
+#endif
+ }
+}
diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
index 6267c74df9..e9b99c9aa8 100644
--- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
+++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
@@ -95,6 +95,9 @@
Markup/%(RecursiveDir)%(FileName)%(Extension)
+
+ Markup/%(RecursiveDir)%(FileName)%(Extension)
+
diff --git a/src/Avalonia.Build.Tasks/Properties/launchSettings.json b/src/Avalonia.Build.Tasks/Properties/launchSettings.json
new file mode 100644
index 0000000000..e9f5af46d6
--- /dev/null
+++ b/src/Avalonia.Build.Tasks/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Compile Sandbox": {
+ "commandName": "Project",
+ "executablePath": "$(SolutionDir)\\src\\Avalonia.Build.Tasks\\bin\\Debug\\net6.0\\Avalonia.Build.Tasks.exe",
+ "commandLineArgs": "$(SolutionDir)\\samples\\Sandbox\\obj\\Debug\\net6.0\\Avalonia\\original.dll $(SolutionDir)\\samples\\Sandbox\\bin\\Debug\\net6.0\\Sandbox.dll.refs $(SolutionDir)\\out.dll"
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index 9b67c9b096..aaac3f8f9c 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -32,6 +32,14 @@ namespace Avalonia.Controls
///
/// Displays data in a customizable grid.
///
+ [TemplatePart(DATAGRID_elementBottomRightCornerHeaderName, typeof(IVisual))]
+ [TemplatePart(DATAGRID_elementColumnHeadersPresenterName, typeof(DataGridColumnHeadersPresenter))]
+ [TemplatePart(DATAGRID_elementFrozenColumnScrollBarSpacerName, typeof(Control))]
+ [TemplatePart(DATAGRID_elementHorizontalScrollbarName, typeof(ScrollBar))]
+ [TemplatePart(DATAGRID_elementRowsPresenterName, typeof(DataGridRowsPresenter))]
+ [TemplatePart(DATAGRID_elementTopLeftCornerHeaderName, typeof(ContentControl))]
+ [TemplatePart(DATAGRID_elementTopRightCornerHeaderName, typeof(ContentControl))]
+ [TemplatePart(DATAGRID_elementVerticalScrollbarName, typeof(ScrollBar))]
[PseudoClasses(":invalid", ":empty-rows", ":empty-columns")]
public partial class DataGrid : TemplatedControl
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
index e3f150f5c4..67183781d3 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs
@@ -13,6 +13,7 @@ namespace Avalonia.Controls
///
/// Represents an individual cell.
///
+ [TemplatePart(DATAGRIDCELL_elementRightGridLine, typeof(Rectangle))]
[PseudoClasses(":selected", ":current", ":edited", ":invalid")]
public class DataGridCell : ContentControl
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index a6faec752d..db5d428942 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
@@ -21,6 +21,11 @@ namespace Avalonia.Controls
///
/// Represents a row.
///
+ [TemplatePart(DATAGRIDROW_elementBottomGridLine, typeof(Rectangle))]
+ [TemplatePart(DATAGRIDROW_elementCells, typeof(DataGridCellsPresenter))]
+ [TemplatePart(DATAGRIDROW_elementDetails, typeof(DataGridDetailsPresenter))]
+ [TemplatePart(DATAGRIDROW_elementRoot, typeof(Panel))]
+ [TemplatePart(DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))]
[PseudoClasses(":selected", ":editing", ":invalid")]
public class DataGridRow : TemplatedControl
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
index 49ca23d34c..a3dfa44fc9 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
@@ -14,6 +14,12 @@ using System.Reactive.Linq;
namespace Avalonia.Controls
{
+ [TemplatePart(DATAGRIDROWGROUPHEADER_expanderButton, typeof(ToggleButton))]
+ [TemplatePart(DATAGRIDROWGROUPHEADER_indentSpacer, typeof(Control))]
+ [TemplatePart(DATAGRIDROWGROUPHEADER_itemCountElement, typeof(TextBlock))]
+ [TemplatePart(DATAGRIDROWGROUPHEADER_propertyNameElement, typeof(TextBlock))]
+ [TemplatePart(DataGridRow.DATAGRIDROW_elementRoot, typeof(Panel))]
+ [TemplatePart(DataGridRow.DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))]
[PseudoClasses(":pressed", ":current", ":expanded")]
public class DataGridRowGroupHeader : TemplatedControl
{
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
index 510072174f..03299bbf35 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
@@ -13,6 +13,7 @@ namespace Avalonia.Controls.Primitives
///
/// Represents an individual row header.
///
+ [TemplatePart(DATAGRIDROWHEADER_elementRootName, typeof(Control))]
[PseudoClasses(":invalid", ":selected", ":editing", ":current")]
public class DataGridRowHeader : ContentControl
{
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index a6dee5cfaa..cbf9b35a05 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -453,42 +453,18 @@ namespace Avalonia.Controls
private void SelectNext()
{
- int next = SelectedIndex + 1;
-
- if (next >= ItemCount)
+ if (ItemCount >= 1)
{
- if (WrapSelection == true)
- {
- next = 0;
- }
- else
- {
- return;
- }
+ MoveSelection(NavigationDirection.Next, WrapSelection);
}
-
-
-
- SelectedIndex = next;
}
private void SelectPrev()
{
- int prev = SelectedIndex - 1;
-
- if (prev < 0)
+ if (ItemCount >= 1)
{
- if (WrapSelection == true)
- {
- prev = ItemCount - 1;
- }
- else
- {
- return;
- }
+ MoveSelection(NavigationDirection.Previous, WrapSelection);
}
-
- SelectedIndex = prev;
}
}
}
diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs
index da13416700..eb60fca367 100644
--- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs
+++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs
@@ -15,6 +15,7 @@ namespace Avalonia.Controls.Generators
///
public class TreeContainerIndex
{
+ private readonly Dictionary
public class AssetLoader : IAssetLoader
{
- private static AssemblyDescriptorResolver s_assemblyDescriptorResolver = new();
+ private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver();
private AssemblyDescriptor? _defaultResmAssembly;
///
/// Introduced for tests.
///
- internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver resolver) =>
+ internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) =>
s_assemblyDescriptorResolver = resolver;
///
@@ -182,13 +182,13 @@ namespace Avalonia.PlatformSupport
throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri));
}
- private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri)
+ private (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri)
{
var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority);
return (asm, uri.GetUnescapeAbsolutePath());
}
- private AssemblyDescriptor? GetAssembly(Uri? uri)
+ private IAssemblyDescriptor? GetAssembly(Uri? uri)
{
if (uri != null)
{
diff --git a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj
index 420ac0796c..5336f1e630 100644
--- a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj
+++ b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj
@@ -19,6 +19,6 @@
-
+
diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs
index a3de7f2b8a..64ffec8482 100644
--- a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs
+++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs
@@ -6,7 +6,15 @@ using Avalonia.Utilities;
namespace Avalonia.PlatformSupport.Internal;
-internal class AssemblyDescriptor
+internal interface IAssemblyDescriptor
+{
+ Assembly Assembly { get; }
+ Dictionary? Resources { get; }
+ Dictionary? AvaloniaResources { get; }
+ string? Name { get; }
+}
+
+internal class AssemblyDescriptor : IAssemblyDescriptor
{
public AssemblyDescriptor(Assembly assembly)
{
diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs
index a78051a9c4..28ae35d57d 100644
--- a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs
+++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs
@@ -5,11 +5,16 @@ using System.Reflection;
namespace Avalonia.PlatformSupport.Internal;
-internal class AssemblyDescriptorResolver
+internal interface IAssemblyDescriptorResolver
{
- private readonly Dictionary _assemblyNameCache = new();
+ IAssemblyDescriptor GetAssembly(string name);
+}
+
+internal class AssemblyDescriptorResolver: IAssemblyDescriptorResolver
+{
+ private readonly Dictionary _assemblyNameCache = new();
- public AssemblyDescriptor GetAssembly(string name)
+ public IAssemblyDescriptor GetAssembly(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj
new file mode 100644
index 0000000000..97e58f8a64
--- /dev/null
+++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ enable
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Avalonia.SourceGenerator/IsExternalInit.cs
new file mode 100644
index 0000000000..c6ddf762ad
--- /dev/null
+++ b/src/Avalonia.SourceGenerator/IsExternalInit.cs
@@ -0,0 +1,14 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Runtime.CompilerServices
+{
+ ///
+ /// Reserved to be used by the compiler for tracking metadata.
+ /// This class should not be used by developers in source code.
+ ///
+ [ExcludeFromCodeCoverage, DebuggerNonUserCode]
+ internal static class IsExternalInit
+ {
+ }
+}
diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs
new file mode 100644
index 0000000000..4fc9397e7a
--- /dev/null
+++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs
@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Avalonia.SourceGenerator
+{
+ [Generator(LanguageNames.CSharp)]
+ public class SubtypesFactoryGenerator : IIncrementalGenerator
+ {
+ private record struct MethodTarget(IMethodSymbol Method, string MethodDecl, ITypeSymbol BaseType, string Namespace);
+ private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName;
+
+ private static bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType)
+ {
+ return type.BaseType is not null && (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType) || IsSubtypeOf(type.BaseType, baseType));
+ }
+
+ private static void GenerateSubTypes(SourceProductionContext context, MethodTarget methodTarget, ImmutableArray types)
+ {
+ var (method, methodDecl, baseType, @namespace) = methodTarget;
+ var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray();
+ var type = method.ContainingType;
+ var isGeneric = type.TypeParameters.Length > 0;
+ var isClass = type.TypeKind == TypeKind.Class;
+
+ var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}";
+ var source = $@"using System;
+using System.Collections.Generic;
+
+namespace {method.ContainingNamespace}
+{{
+ {typeDecl}
+ {{
+ {methodDecl}
+ {{
+ var hasMatch = false;
+ (hasMatch, {method.Parameters[1].Name}) = {method.Parameters[0].Name} switch
+ {{
+{string.Join("\n", candidateTypes.Select(i => $" \"{i.Name}\" => (true, ({method.Parameters[1].Type})new {i}()),"))}
+ _ => (false, default({method.Parameters[1].Type}))
+ }};
+
+ return hasMatch;
+ }}
+ }}
+}}";
+
+ context.AddSource($"{type}.{method.MetadataName}.gen.cs", source);
+ }
+
+ private static MethodTarget? PopulateMethodTargets(GeneratorSyntaxContext context, CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+ if (context.Node is MethodDeclarationSyntax method)
+ {
+ var attributes = method.AttributeLists.SelectMany(i => i.Attributes);
+ var semanticModel = context.SemanticModel;
+ foreach (var attribute in attributes)
+ {
+ var attributeTypeInfo = semanticModel.GetTypeInfo(attribute);
+ if (attributeTypeInfo.Type is null ||
+ attributeTypeInfo.Type.ToString() != s_attributeName ||
+ attribute.ArgumentList is null)
+ {
+ continue;
+ }
+
+ var arguments = attribute.ArgumentList.Arguments;
+ if (arguments.Count != 2)
+ {
+ continue;
+ }
+
+ if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr ||
+ arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax)
+ {
+ continue;
+ }
+
+ var type = semanticModel.GetTypeInfo(typeOfExpr.Type);
+ var ns = semanticModel.GetConstantValue(arguments[1].Expression);
+ var methodDeclInfo = semanticModel.GetDeclaredSymbol(method);
+
+ if (type.Type is not ITypeSymbol baseType ||
+ ns.HasValue is false ||
+ ns.Value is not string nsValue ||
+ methodDeclInfo is not IMethodSymbol methodSymbol ||
+ methodSymbol.Parameters.Length != 2 ||
+ methodSymbol.Parameters[1].RefKind != RefKind.Out)
+ {
+ continue;
+ }
+
+ var parameters = new SeparatedSyntaxList().AddRange(method.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList())));
+ var methodDecl = method
+ .WithAttributeLists(new SyntaxList())
+ .WithParameterList(method.ParameterList.WithParameters(parameters))
+ .WithBody(null)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None))
+ .WithoutTrivia().ToString();
+
+ return new MethodTarget(methodSymbol, methodDecl, baseType, nsValue);
+ }
+ }
+
+ return null;
+ }
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var typesProvider = context.SyntaxProvider.CreateSyntaxProvider(
+ static (syntaxNode, token) =>
+ {
+ token.ThrowIfCancellationRequested();
+ return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax;
+ },
+ static (syntaxContext, token) =>
+ {
+ token.ThrowIfCancellationRequested();
+ return syntaxContext.Node is ClassDeclarationSyntax or StructDeclarationSyntax &&
+ syntaxContext.SemanticModel.GetDeclaredSymbol(syntaxContext.Node) is ITypeSymbol typeSymbol
+ ? typeSymbol : null;
+ })
+ .SelectMany((type, token) =>
+ {
+ token.ThrowIfCancellationRequested();
+ return type is null ? Array.Empty() : new ITypeSymbol[] { type };
+ });
+
+ var methodsProvider = context.SyntaxProvider.CreateSyntaxProvider(
+ static (syntaxNode, token) =>
+ {
+ token.ThrowIfCancellationRequested();
+ return syntaxNode is MethodDeclarationSyntax { AttributeLists.Count: > 0 };
+ }, PopulateMethodTargets)
+ .SelectMany((method, token) =>
+ {
+ token.ThrowIfCancellationRequested();
+ return method is null ? Array.Empty() : new MethodTarget[] { method.Value };
+ });
+
+ var generateContext = methodsProvider.Combine(typesProvider.Collect());
+
+ context.RegisterSourceOutput(generateContext, static (sourceContext, source) =>
+ {
+ sourceContext.CancellationToken.ThrowIfCancellationRequested();
+ GenerateSubTypes(sourceContext, source.Left, source.Right);
+ });
+ }
+ }
+}
diff --git a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml
index b357446bfa..aab1b76259 100644
--- a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml
+++ b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml
@@ -13,7 +13,7 @@
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
- Margin="{TemplateBinding Padding}"
+ Padding="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"
IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}">
diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
index 53a1b721d1..b7addcb61e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
@@ -32,7 +32,7 @@
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
- Margin="{TemplateBinding Padding}"
+ Padding="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"
IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}">
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
index 88529ae3a0..d907bcbef9 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
@@ -160,6 +160,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return true;
}
+ if (type.Equals(types.RelativePoint))
+ {
+ try
+ {
+ var relativePoint = RelativePoint.Parse(text);
+
+ var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false);
+
+ result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List
+ {
+ new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X),
+ new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.Y),
+ new XamlConstantNode(node, types.RelativeUnit, (int) relativePoint.Unit),
+ });
+
+ return true;
+ }
+ catch
+ {
+ throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a relative point", node);
+ }
+ }
+
if (type.Equals(types.GridLength))
{
try
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 99072ace02..76f3cc071f 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -71,6 +71,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlConstructor MatrixFullConstructor { get; }
public IXamlType CornerRadius { get; }
public IXamlConstructor CornerRadiusFullConstructor { get; }
+ public IXamlType RelativeUnit { get; }
+ public IXamlType RelativePoint { get; }
+ public IXamlConstructor RelativePointFullConstructor { get; }
public IXamlType GridLength { get; }
public IXamlConstructor GridLengthConstructorValueType { get; }
public IXamlType Color { get; }
@@ -175,6 +178,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
(Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6);
(CornerRadius, CornerRadiusFullConstructor) = GetNumericTypeInfo("Avalonia.CornerRadius", XamlIlTypes.Double, 4);
+ RelativeUnit = cfg.TypeSystem.GetType("Avalonia.RelativeUnit");
+ RelativePoint = cfg.TypeSystem.GetType("Avalonia.RelativePoint");
+ RelativePointFullConstructor = RelativePoint.GetConstructor(new List { XamlIlTypes.Double, XamlIlTypes.Double, RelativeUnit });
+
GridLength = cfg.TypeSystem.GetType("Avalonia.Controls.GridLength");
GridLengthConstructorValueType = GridLength.GetConstructor(new List { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") });
Color = cfg.TypeSystem.GetType("Avalonia.Media.Color");
diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs
new file mode 100644
index 0000000000..fdb5977d23
--- /dev/null
+++ b/src/Shared/SourceGeneratorAttributes.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Avalonia.SourceGenerator
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
+ internal sealed class SubtypesFactoryAttribute : Attribute
+ {
+ public SubtypesFactoryAttribute(Type baseType, string @namespace)
+ {
+ BaseType = baseType;
+ Namespace = @namespace;
+ }
+
+ public string Namespace { get; }
+ public Type BaseType { get; }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs
index 5026fbaaba..89cde01ff4 100644
--- a/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs
+++ b/src/Windows/Avalonia.Win32/WinRT/NativeWinRTMethods.cs
@@ -23,7 +23,7 @@ namespace Avalonia.Win32.WinRT
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll",
CallingConvention = CallingConvention.StdCall, PreserveSig = false)]
- internal static extern unsafe IntPtr WindowsDeleteString(IntPtr hString);
+ internal static extern unsafe void WindowsDeleteString(IntPtr hString);
[DllImport("Windows.UI.Composition", EntryPoint = "DllGetActivationFactory",
CallingConvention = CallingConvention.StdCall, PreserveSig = false)]
diff --git a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
index 758d501cc1..88abb4a6fa 100644
--- a/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
@@ -6,7 +6,6 @@ using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.UnitTests;
-using Avalonia.VisualTree;
using Moq;
using Xunit;
@@ -34,160 +33,6 @@ namespace Avalonia.Base.UnitTests.Input
}
#pragma warning restore CS0618 // Type or member is obsolete
- [Fact]
- public void MouseMove_Should_Update_IsPointerOver()
- {
- var renderer = new Mock();
-
- using (TestApplication(renderer.Object))
- {
- var inputManager = InputManager.Instance;
-
- Canvas canvas;
- Border border;
- Decorator decorator;
-
- var root = new TestRoot
- {
- MouseDevice = new MouseDevice(),
- Renderer = renderer.Object,
- Child = new Panel
- {
- Children =
- {
- (canvas = new Canvas()),
- (border = new Border
- {
- Child = decorator = new Decorator(),
- })
- }
- }
- };
-
- SetHit(renderer, decorator);
- SendMouseMove(inputManager, root);
-
- Assert.True(decorator.IsPointerOver);
- Assert.True(border.IsPointerOver);
- Assert.False(canvas.IsPointerOver);
- Assert.True(root.IsPointerOver);
-
- SetHit(renderer, canvas);
- SendMouseMove(inputManager, root);
-
- Assert.False(decorator.IsPointerOver);
- Assert.False(border.IsPointerOver);
- Assert.True(canvas.IsPointerOver);
- Assert.True(root.IsPointerOver);
- }
- }
-
- [Fact]
- public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True()
- {
- var renderer = new Mock();
-
- using (TestApplication(renderer.Object))
- {
- var inputManager = InputManager.Instance;
-
- Canvas canvas;
- Border border;
- Decorator decorator;
-
- var root = new TestRoot
- {
- MouseDevice = new MouseDevice(),
- Renderer = renderer.Object,
- Child = new Panel
- {
- Children =
- {
- (canvas = new Canvas()),
- (border = new Border
- {
- Child = decorator = new Decorator(),
- })
- }
- }
- };
-
- SetHit(renderer, canvas);
- SendMouseMove(inputManager, root);
-
- Assert.False(decorator.IsPointerOver);
- Assert.False(border.IsPointerOver);
- Assert.True(canvas.IsPointerOver);
- Assert.True(root.IsPointerOver);
-
- // Ensure that e.Handled is reset between controls.
- decorator.PointerEnter += (s, e) => e.Handled = true;
-
- SetHit(renderer, decorator);
- SendMouseMove(inputManager, root);
-
- Assert.True(decorator.IsPointerOver);
- Assert.True(border.IsPointerOver);
- Assert.False(canvas.IsPointerOver);
- Assert.True(root.IsPointerOver);
- }
- }
-
- [Fact]
- public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order()
- {
- var renderer = new Mock();
- var result = new List<(object, string)>();
-
- void HandleEvent(object sender, PointerEventArgs e)
- {
- result.Add((sender, e.RoutedEvent.Name));
- }
-
- using (TestApplication(renderer.Object))
- {
- var inputManager = InputManager.Instance;
-
- Canvas canvas;
- Border border;
- Decorator decorator;
-
- var root = new TestRoot
- {
- MouseDevice = new MouseDevice(),
- Renderer = renderer.Object,
- Child = new Panel
- {
- Children =
- {
- (canvas = new Canvas()),
- (border = new Border
- {
- Child = decorator = new Decorator(),
- })
- }
- }
- };
-
- SetHit(renderer, canvas);
- SendMouseMove(inputManager, root);
-
- AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator);
- SetHit(renderer, decorator);
- SendMouseMove(inputManager, root);
-
- Assert.Equal(
- new[]
- {
- ((object)canvas, "PointerLeave"),
- ((object)decorator, "PointerEnter"),
- ((object)border, "PointerEnter"),
- },
- result);
- }
- }
-
-
[Fact]
public void GetPosition_Should_Respect_Control_RenderTransform()
{
@@ -216,17 +61,6 @@ namespace Avalonia.Base.UnitTests.Input
}
}
- private void AddEnterLeaveHandlers(
- EventHandler handler,
- params IControl[] controls)
- {
- foreach (var c in controls)
- {
- c.PointerEnter += handler;
- c.PointerLeave += handler;
- }
- }
-
private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point())
{
inputManager.ProcessInput(new RawPointerEventArgs(
@@ -238,15 +72,6 @@ namespace Avalonia.Base.UnitTests.Input
RawInputModifiers.None));
}
- private void SetHit(Mock renderer, IControl hit)
- {
- renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
- .Returns(new[] { hit });
-
- renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>()))
- .Returns(hit);
- }
-
private IDisposable TestApplication(IRenderer renderer)
{
return UnitTestApplication.Start(
diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
new file mode 100644
index 0000000000..b677207b3a
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
@@ -0,0 +1,534 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+
+using Moq;
+
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Input
+{
+ public class PointerOverTests
+ {
+ // https://github.com/AvaloniaUI/Avalonia/issues/2821
+ [Fact]
+ public void Close_Should_Remove_PointerOver()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var device = CreatePointerDeviceMock().Object;
+ var impl = CreateTopLevelImplMock(renderer.Object);
+
+ Canvas canvas;
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas())
+ }
+ });
+
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
+
+ Assert.True(canvas.IsPointerOver);
+
+ impl.Object.Closed!();
+
+ Assert.False(canvas.IsPointerOver);
+ }
+
+ [Fact]
+ public void MouseMove_Should_Update_IsPointerOver()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var device = CreatePointerDeviceMock().Object;
+ var impl = CreateTopLevelImplMock(renderer.Object);
+
+ Canvas canvas;
+ Border border;
+ Decorator decorator;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas()),
+ (border = new Border
+ {
+ Child = decorator = new Decorator(),
+ })
+ }
+ });
+
+ SetHit(renderer, decorator);
+ impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
+
+ Assert.True(decorator.IsPointerOver);
+ Assert.True(border.IsPointerOver);
+ Assert.False(canvas.IsPointerOver);
+ Assert.True(root.IsPointerOver);
+
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
+
+ Assert.False(decorator.IsPointerOver);
+ Assert.False(border.IsPointerOver);
+ Assert.True(canvas.IsPointerOver);
+ Assert.True(root.IsPointerOver);
+ }
+
+
+ [Fact]
+ public void TouchMove_Should_Not_Set_IsPointerOver()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object;
+ var impl = CreateTopLevelImplMock(renderer.Object);
+
+ Canvas canvas;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas())
+ }
+ });
+
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
+
+ Assert.False(canvas.IsPointerOver);
+ Assert.False(root.IsPointerOver);
+ }
+
+ [Fact]
+ public void HitTest_Should_Be_Ignored_If_Element_Captured()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var pointer = new Mock();
+ var device = CreatePointerDeviceMock(pointer.Object).Object;
+ var impl = CreateTopLevelImplMock(renderer.Object);
+
+ Canvas canvas;
+ Border border;
+ Decorator decorator;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas()),
+ (border = new Border
+ {
+ Child = decorator = new Decorator(),
+ })
+ }
+ });
+
+ SetHit(renderer, canvas);
+ pointer.SetupGet(p => p.Captured).Returns(decorator);
+ impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
+
+ Assert.True(decorator.IsPointerOver);
+ Assert.True(border.IsPointerOver);
+ Assert.False(canvas.IsPointerOver);
+ Assert.True(root.IsPointerOver);
+ }
+
+ [Fact]
+ public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var device = CreatePointerDeviceMock().Object;
+ var impl = CreateTopLevelImplMock(renderer.Object);
+
+ Canvas canvas;
+ Border border;
+ Decorator decorator;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas()),
+ (border = new Border
+ {
+ Child = decorator = new Decorator(),
+ })
+ }
+ });
+
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
+
+ Assert.False(decorator.IsPointerOver);
+ Assert.False(border.IsPointerOver);
+ Assert.True(canvas.IsPointerOver);
+ Assert.True(root.IsPointerOver);
+
+ // Ensure that e.Handled is reset between controls.
+ root.PointerMoved += (s, e) => e.Handled = true;
+ decorator.PointerEnter += (s, e) => e.Handled = true;
+
+ SetHit(renderer, decorator);
+ impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
+
+ Assert.True(decorator.IsPointerOver);
+ Assert.True(border.IsPointerOver);
+ Assert.False(canvas.IsPointerOver);
+ Assert.True(root.IsPointerOver);
+ }
+
+ [Fact]
+ public void Pointer_Enter_Move_Leave_Should_Be_Followed()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var deviceMock = CreatePointerDeviceMock();
+ var impl = CreateTopLevelImplMock(renderer.Object);
+ var result = new List<(object?, string)>();
+
+ void HandleEvent(object? sender, PointerEventArgs e)
+ {
+ result.Add((sender, e.RoutedEvent!.Name));
+ }
+
+ Canvas canvas;
+ Border border;
+ Decorator decorator;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas()),
+ (border = new Border
+ {
+ Child = decorator = new Decorator(),
+ })
+ }
+ });
+
+ AddEnterLeaveHandlers(HandleEvent, canvas, decorator);
+
+ // Enter decorator
+ SetHit(renderer, decorator);
+ SetMove(deviceMock, root, decorator);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
+
+ // Leave decorator
+ SetHit(renderer, canvas);
+ SetMove(deviceMock, root, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
+
+ Assert.Equal(
+ new[]
+ {
+ ((object?)decorator, "PointerEnter"),
+ (decorator, "PointerMove"),
+ (decorator, "PointerLeave"),
+ (canvas, "PointerEnter"),
+ (canvas, "PointerMove")
+ },
+ result);
+ }
+
+ [Fact]
+ public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var deviceMock = CreatePointerDeviceMock();
+ var impl = CreateTopLevelImplMock(renderer.Object);
+ var result = new List<(object?, string)>();
+
+ void HandleEvent(object? sender, PointerEventArgs e)
+ {
+ result.Add((sender, e.RoutedEvent!.Name));
+ }
+
+ Canvas canvas;
+ Border border;
+ Decorator decorator;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas()),
+ (border = new Border
+ {
+ Child = decorator = new Decorator(),
+ })
+ }
+ });
+
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
+
+ AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator);
+
+ SetHit(renderer, decorator);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
+
+ Assert.Equal(
+ new[]
+ {
+ ((object?)canvas, "PointerLeave"),
+ (decorator, "PointerEnter"),
+ (border, "PointerEnter"),
+ },
+ result);
+ }
+
+ // https://github.com/AvaloniaUI/Avalonia/issues/7896
+ [Fact]
+ public void PointerEnter_Leave_Should_Set_Correct_Position()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var expectedPosition = new Point(15, 15);
+ var renderer = new Mock();
+ var deviceMock = CreatePointerDeviceMock();
+ var impl = CreateTopLevelImplMock(renderer.Object);
+ var result = new List<(object?, string, Point)>();
+
+ void HandleEvent(object? sender, PointerEventArgs e)
+ {
+ result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
+ }
+
+ Canvas canvas;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas())
+ }
+ });
+
+ AddEnterLeaveHandlers(HandleEvent, root, canvas);
+
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition));
+
+ SetHit(renderer, null);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition));
+
+ Assert.Equal(
+ new[]
+ {
+ ((object?)canvas, "PointerEnter", expectedPosition),
+ (root, "PointerEnter", expectedPosition),
+ (canvas, "PointerLeave", expectedPosition),
+ (root, "PointerLeave", expectedPosition)
+ },
+ result);
+ }
+
+ [Fact]
+ public void Render_Invalidation_Should_Affect_PointerOver()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var deviceMock = CreatePointerDeviceMock();
+ var impl = CreateTopLevelImplMock(renderer.Object);
+
+ var invalidateRect = new Rect(0, 0, 15, 15);
+
+ Canvas canvas;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas())
+ }
+ });
+
+ // Let input know about latest device.
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root));
+ Assert.True(canvas.IsPointerOver);
+
+ SetHit(renderer, canvas);
+ renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect));
+ Assert.True(canvas.IsPointerOver);
+
+ // Raise SceneInvalidated again, but now hide element from the hittest.
+ SetHit(renderer, null);
+ renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect));
+ Assert.False(canvas.IsPointerOver);
+ }
+
+ // https://github.com/AvaloniaUI/Avalonia/issues/7748
+ [Fact]
+ public void LeaveWindow_Should_Reset_PointerOver()
+ {
+ using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager()));
+
+ var renderer = new Mock();
+ var deviceMock = CreatePointerDeviceMock();
+ var impl = CreateTopLevelImplMock(renderer.Object);
+
+ var lastClientPosition = new Point(1, 5);
+ var invalidateRect = new Rect(0, 0, 15, 15);
+ var result = new List<(object?, string, Point)>();
+
+ void HandleEvent(object? sender, PointerEventArgs e)
+ {
+ result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null)));
+ }
+
+ Canvas canvas;
+
+ var root = CreateInputRoot(impl.Object, new Panel
+ {
+ Children =
+ {
+ (canvas = new Canvas())
+ }
+ });
+
+ AddEnterLeaveHandlers(HandleEvent, root, canvas);
+
+ // Init pointer over.
+ SetHit(renderer, canvas);
+ impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition));
+ Assert.True(canvas.IsPointerOver);
+
+ // Send LeaveWindow.
+ impl.Object.Input!(new RawPointerEventArgs(deviceMock.Object, 0, root, RawPointerEventType.LeaveWindow, new Point(), default));
+ Assert.False(canvas.IsPointerOver);
+
+ Assert.Equal(
+ new[]
+ {
+ ((object?)canvas, "PointerEnter", lastClientPosition),
+ (root, "PointerEnter", lastClientPosition),
+ (canvas, "PointerLeave", lastClientPosition),
+ (root, "PointerLeave", lastClientPosition),
+ },
+ result);
+ }
+
+ private static void AddEnterLeaveHandlers(
+ EventHandler handler,
+ params IInputElement[] controls)
+ {
+ foreach (var c in controls)
+ {
+ c.PointerEnter += handler;
+ c.PointerLeave += handler;
+ c.PointerMoved += handler;
+ }
+ }
+
+ private static void SetHit(Mock renderer, IControl? hit)
+ {
+ renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>()))
+ .Returns(hit is null ? Array.Empty() : new[] { hit });
+
+ renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>()))
+ .Returns(hit);
+ }
+
+ private static void SetMove(Mock deviceMock, IInputRoot root, IInputElement element)
+ {
+ deviceMock.Setup(d => d.ProcessRawEvent(It.IsAny()))
+ .Callback(() => element.RaiseEvent(CreatePointerMovedArgs(root, element)));
+ }
+
+ private static Mock CreateTopLevelImplMock(IRenderer renderer)
+ {
+ var impl = new Mock();
+ impl.DefaultValue = DefaultValue.Mock;
+ impl.SetupAllProperties();
+ impl.SetupGet(r => r.RenderScaling).Returns(1);
+ impl.Setup(r => r.CreateRenderer(It.IsAny())).Returns(renderer);
+ impl.Setup(r => r.PointToScreen(It.IsAny())).Returns(p => new PixelPoint((int)p.X, (int)p.Y));
+ impl.Setup(r => r.PointToClient(It.IsAny())).Returns(p => new Point(p.X, p.Y));
+ return impl;
+ }
+
+ private static IInputRoot CreateInputRoot(IWindowImpl impl, IControl child)
+ {
+ var root = new Window(impl)
+ {
+ Width = 100,
+ Height = 100,
+ Content = child,
+ Template = new FuncControlTemplate((w, _) => new ContentPresenter
+ {
+ Content = w.Content
+ })
+ };
+ root.Show();
+ return root;
+ }
+
+ private static IInputRoot CreateInputRoot(IRenderer renderer, IControl child)
+ {
+ return CreateInputRoot(CreateTopLevelImplMock(renderer).Object, child);
+ }
+
+ private static RawPointerEventArgs CreateRawPointerMovedArgs(
+ IPointerDevice pointerDevice,
+ IInputRoot root,
+ Point? positition = null)
+ {
+ return new RawPointerEventArgs(pointerDevice, 0, root, RawPointerEventType.Move,
+ positition ?? default, default);
+ }
+
+ private static PointerEventArgs CreatePointerMovedArgs(
+ IInputRoot root, IInputElement? source, Point? positition = null)
+ {
+ return new PointerEventArgs(InputElement.PointerMovedEvent, source, new Mock().Object, root,
+ positition ?? default, default, PointerPointProperties.None, KeyModifiers.None);
+ }
+
+ private static Mock CreatePointerDeviceMock(
+ IPointer? pointer = null,
+ PointerType pointerType = PointerType.Mouse)
+ {
+ if (pointer is null)
+ {
+ var pointerMock = new Mock();
+ pointerMock.SetupGet(p => p.Type).Returns(pointerType);
+ pointer = pointerMock.Object;
+ }
+
+ var pointerDevice = new Mock();
+ pointerDevice.Setup(d => d.TryGetPointer(It.IsAny()))
+ .Returns(pointer);
+
+ return pointerDevice;
+ }
+ }
+}
diff --git a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
index 1392635b32..36929d5e95 100644
--- a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs
@@ -216,8 +216,8 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("hsl(-1000, -1000, -1000)", new HslColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsl(-1000, -1000%, -1000%)", new HslColor(1, 0, 0, 0)), // Clamps to min
- Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max
- Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max
+ Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
+ Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsl(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsl(300, 80%, 20%)", new HslColor(1.0, 300, 0.8, 0.2)),
@@ -262,8 +262,8 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("hsv(-1000, -1000, -1000)", new HsvColor(1, 0, 0, 0)), // Clamps to min
Tuple.Create("hsv(-1000, -1000%, -1000%)", new HsvColor(1, 0, 0, 0)), // Clamps to min
- Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max
- Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max
+ Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
+ Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero)
Tuple.Create("hsv(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)),
Tuple.Create("hsv(300, 80%, 20%)", new HsvColor(1.0, 300, 0.8, 0.2)),
@@ -303,8 +303,20 @@ namespace Avalonia.Base.UnitTests.Media
Tuple.Create("#123456", new Color(0xff, 0x12, 0x34, 0x56)),
Tuple.Create("rgb(100, 30, 45)", new Color(255, 100, 30, 45)),
- Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(229, 100, 30, 45)),
- Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 100, 30, 45)),
+ Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(230, 100, 30, 45)),
+ Tuple.Create("rgba(100, 30, 45, 90%)", new Color(230, 100, 30, 45)),
+
+ Tuple.Create("rgb(255,0,0)", new Color(255, 255, 0, 0)),
+ Tuple.Create("rgb(0,255,0)", new Color(255, 0, 255, 0)),
+ Tuple.Create("rgb(0,0,255)", new Color(255, 0, 0, 255)),
+
+ Tuple.Create("rgb(100%, 0, 0)", new Color(255, 255, 0, 0)),
+ Tuple.Create("rgb(0, 100%, 0)", new Color(255, 0, 255, 0)),
+ Tuple.Create("rgb(0, 0, 100%)", new Color(255, 0, 0, 255)),
+
+ Tuple.Create("rgba(0, 0, 100%, 50%)", new Color(128, 0, 0, 255)),
+ Tuple.Create("rgba(50%, 10%, 80%, 50%)", new Color(128, 128, 26, 204)),
+ Tuple.Create("rgba(50%, 10%, 80%, 0.5)", new Color(128, 128, 26, 204)),
// HSL
Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)),
diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
index cb2fd11175..98695fe88e 100644
--- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
@@ -36,6 +36,81 @@ namespace Avalonia.Controls.UnitTests
Assert.False(target.IsDropDownOpen);
}
+ [Fact]
+ public void WrapSelection_Should_Work()
+ {
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var items = new[]
+ {
+ new ComboBoxItem() { Content = "bla" },
+ new ComboBoxItem() { Content = "dd" },
+ new ComboBoxItem() { Content = "sdf", IsEnabled = false }
+ };
+ var target = new ComboBox
+ {
+ Items = items,
+ Template = GetTemplate(),
+ WrapSelection = true
+ };
+ var root = new TestRoot(target);
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+ target.Focus();
+ Assert.Equal(target.SelectedIndex, -1);
+ Assert.True(target.IsFocused);
+ target.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = Key.Up,
+ });
+ Assert.Equal(target.SelectedIndex, 1);
+ target.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = Key.Down,
+ });
+ Assert.Equal(target.SelectedIndex, 0);
+ }
+ }
+
+ [Fact]
+ public void Focuses_Next_Item_On_Key_Down()
+ {
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var items = new[]
+ {
+ new ComboBoxItem() { Content = "bla" },
+ new ComboBoxItem() { Content = "dd", IsEnabled = false },
+ new ComboBoxItem() { Content = "sdf" }
+ };
+ var target = new ComboBox
+ {
+ Items = items,
+ Template = GetTemplate()
+ };
+ var root = new TestRoot(target);
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+ target.Focus();
+ Assert.Equal(target.SelectedIndex, -1);
+ Assert.True(target.IsFocused);
+ target.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = Key.Down,
+ });
+ Assert.Equal(target.SelectedIndex, 0);
+ target.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = InputElement.KeyDownEvent,
+ Key = Key.Down,
+ });
+ Assert.Equal(target.SelectedIndex, 2);
+ }
+ }
+
[Fact]
public void SelectionBoxItem_Is_Rectangle_With_VisualBrush_When_Selection_Is_Control()
{
diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
index 9c2d760733..db6349cc5a 100644
--- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
@@ -193,6 +193,9 @@ namespace Avalonia.Controls.UnitTests
public void Impl_Input_Should_Pass_Input_To_InputManager()
{
var inputManagerMock = new Mock();
+ inputManagerMock.DefaultValue = DefaultValue.Mock;
+ inputManagerMock.SetupAllProperties();
+
var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object);
using (UnitTestApplication.Start(services))
@@ -249,24 +252,6 @@ namespace Avalonia.Controls.UnitTests
}
}
- [Fact]
- public void Close_Should_Notify_MouseDevice()
- {
- using (UnitTestApplication.Start(TestServices.StyledWindow))
- {
- var impl = new Mock();
- var mouseDevice = new Mock();
- impl.SetupAllProperties();
- impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object);
-
- var target = new TestTopLevel(impl.Object);
-
- impl.Object.Closed();
-
- mouseDevice.Verify(x => x.TopLevelClosed(target));
- }
- }
-
[Fact]
public void Close_Should_Dispose_LayoutManager()
{
diff --git a/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs b/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs
index f950fb7e99..dfd195073b 100644
--- a/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs
+++ b/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs
@@ -16,7 +16,7 @@ public class AssetLoaderTests
static AssetLoaderTests()
{
- var resolver = Mock.Of();
+ var resolver = Mock.Of();
var descriptor = CreateAssemblyDescriptor(AssemblyNameWithWhitespace);
Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor);
@@ -49,13 +49,13 @@ public class AssetLoaderTests
Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName);
}
- private static AssemblyDescriptor CreateAssemblyDescriptor(string assemblyName)
+ private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName)
{
var assembly = Mock.Of();
Mock.Get(assembly).Setup(x => x.GetName()).Returns(new AssemblyName(assemblyName));
Mock.Get(assembly).Setup(x => x.FullName).Returns(assemblyName);
- var descriptor = Mock.Of();
+ var descriptor = Mock.Of();
Mock.Get(descriptor).Setup(x => x.Assembly).Returns(assembly);
return descriptor;
}
diff --git a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs
index 5f0d41590f..03c89732f3 100644
--- a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs
+++ b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs
@@ -16,39 +16,39 @@ namespace Avalonia.UnitTests
}
///
- public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
+ public void Post(Action action, DispatcherPriority priority)
{
action();
}
///
- public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal)
+ public void Post(Action action, T arg, DispatcherPriority priority)
{
action(arg);
}
///
- public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Action action, DispatcherPriority priority)
{
action();
return Task.CompletedTask;
}
///
- public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Func function, DispatcherPriority priority)
{
var result = function();
return Task.FromResult(result);
}
///
- public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Func function, DispatcherPriority priority)
{
return function();
}
///
- public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal)
+ public Task InvokeAsync(Func> function, DispatcherPriority priority)
{
return function();
}