diff --git a/Avalonia.sln b/Avalonia.sln
index a792774d94..5043f474c2 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -117,6 +117,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
build\CoreLibraries.props = build\CoreLibraries.props
+ build\DevAnalyzers.props = build\DevAnalyzers.props
build\EmbedXaml.props = build\EmbedXaml.props
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
@@ -234,7 +235,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
+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}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -2238,6 +2241,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
+ {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
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2303,6 +2330,7 @@ Global
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props
new file mode 100644
index 0000000000..28959dbd47
--- /dev/null
+++ b/build/DevAnalyzers.props
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 620b750a40..d16c466fe6 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -2461,6 +2461,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
if(_parent != nullptr)
{
+ auto cparent = dynamic_cast(_parent.getRaw());
+
+ if(cparent != nullptr)
+ {
+ if(cparent->WindowState() == Maximized)
+ {
+ cparent->SetWindowState(Normal);
+ }
+ }
+
_parent->GetPosition(&position);
_parent->BaseEvents->PositionChanged(position);
}
diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
index 520bbdf32b..b2c9ec72eb 100644
--- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
+++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
@@ -1,15 +1,14 @@
net6.0
- falseenable
- True
+
+ true
+ 16777216
+ false
+ false
-
-
-
-
false
@@ -23,19 +22,36 @@
-O3-O3false
+ false
+ false
+ false
+ false
+ false
+ true
+ false
+ true
+ true
+ true
+ link
+ true
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Web/LinkerConfig.xml b/samples/ControlCatalog.Web/LinkerConfig.xml
deleted file mode 100644
index 5839a0fe03..0000000000
--- a/samples/ControlCatalog.Web/LinkerConfig.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 8632551fa2..59d724db69 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -193,6 +193,14 @@
Mica
+
+
+ LeftToRight
+ RightToLeft
+
+
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index 79cf07c8d9..e8ea39abbb 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -76,6 +76,15 @@ namespace ControlCatalog
}
};
+ var flowDirections = this.Find("FlowDirection");
+ flowDirections.SelectionChanged += (sender, e) =>
+ {
+ if (flowDirections.SelectedItem is FlowDirection flowDirection)
+ {
+ this.FlowDirection = flowDirection;
+ }
+ };
+
var decorations = this.Find("Decorations");
decorations.SelectionChanged += (sender, e) =>
{
diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml
index 7de0872eae..059b4d9788 100644
--- a/samples/ControlCatalog/Pages/ButtonsPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml
@@ -147,6 +147,35 @@
+
+
+
+
+ A button with an added drop-down chevron to visually indicate it has a flyout with additional actions.
+
+
+
+
+
+
+
+
+
+
+
+ Disabled
+
+
+
+
+ Content="Re-themed"
+ Foreground="White">
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
index 2d65ea2b7b..0c46ce3724 100644
--- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
@@ -16,6 +16,7 @@
24241
+ 241
@@ -59,8 +60,11 @@
+
+
+
diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml
index eea5af5932..682f395e5b 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -15,6 +15,7 @@
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
index 7a0f9af83d..f545206a2f 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
@@ -4,7 +4,7 @@
-
+
@@ -19,9 +19,7 @@
-
-
-
+
@@ -45,40 +43,40 @@
-
+
-
+
-
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
index 0d1dd03c6e..e8e2df03b4 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
@@ -61,17 +61,17 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
index a861699675..11d6b9fdfc 100644
--- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
@@ -43,7 +43,7 @@
@@ -165,7 +165,7 @@
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
- TextElement.Foreground="{TemplateBinding Foreground}"
+ Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius}"/>
@@ -212,7 +212,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml
new file mode 100644
index 0000000000..b96c689ab6
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index 24bdbca740..33d502772e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -110,7 +110,7 @@
BorderThickness="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
- TextElement.Foreground="{DynamicResource ExpanderForeground}" />
+ Foreground="{DynamicResource ExpanderForeground}" />
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml
index 0544ce1eed..11f3c12772 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml
@@ -35,13 +35,13 @@
@@ -49,7 +49,7 @@
@@ -57,7 +57,7 @@
@@ -65,7 +65,7 @@
@@ -73,7 +73,7 @@
@@ -81,7 +81,7 @@
@@ -89,6 +89,6 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
index 09f11d9c11..33cf6bfdde 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
@@ -218,7 +218,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
index fd3e3b0ed6..cd2c02c567 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
@@ -208,7 +208,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
index 059e041e25..f86b67bb6c 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
@@ -107,7 +107,7 @@
@@ -116,7 +116,7 @@
@@ -125,7 +125,7 @@
@@ -134,7 +134,7 @@
@@ -143,7 +143,7 @@
@@ -152,7 +152,7 @@
@@ -161,7 +161,7 @@
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index 00246272e8..0e82ef7367 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -182,5 +182,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFo
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform.get()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 184
+Total Issues: 186
diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
index a0b1f99fa1..7e9947812b 100644
--- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj
+++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
@@ -19,4 +19,5 @@
+
diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs
index aa730b3219..eaa886ccbd 100644
--- a/src/Avalonia.Visuals/Media/Color.cs
+++ b/src/Avalonia.Visuals/Media/Color.cs
@@ -1,3 +1,10 @@
+// Color conversion portions of this source file are adapted from the WinUI project
+// (https://github.com/microsoft/microsoft-ui-xaml)
+// and the Windows Community Toolkit project.
+// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
using System;
using System.Globalization;
#if !BUILDTASK
@@ -14,6 +21,8 @@ namespace Avalonia.Media
#endif
readonly struct Color : IEquatable
{
+ private const double byteToDouble = 1.0 / 255;
+
static Color()
{
#if !BUILDTASK
@@ -41,6 +50,13 @@ namespace Avalonia.Media
///
public byte B { get; }
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The alpha component.
+ /// The red component.
+ /// The green component.
+ /// The blue component.
public Color(byte a, byte r, byte g, byte b)
{
A = a;
@@ -144,17 +160,46 @@ namespace Avalonia.Media
return false;
}
- if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
+ if (s[0] == '#' &&
+ TryParseHexFormat(s.AsSpan(), out color))
{
return true;
}
+ if (s.Length > 5 &&
+ (s[0] == 'r' || s[0] == 'R') &&
+ (s[1] == 'g' || s[1] == 'G') &&
+ (s[2] == 'b' || s[2] == 'B') &&
+ TryParseCssFormat(s, out color))
+ {
+ return true;
+ }
+
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'l' || s[2] == 'L') &&
+ HslColor.TryParse(s, out HslColor hslColor))
+ {
+ color = hslColor.ToRgb();
+ return true;
+ }
+
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'v' || s[2] == 'V') &&
+ HsvColor.TryParse(s, out HsvColor hsvColor))
+ {
+ color = hsvColor.ToRgb();
+ return true;
+ }
+
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
-
return true;
}
@@ -172,21 +217,52 @@ namespace Avalonia.Media
if (s.Length == 0)
{
color = default;
-
return false;
}
- if (s[0] == '#')
+ if (s[0] == '#' &&
+ TryParseHexFormat(s, out color))
+ {
+ return true;
+ }
+
+ // At this point all parsing uses strings
+ var str = s.ToString();
+
+ if (s.Length > 5 &&
+ (s[0] == 'r' || s[0] == 'R') &&
+ (s[1] == 'g' || s[1] == 'G') &&
+ (s[2] == 'b' || s[2] == 'B') &&
+ TryParseCssFormat(str, out color))
+ {
+ return true;
+ }
+
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'l' || s[2] == 'L') &&
+ HslColor.TryParse(str, out HslColor hslColor))
{
- return TryParseInternal(s, out color);
+ color = hslColor.ToRgb();
+ return true;
}
- var knownColor = KnownColors.GetKnownColor(s.ToString());
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'v' || s[2] == 'V') &&
+ HsvColor.TryParse(str, out HsvColor hsvColor))
+ {
+ color = hsvColor.ToRgb();
+ return true;
+ }
+
+ var knownColor = KnownColors.GetKnownColor(str);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
-
return true;
}
@@ -195,7 +271,7 @@ namespace Avalonia.Media
return false;
}
- private static bool TryParseInternal(ReadOnlySpan s, out Color color)
+ private static bool TryParseHexFormat(ReadOnlySpan s, out Color color)
{
static bool TryParseCore(ReadOnlySpan input, ref Color color)
{
@@ -249,6 +325,91 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
+ private static bool TryParseCssFormat(string s, out Color color)
+ {
+ color = default;
+
+ if (s is null)
+ {
+ return false;
+ }
+
+ string workingString = s.Trim();
+
+ if (workingString.Length == 0 ||
+ workingString.IndexOf(",", StringComparison.Ordinal) < 0)
+ {
+ return false;
+ }
+
+ if (workingString.Length > 6 &&
+ workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(5, workingString.Length - 6);
+ }
+
+ if (workingString.Length > 5 &&
+ workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(4, workingString.Length - 5);
+ }
+
+ 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))
+ {
+ color = new Color(0xFF, red, green, blue);
+ return true;
+ }
+ }
+ 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))
+ {
+ color = new Color((byte)(alpha * 255), red, green, blue);
+ return true;
+ }
+ }
+
+ // Local function to specially parse a double value with an optional percentage sign
+ bool TryInternalParse(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);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outDouble = percentage / 100.0;
+ return result;
+ }
+ else
+ {
+ return double.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outDouble);
+ }
+ }
+
+ return false;
+ }
+
///
/// Returns the string representation of the color.
///
@@ -272,15 +433,24 @@ namespace Avalonia.Media
return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B;
}
+ ///
+ /// Returns the HSL color model equivalent of this RGB color.
+ ///
+ /// The HSL equivalent color.
+ public HslColor ToHsl()
+ {
+ // Don't use the HslColor(Color) constructor to avoid an extra HslColor
+ return Color.ToHsl(R, G, B, A);
+ }
+
///
/// Returns the HSV color model equivalent of this RGB color.
///
/// The HSV equivalent color.
public HsvColor ToHsv()
{
- // Use the by-channel conversion method directly for performance
// Don't use the HsvColor(Color) constructor to avoid an extra HsvColor
- return HsvColor.FromRgb(R, G, B, A);
+ return Color.ToHsv(R, G, B, A);
}
///
@@ -289,11 +459,13 @@ namespace Avalonia.Media
return A == other.A && R == other.R && G == other.G && B == other.B;
}
+ ///
public override bool Equals(object? obj)
{
return obj is Color other && Equals(other);
}
+ ///
public override int GetHashCode()
{
unchecked
@@ -306,11 +478,261 @@ namespace Avalonia.Media
}
}
+ ///
+ /// Converts the given RGB color to its HSL color equivalent.
+ ///
+ /// The color in the RGB color model.
+ /// A new equivalent to the given RGBA values.
+ public static HslColor ToHsl(Color color)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsl(
+ (byteToDouble * color.R),
+ (byteToDouble * color.G),
+ (byteToDouble * color.B),
+ (byteToDouble * color.A));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSL color equivalent.
+ ///
+ /// The Red component in the RGB color model.
+ /// The Green component in the RGB color model.
+ /// The Blue component in the RGB color model.
+ /// The Alpha component.
+ /// A new equivalent to the given RGBA values.
+ public static HslColor ToHsl(
+ byte red,
+ byte green,
+ byte blue,
+ byte alpha = 0xFF)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsl(
+ (byteToDouble * red),
+ (byteToDouble * green),
+ (byteToDouble * blue),
+ (byteToDouble * alpha));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSL color equivalent.
+ ///
+ ///
+ /// Warning: No bounds checks or clamping is done on the input component values.
+ /// This method is for internal-use only and the caller must ensure bounds.
+ ///
+ /// The Red component in the RGB color model within the range 0..1.
+ /// The Green component in the RGB color model within the range 0..1.
+ /// The Blue component in the RGB color model within the range 0..1.
+ /// The Alpha component in the RGB color model within the range 0..1.
+ /// A new equivalent to the given RGBA values.
+ internal static HslColor ToHsl(
+ double r,
+ double g,
+ double b,
+ double a = 1.0)
+ {
+ // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
+ // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
+ // It has been modified.
+
+ double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
+ double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
+ double chroma = max - min;
+ double h1;
+
+ if (chroma == 0)
+ {
+ h1 = 0;
+ }
+ else if (max == r)
+ {
+ // The % operator doesn't do proper modulo on negative
+ // numbers, so we'll add 6 before using it
+ h1 = (((g - b) / chroma) + 6) % 6;
+ }
+ else if (max == g)
+ {
+ h1 = 2 + ((b - r) / chroma);
+ }
+ else
+ {
+ h1 = 4 + ((r - g) / chroma);
+ }
+
+ double lightness = 0.5 * (max + min);
+ double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
+
+ return new HslColor(a, 60 * h1, saturation, lightness, clampValues: false);
+ }
+
+ ///
+ /// Converts the given RGB color to its HSV color equivalent.
+ ///
+ /// The color in the RGB color model.
+ /// A new equivalent to the given RGBA values.
+ public static HsvColor ToHsv(Color color)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsv(
+ (byteToDouble * color.R),
+ (byteToDouble * color.G),
+ (byteToDouble * color.B),
+ (byteToDouble * color.A));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSV color equivalent.
+ ///
+ /// The Red component in the RGB color model.
+ /// The Green component in the RGB color model.
+ /// The Blue component in the RGB color model.
+ /// The Alpha component.
+ /// A new equivalent to the given RGBA values.
+ public static HsvColor ToHsv(
+ byte red,
+ byte green,
+ byte blue,
+ byte alpha = 0xFF)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsv(
+ (byteToDouble * red),
+ (byteToDouble * green),
+ (byteToDouble * blue),
+ (byteToDouble * alpha));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSV color equivalent.
+ ///
+ ///
+ /// Warning: No bounds checks or clamping is done on the input component values.
+ /// This method is for internal-use only and the caller must ensure bounds.
+ ///
+ /// The Red component in the RGB color model within the range 0..1.
+ /// The Green component in the RGB color model within the range 0..1.
+ /// The Blue component in the RGB color model within the range 0..1.
+ /// The Alpha component in the RGB color model within the range 0..1.
+ /// A new equivalent to the given RGBA values.
+ internal static HsvColor ToHsv(
+ double r,
+ double g,
+ double b,
+ double a = 1.0)
+ {
+ // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
+ // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
+ // This was used because it is the best documented and likely most optimized for performance
+ // Alpha support was added
+
+ double hue;
+ double saturation;
+ double value;
+
+ double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
+ double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
+
+ // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255).
+ // Conceptually speaking, it represents how much color is present.
+ // If at least one of R, G, B is 255, then there exists as much color as there can be.
+ // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds
+ // to black (i.e., the absence of any color).
+ value = max;
+
+ // The "chroma" of the color is a value directly proportional to the extent to which
+ // the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0),
+ // then the chroma is maximized - this is a pure yellow, no gray of any kind.
+ // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero
+ // implies that this color is pure greyscale, with no actual hue to be found.
+ var chroma = max - min;
+
+ // If the chrome is zero, then hue is technically undefined - a greyscale color
+ // has no hue. For the sake of convenience, we'll just set hue to zero, since
+ // it will be unused in this circumstance. Since the color is purely gray,
+ // saturation is also equal to zero - you can think of saturation as basically
+ // a measure of hue intensity, such that no hue at all corresponds to a
+ // nonexistent intensity.
+ if (chroma == 0)
+ {
+ hue = 0.0;
+ saturation = 0.0;
+ }
+ else
+ {
+ // In this block, hue is properly defined, so we'll extract both hue
+ // and saturation information from the RGB color.
+
+ // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees.
+ // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red.
+ // Every other hue is somewhere between either red and green, green and blue, and blue and red,
+ // so every other hue can be thought of as an angle on this color wheel.
+ // These if/else statements determines where on this color wheel our color lies.
+ if (r == max)
+ {
+ // If the red channel is the most pronounced channel, then we exist
+ // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees
+ // where red dominates. We figure out where in that section we are exactly
+ // by considering whether the green or the blue channel is greater - by subtracting green from blue,
+ // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then
+ // we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger,
+ // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue
+ // even if we're very close to greyscale.
+ hue = 60 * (g - b) / chroma;
+ }
+ else if (g == max)
+ {
+ // We do the exact same for the case where the green channel is the most pronounced channel,
+ // only this time we want to see if we should tilt towards the blue direction or the red direction.
+ // We add 120 to center our value in the green third of the color wheel.
+ hue = 120 + (60 * (b - r) / chroma);
+ }
+ else // blue == max
+ {
+ // And we also do the exact same for the case where the blue channel is the most pronounced channel,
+ // only this time we want to see if we should tilt towards the red direction or the green direction.
+ // We add 240 to center our value in the blue third of the color wheel.
+ hue = 240 + (60 * (r - g) / chroma);
+ }
+
+ // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero -
+ // this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values.
+ if (hue < 0.0)
+ {
+ hue += 360.0;
+ }
+
+ // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is.
+ // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value).
+ // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1,
+ // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be.
+ // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5,
+ // meaning that this color is partially washed out.
+ // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue.
+ saturation = chroma / value;
+ }
+
+ return new HsvColor(a, hue, saturation, value, clampValues: false);
+ }
+
+ ///
+ /// Indicates whether the values of two specified objects are equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are equal; otherwise, false.
public static bool operator ==(Color left, Color right)
{
return left.Equals(right);
}
+ ///
+ /// Indicates whether the values of two specified objects are not equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are not equal; otherwise, false.
public static bool operator !=(Color left, Color right)
{
return !left.Equals(right);
diff --git a/src/Avalonia.Visuals/Media/HslColor.cs b/src/Avalonia.Visuals/Media/HslColor.cs
new file mode 100644
index 0000000000..e27a4f3106
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/HslColor.cs
@@ -0,0 +1,476 @@
+// Color conversion portions of this source file are adapted from the Windows Community Toolkit project.
+// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Globalization;
+using System.Text;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Defines a color using the hue/saturation/lightness (HSL) model.
+ ///
+#if !BUILDTASK
+ public
+#endif
+ readonly struct HslColor : IEquatable
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ public HslColor(
+ double alpha,
+ double hue,
+ double saturation,
+ double lightness)
+ {
+ A = MathUtilities.Clamp(alpha, 0.0, 1.0);
+ H = MathUtilities.Clamp(hue, 0.0, 360.0);
+ S = MathUtilities.Clamp(saturation, 0.0, 1.0);
+ L = MathUtilities.Clamp(lightness, 0.0, 1.0);
+
+ // The maximum value of Hue is technically 360 minus epsilon (just below 360).
+ // This is because, in a color circle, 360 degrees is equivalent to 0 degrees.
+ // However, that is too tricky to work with in code and isn't as intuitive.
+ // Therefore, since 360 == 0, just wrap 360 if needed back to 0.
+ H = (H == 360.0 ? 0 : H);
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ ///
+ /// This constructor exists only for internal use where performance is critical.
+ /// Whether or not the component values are in the correct ranges must be known.
+ ///
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ /// Whether to clamp component values to their required ranges.
+ internal HslColor(
+ double alpha,
+ double hue,
+ double saturation,
+ double lightness,
+ bool clampValues)
+ {
+ if (clampValues)
+ {
+ A = MathUtilities.Clamp(alpha, 0.0, 1.0);
+ H = MathUtilities.Clamp(hue, 0.0, 360.0);
+ S = MathUtilities.Clamp(saturation, 0.0, 1.0);
+ L = MathUtilities.Clamp(lightness, 0.0, 1.0);
+
+ // See comments in constructor above
+ H = (H == 360.0 ? 0 : H);
+ }
+ else
+ {
+ A = alpha;
+ H = hue;
+ S = saturation;
+ L = lightness;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The RGB color to convert to HSL.
+ public HslColor(Color color)
+ {
+ var hsl = Color.ToHsl(color);
+
+ A = hsl.A;
+ H = hsl.H;
+ S = hsl.S;
+ L = hsl.L;
+ }
+
+ ///
+ /// Gets the Alpha (transparency) component in the range from 0..1.
+ ///
+ public double A { get; }
+
+ ///
+ /// Gets the Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ ///
+ public double H { get; }
+
+ ///
+ /// Gets the Saturation component in the range from 0..1.
+ ///
+ public double S { get; }
+
+ ///
+ /// Gets the Lightness component in the range from 0..1.
+ ///
+ public double L { get; }
+
+ ///
+ public bool Equals(HslColor other)
+ {
+ return other.A == A &&
+ other.H == H &&
+ other.S == S &&
+ other.L == L;
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is HslColor hslColor)
+ {
+ return Equals(hslColor);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Gets a hashcode for this object.
+ /// Hashcode is not guaranteed to be unique.
+ ///
+ /// The hashcode for this object.
+ public override int GetHashCode()
+ {
+ // Same algorithm as Color
+ // This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements
+ unchecked
+ {
+ int hashCode = A.GetHashCode();
+ hashCode = (hashCode * 397) ^ H.GetHashCode();
+ hashCode = (hashCode * 397) ^ S.GetHashCode();
+ hashCode = (hashCode * 397) ^ L.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ ///
+ /// Returns the RGB color model equivalent of this HSL color.
+ ///
+ /// The RGB equivalent color.
+ public Color ToRgb()
+ {
+ // Use the by-component conversion method directly for performance
+ return HslColor.ToRgb(H, S, L, A);
+ }
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ // Use a format similar to CSS. However:
+ // - To ensure precision is never lost, allow decimal places.
+ // This is especially important for round-trip serialization.
+ // - To maintain numerical consistency, do not use percent.
+ //
+ // Example:
+ //
+ // hsla(hue, saturation, lightness, alpha)
+ // hsla(230, 1.0, 0.5, 1.0)
+ //
+
+ sb.Append("hsva(");
+ sb.Append(H.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(S.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(L.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(A.ToString(CultureInfo.InvariantCulture));
+ sb.Append(')');
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Parses an HSL color string.
+ ///
+ /// The HSL color string to parse.
+ /// The parsed .
+ public static HslColor Parse(string s)
+ {
+ if (s is null)
+ {
+ throw new ArgumentNullException(nameof(s));
+ }
+
+ if (TryParse(s, out HslColor hslColor))
+ {
+ return hslColor;
+ }
+
+ throw new FormatException($"Invalid HSL color string: '{s}'.");
+ }
+
+ ///
+ /// Parses an HSL color string.
+ ///
+ /// The HSL color string to parse.
+ /// The parsed .
+ /// True if parsing was successful; otherwise, false.
+ public static bool TryParse(string s, out HslColor hslColor)
+ {
+ hslColor = default;
+
+ if (s is null)
+ {
+ return false;
+ }
+
+ string workingString = s.Trim();
+
+ if (workingString.Length == 0 ||
+ workingString.IndexOf(",", StringComparison.Ordinal) < 0)
+ {
+ return false;
+ }
+
+ if (workingString.Length > 6 &&
+ workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(5, workingString.Length - 6);
+ }
+
+ if (workingString.Length > 5 &&
+ workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(4, workingString.Length - 5);
+ }
+
+ string[] components = workingString.Split(',');
+
+ if (components.Length == 3) // HSL
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double lightness))
+ {
+ hslColor = new HslColor(1.0, hue, saturation, lightness);
+ return true;
+ }
+ }
+ else if (components.Length == 4) // HSLA
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double lightness) &&
+ TryInternalParse(components[3], out double alpha))
+ {
+ hslColor = new HslColor(alpha, hue, saturation, lightness);
+ return true;
+ }
+ }
+
+ // Local function to specially parse a double value with an optional percentage sign
+ bool TryInternalParse(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);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outDouble = percentage / 100.0;
+ return result;
+ }
+ else
+ {
+ return double.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outDouble);
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Creates a new from individual color component values.
+ ///
+ ///
+ /// This exists for symmetry with the struct; however, the
+ /// appropriate constructor should commonly be used instead.
+ ///
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ /// A new built from the individual color component values.
+ public static HslColor FromAhsl(double a, double h, double s, double l)
+ {
+ return new HslColor(a, h, s, l);
+ }
+
+ ///
+ /// Creates a new from individual color component values.
+ ///
+ ///
+ /// This exists for symmetry with the struct; however, the
+ /// appropriate constructor should commonly be used instead.
+ ///
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ /// A new built from the individual color component values.
+ public static HslColor FromHsl(double h, double s, double l)
+ {
+ return new HslColor(1.0, h, s, l);
+ }
+
+ ///
+ /// Converts the given HSL color to its RGB color equivalent.
+ ///
+ /// The color in the HSL color model.
+ /// A new RGB equivalent to the given HSLA values.
+ public static Color ToRgb(HslColor hslColor)
+ {
+ return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
+ }
+
+ ///
+ /// Converts the given HSLA color component values to their RGB color equivalent.
+ ///
+ /// The Hue component in the HSL color model in the range from 0..360.
+ /// The Saturation component in the HSL color model in the range from 0..1.
+ /// The Lightness component in the HSL color model in the range from 0..1.
+ /// The Alpha component in the range from 0..1.
+ /// A new RGB equivalent to the given HSLA values.
+ public static Color ToRgb(
+ double hue,
+ double saturation,
+ double lightness,
+ double alpha = 1.0)
+ {
+ // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
+ // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
+ // It has been modified to ensure input ranges and not throw exceptions.
+
+ // We want the hue to be between 0 and 359,
+ // so we first ensure that that's the case.
+ while (hue >= 360.0)
+ {
+ hue -= 360.0;
+ }
+
+ while (hue < 0.0)
+ {
+ hue += 360.0;
+ }
+
+ // We similarly clamp saturation, lightness and alpha between 0 and 1.
+ saturation = saturation < 0.0 ? 0.0 : saturation;
+ saturation = saturation > 1.0 ? 1.0 : saturation;
+
+ lightness = lightness < 0.0 ? 0.0 : lightness;
+ lightness = lightness > 1.0 ? 1.0 : lightness;
+
+ alpha = alpha < 0.0 ? 0.0 : alpha;
+ alpha = alpha > 1.0 ? 1.0 : alpha;
+
+ double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation;
+ double h1 = hue / 60;
+ double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
+ double m = lightness - (0.5 * chroma);
+ double r1, g1, b1;
+
+ if (h1 < 1)
+ {
+ r1 = chroma;
+ g1 = x;
+ b1 = 0;
+ }
+ else if (h1 < 2)
+ {
+ r1 = x;
+ g1 = chroma;
+ b1 = 0;
+ }
+ else if (h1 < 3)
+ {
+ r1 = 0;
+ g1 = chroma;
+ b1 = x;
+ }
+ else if (h1 < 4)
+ {
+ r1 = 0;
+ g1 = x;
+ b1 = chroma;
+ }
+ else if (h1 < 5)
+ {
+ r1 = x;
+ g1 = 0;
+ b1 = chroma;
+ }
+ else
+ {
+ r1 = chroma;
+ g1 = 0;
+ b1 = x;
+ }
+
+ return Color.FromArgb(
+ (byte)Math.Round(255 * alpha),
+ (byte)Math.Round(255 * (r1 + m)),
+ (byte)Math.Round(255 * (g1 + m)),
+ (byte)Math.Round(255 * (b1 + m)));
+ }
+
+ ///
+ /// Indicates whether the values of two specified objects are equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are equal; otherwise, false.
+ public static bool operator ==(HslColor left, HslColor right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ /// Indicates whether the values of two specified objects are not equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are not equal; otherwise, false.
+ public static bool operator !=(HslColor left, HslColor right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ /// Explicit conversion from an to a .
+ ///
+ /// The to convert.
+ public static explicit operator Color(HslColor hslColor)
+ {
+ return hslColor.ToRgb();
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs
index 140f87b3eb..164aeb1df1 100644
--- a/src/Avalonia.Visuals/Media/HsvColor.cs
+++ b/src/Avalonia.Visuals/Media/HsvColor.cs
@@ -1,6 +1,6 @@
-// Color conversion portions of this source file are adapted from the WinUI project.
-// (https://github.com/microsoft/microsoft-ui-xaml)
-//
+// Color conversion portions of this source file are adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
@@ -21,11 +21,11 @@ namespace Avalonia.Media
///
/// Initializes a new instance of the struct.
///
- /// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
- /// The Saturation channel value in the range from 0..1.
- /// The Value channel value in the range from 0..1.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
public HsvColor(
double alpha,
double hue,
@@ -49,14 +49,14 @@ namespace Avalonia.Media
///
///
/// This constructor exists only for internal use where performance is critical.
- /// Whether or not the channel values are in the correct ranges must be known.
+ /// Whether or not the component values are in the correct ranges must be known.
///
- /// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
- /// The Saturation channel value in the range from 0..1.
- /// The Value channel value in the range from 0..1.
- /// Whether to clamp channel values to their required ranges.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
+ /// Whether to clamp component values to their required ranges.
internal HsvColor(
double alpha,
double hue,
@@ -89,7 +89,7 @@ namespace Avalonia.Media
/// The RGB color to convert to HSV.
public HsvColor(Color color)
{
- var hsv = HsvColor.FromRgb(color);
+ var hsv = Color.ToHsv(color);
A = hsv.A;
H = hsv.H;
@@ -98,23 +98,23 @@ namespace Avalonia.Media
}
///
- /// Gets the Alpha (transparency) channel value in the range from 0..1.
+ /// Gets the Alpha (transparency) component in the range from 0..1.
///
public double A { get; }
///
- /// Gets the Hue channel value in the range from 0..360.
+ /// Gets the Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
///
public double H { get; }
///
- /// Gets the Saturation channel value in the range from 0..1.
+ /// Gets the Saturation component in the range from 0..1.
///
public double S { get; }
///
- /// Gets the Value channel value in the range from 0..1.
+ /// Gets the Value component in the range from 0..1.
///
public double V { get; }
@@ -165,7 +165,7 @@ namespace Avalonia.Media
/// The RGB equivalent color.
public Color ToRgb()
{
- // Use the by-channel conversion method directly for performance
+ // Use the by-component conversion method directly for performance
return HsvColor.ToRgb(H, S, V, A);
}
@@ -174,26 +174,16 @@ namespace Avalonia.Media
{
var sb = new StringBuilder();
- // Use a format similar to HSL in HTML/CSS "hsla(0, 100%, 50%, 0.5)"
- //
- // However:
- // - To ensure precision is never lost, allow decimal places
- // - To maintain numerical consistency do not use percent
+ // Use a format similar to CSS. However:
+ // - To ensure precision is never lost, allow decimal places.
+ // This is especially important for round-trip serialization.
+ // - To maintain numerical consistency, do not use percent.
//
// Example:
//
// hsva(hue, saturation, value, alpha)
// hsva(230, 1.0, 0.5, 1.0)
//
- // Where:
- //
- // hue : double from 0 to 360
- // saturation : double from 0 to 1
- // (HTML uses a percentage)
- // value : double from 0 to 1
- // (HTML uses a percentage)
- // alpha : double from 0 to 1
- // (HTML does not use a percentage for alpha)
sb.Append("hsva(");
sb.Append(H.ToString(CultureInfo.InvariantCulture));
@@ -270,8 +260,8 @@ namespace Avalonia.Media
if (components.Length == 3) // HSV
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
- double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
- double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value))
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double value))
{
hsvColor = new HsvColor(1.0, hue, saturation, value);
return true;
@@ -280,35 +270,78 @@ namespace Avalonia.Media
else if (components.Length == 4) // HSVA
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
- double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
- double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value) &&
- double.TryParse(components[3], NumberStyles.Number, CultureInfo.InvariantCulture, out double alpha))
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double value) &&
+ TryInternalParse(components[3], out double alpha))
{
hsvColor = new HsvColor(alpha, hue, saturation, value);
return true;
}
}
+ // Local function to specially parse a double value with an optional percentage sign
+ bool TryInternalParse(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);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outDouble = percentage / 100.0;
+ return result;
+ }
+ else
+ {
+ return double.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outDouble);
+ }
+ }
+
return false;
}
///
- /// Creates a new from individual color channel values.
+ /// Creates a new from individual color component values.
///
///
/// This exists for symmetry with the struct; however, the
/// appropriate constructor should commonly be used instead.
///
- /// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
- /// The Saturation channel value in the range from 0..1.
- /// The Value channel value in the range from 0..1.
- /// A new built from the individual color channel values.
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
+ /// A new built from the individual color component values.
public static HsvColor FromAhsv(double a, double h, double s, double v)
{
return new HsvColor(a, h, s, v);
}
+ ///
+ /// Creates a new from individual color component values.
+ ///
+ ///
+ /// This exists for symmetry with the struct; however, the
+ /// appropriate constructor should commonly be used instead.
+ ///
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
+ /// A new built from the individual color component values.
+ public static HsvColor FromHsv(double h, double s, double v)
+ {
+ return new HsvColor(1.0, h, s, v);
+ }
+
///
/// Converts the given HSV color to its RGB color equivalent.
///
@@ -320,12 +353,12 @@ namespace Avalonia.Media
}
///
- /// Converts the given HSVA color channel values to its RGB color equivalent.
+ /// Converts the given HSVA color component values to their RGB color equivalent.
///
- /// The hue channel value in the HSV color model in the range from 0..360.
- /// The saturation channel value in the HSV color model in the range from 0..1.
- /// The value channel value in the HSV color model in the range from 0..1.
- /// The alpha channel value in the range from 0..1.
+ /// The Hue component in the HSV color model in the range from 0..360.
+ /// The Saturation component in the HSV color model in the range from 0..1.
+ /// The Value component in the HSV color model in the range from 0..1.
+ /// The Alpha component in the range from 0..1.
/// A new RGB equivalent to the given HSVA values.
public static Color ToRgb(
double hue,
@@ -336,7 +369,7 @@ namespace Avalonia.Media
// Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
// https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
// This was used because it is the best documented and likely most optimized for performance
- // Alpha channel support was added
+ // Alpha support was added
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
@@ -457,152 +490,6 @@ namespace Avalonia.Media
(byte)Math.Round(b * 255));
}
- ///
- /// Converts the given RGB color to its HSV color equivalent.
- ///
- /// The color in the RGB color model.
- /// A new equivalent to the given RGBA values.
- public static HsvColor FromRgb(Color color)
- {
- return HsvColor.FromRgb(color.R, color.G, color.B, color.A);
- }
-
- ///
- /// Converts the given RGBA color channel values to its HSV color equivalent.
- ///
- /// The red channel value in the RGB color model.
- /// The green channel value in the RGB color model.
- /// The blue channel value in the RGB color model.
- /// The alpha channel value.
- /// A new equivalent to the given RGBA values.
- public static HsvColor FromRgb(
- byte red,
- byte green,
- byte blue,
- byte alpha = 0xFF)
- {
- // Normalize RGBA channel values into the 0..1 range
- return HsvColor.FromRgb(
- (red / 255.0),
- (green / 255.0),
- (blue / 255.0),
- (alpha / 255.0));
- }
-
- // TODO: Mark the below method Internal and make Internals visible to Avalonia.Controls...
-
- ///
- /// Converts the given RGBA color channel values to its HSV color equivalent.
- ///
- ///
- /// Warning: No bounds checks or clamping is done on the input channel values.
- /// This method is for internal-use only and the caller must ensure bounds.
- ///
- /// The red channel value in the RGB color model within the range 0..1.
- /// The green channel value in the RGB color model within the range 0..1.
- /// The blue channel value in the RGB color model within the range 0..1.
- /// The alpha channel value in the RGB color model within the range 0..1.
- /// A new equivalent to the given RGBA values.
- public static HsvColor FromRgb(
- double r,
- double g,
- double b,
- double a = 1.0)
- {
- // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
- // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
- // This was used because it is the best documented and likely most optimized for performance
- // Alpha channel support was added
-
- double hue;
- double saturation;
- double value;
-
- double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
- double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
-
- // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255).
- // Conceptually speaking, it represents how much color is present.
- // If at least one of R, G, B is 255, then there exists as much color as there can be.
- // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds
- // to black (i.e., the absence of any color).
- value = max;
-
- // The "chroma" of the color is a value directly proportional to the extent to which
- // the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0),
- // then the chroma is maximized - this is a pure yellow, no gray of any kind.
- // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero
- // implies that this color is pure greyscale, with no actual hue to be found.
- var chroma = max - min;
-
- // If the chrome is zero, then hue is technically undefined - a greyscale color
- // has no hue. For the sake of convenience, we'll just set hue to zero, since
- // it will be unused in this circumstance. Since the color is purely gray,
- // saturation is also equal to zero - you can think of saturation as basically
- // a measure of hue intensity, such that no hue at all corresponds to a
- // nonexistent intensity.
- if (chroma == 0)
- {
- hue = 0.0;
- saturation = 0.0;
- }
- else
- {
- // In this block, hue is properly defined, so we'll extract both hue
- // and saturation information from the RGB color.
-
- // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees.
- // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red.
- // Every other hue is somewhere between either red and green, green and blue, and blue and red,
- // so every other hue can be thought of as an angle on this color wheel.
- // These if/else statements determines where on this color wheel our color lies.
- if (r == max)
- {
- // If the red channel is the most pronounced channel, then we exist
- // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees
- // where red dominates. We figure out where in that section we are exactly
- // by considering whether the green or the blue channel is greater - by subtracting green from blue,
- // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then
- // we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger,
- // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue
- // even if we're very close to greyscale.
- hue = 60 * (g - b) / chroma;
- }
- else if (g == max)
- {
- // We do the exact same for the case where the green channel is the most pronounced channel,
- // only this time we want to see if we should tilt towards the blue direction or the red direction.
- // We add 120 to center our value in the green third of the color wheel.
- hue = 120 + (60 * (b - r) / chroma);
- }
- else // blue == max
- {
- // And we also do the exact same for the case where the blue channel is the most pronounced channel,
- // only this time we want to see if we should tilt towards the red direction or the green direction.
- // We add 240 to center our value in the blue third of the color wheel.
- hue = 240 + (60 * (r - g) / chroma);
- }
-
- // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero -
- // this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values.
- if (hue < 0.0)
- {
- hue += 360.0;
- }
-
- // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is.
- // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value).
- // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1,
- // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be.
- // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5,
- // meaning that this color is partially washed out.
- // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue.
- saturation = chroma / value;
- }
-
- return new HsvColor(a, hue, saturation, value, false);
- }
-
///
/// Indicates whether the values of two specified objects are equal.
///
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index 23016de148..2c0298affa 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -285,6 +285,12 @@ namespace Avalonia.Rendering
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
+ if (visual.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+ renderTransform *= mirrorMatrix;
+ }
+
m = renderTransform * m;
if (clipToBounds)
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
index 63c22efc3f..019c3e0e9b 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
@@ -195,6 +195,12 @@ namespace Avalonia.Rendering.SceneGraph
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
+ if (visual.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+ renderTransform *= mirrorMatrix;
+ }
+
m = renderTransform * m;
using (contextImpl.BeginUpdate(node))
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index 324b253a0f..26a7d5d3b8 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -68,6 +68,12 @@ namespace Avalonia
public static readonly StyledProperty OpacityMaskProperty =
AvaloniaProperty.Register(nameof(OpacityMask));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty HasMirrorTransformProperty =
+ AvaloniaProperty.RegisterDirect(nameof(HasMirrorTransform), o => o.HasMirrorTransform);
+
///
/// Defines the property.
///
@@ -96,6 +102,7 @@ namespace Avalonia
private TransformedBounds? _transformedBounds;
private IRenderRoot? _visualRoot;
private IVisual? _visualParent;
+ private bool _hasMirrorTransform;
///
/// Initializes static members of the class.
@@ -107,7 +114,8 @@ namespace Avalonia
ClipProperty,
ClipToBoundsProperty,
IsVisibleProperty,
- OpacityProperty);
+ OpacityProperty,
+ HasMirrorTransformProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
ZIndexProperty.Changed.Subscribe(ZIndexChanged);
}
@@ -219,6 +227,15 @@ namespace Avalonia
set { SetValue(OpacityMaskProperty, value); }
}
+ ///
+ /// Gets or sets a value indicating whether to apply mirror transform on this control.
+ ///
+ public bool HasMirrorTransform
+ {
+ get { return _hasMirrorTransform; }
+ protected set { SetAndRaise(HasMirrorTransformProperty, ref _hasMirrorTransform, value); }
+ }
+
///
/// Gets or sets the render transform of the control.
///
diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs
index ff8a515db3..3a3c2693d0 100644
--- a/src/Avalonia.Visuals/VisualExtensions.cs
+++ b/src/Avalonia.Visuals/VisualExtensions.cs
@@ -110,6 +110,12 @@ namespace Avalonia
result *= renderTransform;
}
+ if (v.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, v.Bounds.Width, 0);
+ result *= mirrorMatrix;
+ }
+
var topLeft = v.Bounds.TopLeft;
if (topLeft != default)
diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs
index 97c4554de6..b1251618c4 100644
--- a/src/Avalonia.Visuals/VisualTree/IVisual.cs
+++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs
@@ -75,6 +75,11 @@ namespace Avalonia.VisualTree
///
IBrush? OpacityMask { get; set; }
+ ///
+ /// Gets a value indicating whether to apply mirror transform on this control.
+ ///
+ bool HasMirrorTransform { get; }
+
///
/// Gets or sets the render transform of the control.
///
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
index f75c96c759..b89ea8399a 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
@@ -12,5 +12,6 @@
-
+
+
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 548aae31a8..6624392258 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -68,4 +68,5 @@
+
diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
index 21370b3bf5..119f57973c 100644
--- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
+++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
@@ -18,4 +18,5 @@
+
diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs
index c14b150c1a..b5bb31ed81 100644
--- a/src/Shared/ModuleInitializer.cs
+++ b/src/Shared/ModuleInitializer.cs
@@ -3,7 +3,7 @@ namespace System.Runtime.CompilerServices
#if !NET5_0_OR_GREATER
internal class ModuleInitializerAttribute : Attribute
{
-
+
}
#endif
}
diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
index 50ace8209c..26c0fb756e 100644
--- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
+++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
@@ -16,5 +16,6 @@
-
+
+
diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
index a1d8a60cb9..bd1f05b30f 100644
--- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
+++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
@@ -18,4 +18,5 @@
+
diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
index 9f49808422..cc8a40e2d4 100644
--- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
+++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
@@ -18,4 +18,5 @@
+
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index ba842757d7..b9a1880435 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -16,5 +16,6 @@
-
+
+
diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
index 39a0305b1e..4e134b7d4e 100644
--- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
+++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
@@ -19,4 +19,6 @@
+
+
diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj
new file mode 100644
index 0000000000..53e3e74e76
--- /dev/null
+++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ 10
+ enable
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs
new file mode 100644
index 0000000000..8ecd9119f6
--- /dev/null
+++ b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs
@@ -0,0 +1,40 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace DevAnalyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class GenericVirtualAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "AVADEV1001";
+
+ private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+ DiagnosticId,
+ "Do not use generic virtual methods",
+ "Method '{0}' is a generic virtual method",
+ "Performance",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Generic virtual methods affect JIT startup time adversly and should be avoided.");
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
+ }
+
+ private static void AnalyzeMethod(SymbolAnalysisContext context)
+ {
+ var symbol = (IMethodSymbol)context.Symbol;
+
+ if (symbol.IsGenericMethod &&
+ (symbol.IsVirtual || symbol.ContainingType.TypeKind == TypeKind.Interface))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name));
+ }
+ }
+}
diff --git a/src/tools/DevAnalyzers/GlobalSuppressions.cs b/src/tools/DevAnalyzers/GlobalSuppressions.cs
new file mode 100644
index 0000000000..9428b904b8
--- /dev/null
+++ b/src/tools/DevAnalyzers/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
index 3994d20c68..2dbb054221 100644
--- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
+++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
@@ -19,7 +19,7 @@
-
+
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs
new file mode 100644
index 0000000000..885e202544
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs
@@ -0,0 +1,149 @@
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_Binding
+ {
+ private static TestClass _target = null!;
+ private static TestBindingObservable s_stringSource = new();
+ private static TestBindingObservable s_struct1Source = new();
+ private static TestBindingObservable s_struct2Source = new();
+ private static TestBindingObservable s_struct3Source = new();
+ private static TestBindingObservable s_struct4Source = new();
+ private static TestBindingObservable s_struct5Source = new();
+ private static TestBindingObservable s_struct6Source = new();
+ private static TestBindingObservable s_struct7Source = new();
+ private static TestBindingObservable s_struct8Source = new();
+
+ public AvaloniaObject_Binding()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _target = new TestClass();
+ }
+
+ [Benchmark]
+ public void Setup_Dispose_LocalValue_Bindings()
+ {
+ var target = _target;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
+ using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
+ using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
+ using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
+ using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
+ using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
+ using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
+ using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
+ using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
+ }
+ }
+
+
+ [Benchmark]
+ public void Fire_LocalValue_Bindings()
+ {
+ var target = _target;
+
+ using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
+ using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
+ using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
+ using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
+ using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
+ using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
+ using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
+ using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
+ using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
+
+ for (var i = 0; i < 100; ++i)
+ {
+ s_stringSource.OnNext(i.ToString());
+ s_struct1Source.OnNext(new(i + 1));
+ s_struct2Source.OnNext(new(i + 1));
+ s_struct3Source.OnNext(new(i + 1));
+ s_struct4Source.OnNext(new(i + 1));
+ s_struct5Source.OnNext(new(i + 1));
+ s_struct6Source.OnNext(new(i + 1));
+ s_struct7Source.OnNext(new(i + 1));
+ s_struct8Source.OnNext(new(i + 1));
+ }
+ }
+
+ [GlobalSetup(Target = nameof(Fire_LocalValue_Bindings_With_Style_Values))]
+ public void SetupStyleValues()
+ {
+ _target = new TestClass();
+ _target.SetValue(TestClass.StringProperty, "foo", BindingPriority.Style);
+ _target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
+ }
+
+ [Benchmark]
+ public void Fire_LocalValue_Bindings_With_Style_Values()
+ {
+ var target = _target;
+
+ using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
+ using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
+ using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
+ using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
+ using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
+ using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
+ using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
+ using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
+ using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
+
+ for (var i = 0; i < 100; ++i)
+ {
+ s_stringSource.OnNext(i.ToString());
+ s_struct1Source.OnNext(new(i + 1));
+ s_struct2Source.OnNext(new(i + 1));
+ s_struct3Source.OnNext(new(i + 1));
+ s_struct4Source.OnNext(new(i + 1));
+ s_struct5Source.OnNext(new(i + 1));
+ s_struct6Source.OnNext(new(i + 1));
+ s_struct7Source.OnNext(new(i + 1));
+ s_struct8Source.OnNext(new(i + 1));
+ }
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs
new file mode 100644
index 0000000000..1f07d4e867
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs
@@ -0,0 +1,81 @@
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_Construct
+ {
+ public AvaloniaObject_Construct()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [Benchmark(Baseline = true)]
+ public void ConstructClrObject_And_Set_Values()
+ {
+ var target = new BaselineTestClass();
+ target.StringProperty = "foo";
+ target.Struct1Property = new(1);
+ target.Struct2Property = new(1);
+ target.Struct3Property = new(1);
+ target.Struct4Property = new(1);
+ target.Struct5Property = new(1);
+ target.Struct6Property = new(1);
+ target.Struct7Property = new(1);
+ target.Struct8Property = new(1);
+ }
+
+ [Benchmark]
+ public void Construct_And_Set_Values()
+ {
+ var target = new TestClass();
+ target.SetValue(TestClass.StringProperty, "foo");
+ target.SetValue(TestClass.Struct1Property, new(1));
+ target.SetValue(TestClass.Struct2Property, new(1));
+ target.SetValue(TestClass.Struct3Property, new(1));
+ target.SetValue(TestClass.Struct4Property, new(1));
+ target.SetValue(TestClass.Struct5Property, new(1));
+ target.SetValue(TestClass.Struct6Property, new(1));
+ target.SetValue(TestClass.Struct7Property, new(1));
+ target.SetValue(TestClass.Struct8Property, new(1));
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+
+ private class BaselineTestClass
+ {
+ public string? StringProperty { get; set; }
+ public Struct1 Struct1Property { get; set; }
+ public Struct2 Struct2Property { get; set; }
+ public Struct3 Struct3Property { get; set; }
+ public Struct4 Struct4Property { get; set; }
+ public Struct5 Struct5Property { get; set; }
+ public Struct6 Struct6Property { get; set; }
+ public Struct7 Struct7Property { get; set; }
+ public Struct8 Struct8Property { get; set; }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs
new file mode 100644
index 0000000000..9157dd156c
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_GetObservable
+ {
+ private TestClass _target = null!;
+ public static int result;
+
+ public AvaloniaObject_GetObservable()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _target = new();
+ }
+
+ [Benchmark(Baseline = true)]
+ public void PropertyChangedSubscription()
+ {
+ var target = _target;
+
+ static void ChangeHandler(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == TestClass.StringProperty)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value?.Length ?? 0;
+ }
+ else if (e.Property == TestClass.Struct1Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct2Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct3Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct4Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct5Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct6Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct7Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct8Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ }
+
+ target.PropertyChanged += ChangeHandler;
+
+ // GetObservable fires with the initial value so to compare like-for-like we also need
+ // to get the initial value here.
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ target.SetValue(TestClass.StringProperty, "foo" + i);
+ target.SetValue(TestClass.Struct1Property, new(i + 1));
+ target.SetValue(TestClass.Struct2Property, new(i + 1));
+ target.SetValue(TestClass.Struct3Property, new(i + 1));
+ target.SetValue(TestClass.Struct4Property, new(i + 1));
+ target.SetValue(TestClass.Struct5Property, new(i + 1));
+ target.SetValue(TestClass.Struct6Property, new(i + 1));
+ target.SetValue(TestClass.Struct7Property, new(i + 1));
+ target.SetValue(TestClass.Struct8Property, new(i + 1));
+ }
+
+ target.PropertyChanged -= ChangeHandler;
+ }
+
+ [Benchmark]
+ public void GetObservables()
+ {
+ var target = _target;
+
+ var sub1 = target.GetObservable(TestClass.StringProperty).Subscribe(x => result += x?.Length ?? 0);
+ var sub2 = target.GetObservable(TestClass.Struct1Property).Subscribe(x => result += x.Int1);
+ var sub3 = target.GetObservable(TestClass.Struct2Property).Subscribe(x => result += x.Int1);
+ var sub4 = target.GetObservable(TestClass.Struct3Property).Subscribe(x => result += x.Int1);
+ var sub5 = target.GetObservable(TestClass.Struct4Property).Subscribe(x => result += x.Int1);
+ var sub6 = target.GetObservable(TestClass.Struct5Property).Subscribe(x => result += x.Int1);
+ var sub7 = target.GetObservable(TestClass.Struct6Property).Subscribe(x => result += x.Int1);
+ var sub8 = target.GetObservable(TestClass.Struct7Property).Subscribe(x => result += x.Int1);
+ var sub9 = target.GetObservable(TestClass.Struct8Property).Subscribe(x => result += x.Int1);
+
+ for (var i = 0; i < 100; ++i)
+ {
+ target.SetValue(TestClass.StringProperty, "foo" + i);
+ target.SetValue(TestClass.Struct1Property, new(i + 1));
+ target.SetValue(TestClass.Struct2Property, new(i + 1));
+ target.SetValue(TestClass.Struct3Property, new(i + 1));
+ target.SetValue(TestClass.Struct4Property, new(i + 1));
+ target.SetValue(TestClass.Struct5Property, new(i + 1));
+ target.SetValue(TestClass.Struct6Property, new(i + 1));
+ target.SetValue(TestClass.Struct7Property, new(i + 1));
+ target.SetValue(TestClass.Struct8Property, new(i + 1));
+ }
+
+ sub1.Dispose();
+ sub2.Dispose();
+ sub3.Dispose();
+ sub4.Dispose();
+ sub5.Dispose();
+ sub6.Dispose();
+ sub7.Dispose();
+ sub8.Dispose();
+ sub9.Dispose();
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs
new file mode 100644
index 0000000000..0bfea0fafe
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs
@@ -0,0 +1,181 @@
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_GetValue
+ {
+ private BaselineTestClass _baseline = new(){ StringProperty = "foo" };
+ private TestClass _target = new();
+
+ public AvaloniaObject_GetValue()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [Benchmark(Baseline = true)]
+ public int GetClrPropertyValues()
+ {
+ var target = _baseline;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.StringProperty?.Length ?? 0;
+ result += target.Struct1Property.Int1;
+ result += target.Struct2Property.Int1;
+ result += target.Struct3Property.Int1;
+ result += target.Struct4Property.Int1;
+ result += target.Struct5Property.Int1;
+ result += target.Struct6Property.Int1;
+ result += target.Struct7Property.Int1;
+ result += target.Struct8Property.Int1;
+ }
+
+ return result;
+ }
+
+ [Benchmark]
+ public int GetDefaultValues()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ [GlobalSetup(Target = nameof(Get_Local_Values))]
+ public void SetupLocalValues()
+ {
+ _target.SetValue(TestClass.StringProperty, "foo");
+ _target.SetValue(TestClass.Struct1Property, new(1));
+ _target.SetValue(TestClass.Struct2Property, new(1));
+ _target.SetValue(TestClass.Struct3Property, new(1));
+ _target.SetValue(TestClass.Struct4Property, new(1));
+ _target.SetValue(TestClass.Struct5Property, new(1));
+ _target.SetValue(TestClass.Struct6Property, new(1));
+ _target.SetValue(TestClass.Struct7Property, new(1));
+ _target.SetValue(TestClass.Struct8Property, new(1));
+ }
+
+ [Benchmark]
+ public int Get_Local_Values()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ [GlobalSetup(Target = nameof(Get_Local_Values_With_Style_Values))]
+ public void SetupLocalValuesAndStyleValues()
+ {
+ var target = _target;
+ target.SetValue(TestClass.StringProperty, "foo");
+ target.SetValue(TestClass.Struct1Property, new(1));
+ target.SetValue(TestClass.Struct2Property, new(1));
+ target.SetValue(TestClass.Struct3Property, new(1));
+ target.SetValue(TestClass.Struct4Property, new(1));
+ target.SetValue(TestClass.Struct5Property, new(1));
+ target.SetValue(TestClass.Struct6Property, new(1));
+ target.SetValue(TestClass.Struct7Property, new(1));
+ target.SetValue(TestClass.Struct8Property, new(1));
+ target.SetValue(TestClass.StringProperty, "bar", BindingPriority.Style);
+ target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
+ }
+
+ [Benchmark]
+ public int Get_Local_Values_With_Style_Values()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+
+ private class BaselineTestClass
+ {
+ public string? StringProperty { get; set; }
+ public Struct1 Struct1Property { get; set; }
+ public Struct2 Struct2Property { get; set; }
+ public Struct3 Struct3Property { get; set; }
+ public Struct4 Struct4Property { get; set; }
+ public Struct5 Struct5Property { get; set; }
+ public Struct6 Struct6Property { get; set; }
+ public Struct7 Struct7Property { get; set; }
+ public Struct8 Struct8Property { get; set; }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs
new file mode 100644
index 0000000000..45e129ecd8
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_GetValueInherited
+ {
+ private TestClass _root = null!;
+ private TestClass _target = null!;
+
+ public AvaloniaObject_GetValueInherited()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [Params(1, 2, 10, 50, 100, 200)]
+ public int Depth { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _root = new();
+ _root.SetValue(TestClass.StringProperty, "foo");
+ _root.SetValue(TestClass.Struct1Property, new(1));
+ _root.SetValue(TestClass.Struct2Property, new(1));
+ _root.SetValue(TestClass.Struct3Property, new(1));
+ _root.SetValue(TestClass.Struct4Property, new(1));
+ _root.SetValue(TestClass.Struct5Property, new(1));
+ _root.SetValue(TestClass.Struct6Property, new(1));
+ _root.SetValue(TestClass.Struct7Property, new(1));
+ _root.SetValue(TestClass.Struct8Property, new(1));
+
+ var parent = _root;
+
+ for (var i = 0; i < Depth; ++i)
+ {
+ var c = new TestClass();
+ ((ISetLogicalParent)c).SetParent(parent);
+ parent = c;
+ }
+
+ _target = parent;
+ }
+
+ [Benchmark]
+ public int GetInheritedValues()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ private class TestClass : Control
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String", inherits: true);
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1", inherits: true);
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2", inherits: true);
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3", inherits: true);
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4", inherits: true);
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5", inherits: true);
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6", inherits: true);
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7", inherits: true);
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8", inherits: true);
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_SetValue.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_SetValue.cs
new file mode 100644
index 0000000000..2df265984b
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_SetValue.cs
@@ -0,0 +1,130 @@
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_SetValue
+ {
+ private BaselineTestClass _baseline = new();
+ private TestClass _target = new();
+
+ public AvaloniaObject_SetValue()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [Benchmark(Baseline = true)]
+ public int SetClrPropertyValues()
+ {
+ var target = _baseline;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ target.StringProperty = "foo";
+ target.Struct1Property = new(i + 1);
+ target.Struct2Property = new(i + 1);
+ target.Struct3Property = new(i + 1);
+ target.Struct4Property = new(i + 1);
+ target.Struct5Property = new(i + 1);
+ target.Struct6Property = new(i + 1);
+ target.Struct7Property = new(i + 1);
+ target.Struct8Property = new(i + 1);
+ }
+
+ return result;
+ }
+
+ [Benchmark]
+ public void SetValues()
+ {
+ var target = _target;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ target.SetValue(TestClass.StringProperty, "foo");
+ target.SetValue(TestClass.Struct1Property, new(i + 1));
+ target.SetValue(TestClass.Struct2Property, new(i + 1));
+ target.SetValue(TestClass.Struct3Property, new(i + 1));
+ target.SetValue(TestClass.Struct4Property, new(i + 1));
+ target.SetValue(TestClass.Struct5Property, new(i + 1));
+ target.SetValue(TestClass.Struct6Property, new(i + 1));
+ target.SetValue(TestClass.Struct7Property, new(i + 1));
+ target.SetValue(TestClass.Struct8Property, new(i + 1));
+ }
+ }
+
+ [GlobalSetup(Target = nameof(Set_Local_Values_With_Style_Values))]
+ public void SetupStyleValues()
+ {
+ var target = _target;
+ target.SetValue(TestClass.StringProperty, "foo", BindingPriority.Style);
+ target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
+ }
+
+ [Benchmark]
+ public void Set_Local_Values_With_Style_Values()
+ {
+ var target = _target;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ target.SetValue(TestClass.StringProperty, "foo");
+ target.SetValue(TestClass.Struct1Property, new(i + 1));
+ target.SetValue(TestClass.Struct2Property, new(i + 1));
+ target.SetValue(TestClass.Struct3Property, new(i + 1));
+ target.SetValue(TestClass.Struct4Property, new(i + 1));
+ target.SetValue(TestClass.Struct5Property, new(i + 1));
+ target.SetValue(TestClass.Struct6Property, new(i + 1));
+ target.SetValue(TestClass.Struct7Property, new(i + 1));
+ target.SetValue(TestClass.Struct8Property, new(i + 1));
+ }
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+
+ private class BaselineTestClass
+ {
+ public string? StringProperty { get; set; }
+ public Struct1 Struct1Property { get; set; }
+ public Struct2 Struct2Property { get; set; }
+ public Struct3 Struct3Property { get; set; }
+ public Struct4 Struct4Property { get; set; }
+ public Struct5 Struct5Property { get; set; }
+ public Struct6 Struct6Property { get; set; }
+ public Struct7 Struct7Property { get; set; }
+ public Struct8 Struct8Property { get; set; }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
index 02f9397e2c..e823b29526 100644
--- a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
+++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
@@ -1,14 +1,22 @@
-using BenchmarkDotNet.Attributes;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class DirectPropertyBenchmark
{
+ private DirectClass _target = new();
+
+ public DirectPropertyBenchmark()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(DirectClass).TypeHandle);
+ }
+
[Benchmark(Baseline = true)]
public void SetAndRaiseOriginal()
{
- var obj = new DirectClass();
+ var obj = _target;
for (var i = 0; i < 100; ++i)
{
@@ -19,7 +27,7 @@ namespace Avalonia.Benchmarks.Base
[Benchmark]
public void SetAndRaiseSimple()
{
- var obj = new DirectClass();
+ var obj = _target;
for (var i = 0; i < 100; ++i)
{
diff --git a/tests/Avalonia.Benchmarks/Styling/Style_Activation.cs b/tests/Avalonia.Benchmarks/Styling/Style_Activation.cs
new file mode 100644
index 0000000000..f5878286e7
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Styling/Style_Activation.cs
@@ -0,0 +1,65 @@
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Styling
+{
+ [MemoryDiagnoser]
+ public class Style_Activation
+ {
+ private TestClass _target = null!;
+
+ public Style_Activation()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _target = new TestClass();
+
+ var style = new Style(x => x.OfType().Class("foo"))
+ {
+ Setters = { new Setter(TestClass.StringProperty, "foo") }
+ };
+
+ style.TryAttach(_target, null);
+ }
+
+ [Benchmark]
+ public void Toggle_Style_Activation_Via_Class()
+ {
+ for (var i = 0; i < 100; ++i)
+ {
+ _target.Classes.Add("foo");
+ _target.Classes.Remove("foo");
+ }
+ }
+
+ private class TestClass : Control
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Styling/Style_Apply.cs b/tests/Avalonia.Benchmarks/Styling/Style_Apply.cs
new file mode 100644
index 0000000000..2943eef55a
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Styling/Style_Apply.cs
@@ -0,0 +1,89 @@
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Styling
+{
+ [MemoryDiagnoser]
+ public class Style_Apply
+ {
+ private List