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 - false enable - True + + true + 16777216 + false + false - - - - false @@ -23,19 +22,36 @@ -O3 -O3 false + 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/Pages/CanvasPage.xaml b/samples/ControlCatalog/Pages/CanvasPage.xaml index 747df593b3..416164f391 100644 --- a/samples/ControlCatalog/Pages/CanvasPage.xaml +++ b/samples/ControlCatalog/Pages/CanvasPage.xaml @@ -14,7 +14,7 @@ - + diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 5a7daa6d12..9629324c8d 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -83,6 +83,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 5d71a499e3..9dfb34517b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2060,6 +2060,25 @@ namespace Avalonia.Controls forceHorizontalScroll: true); } } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + if (DataConnection.DataSource != null && !DataConnection.EventsWired) + { + DataConnection.WireEvents(DataConnection.DataSource); + } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + // When wired to INotifyCollectionChanged, the DataGrid will be cleaned up by GC + if (DataConnection.DataSource != null && DataConnection.EventsWired) + { + DataConnection.UnWireEvents(DataConnection.DataSource); + } + } /// /// Arranges the content of the . diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 12afe71f77..fe3ac31734 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -36,6 +36,11 @@ MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontWeightProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.ForegroundProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AttachedProperty Avalonia.AttachedProperty Avalonia.Controls.TextBlock.FontSizeProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextAlignmentProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextTrimmingProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.TextWrappingProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.LineHeightProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Controls.TextBlock.MaxLinesProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.FontFamily Avalonia.Controls.TextBlock.GetFontFamily(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Double Avalonia.Controls.TextBlock.GetFontSize(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.FontStyle Avalonia.Controls.TextBlock.GetFontStyle(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract. @@ -89,4 +94,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. -Total Issues: 90 +Total Issues: 95 diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 93acd88fb1..8229f25a07 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -1,5 +1,6 @@ using System; +using Avalonia.Controls.Documents; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -46,7 +47,73 @@ namespace Avalonia.Controls.Presenters /// public static readonly StyledProperty BoxShadowProperty = Border.BoxShadowProperty.AddOwner(); - + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ForegroundProperty = + TextElement.ForegroundProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFamilyProperty = + TextElement.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontSizeProperty = + TextElement.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStyleProperty = + TextElement.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontWeightProperty = + TextElement.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStretchProperty = + TextElement.FontStretchProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextAlignmentProperty = + TextBlock.TextAlignmentProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextWrappingProperty = + TextBlock.TextWrappingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty TextTrimmingProperty = + TextBlock.TextTrimmingProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty LineHeightProperty = + TextBlock.LineHeightProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly AttachedProperty MaxLinesProperty = + TextBlock.MaxLinesProperty.AddOwner(); + /// /// Defines the property. /// @@ -159,6 +226,105 @@ namespace Avalonia.Controls.Presenters set => SetValue(BoxShadowProperty, value); } + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get => GetValue(FontFamilyProperty); + set => SetValue(FontFamilyProperty, value); + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get => GetValue(FontSizeProperty); + set => SetValue(FontSizeProperty, value); + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get => GetValue(FontStyleProperty); + set => SetValue(FontStyleProperty, value); + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get => GetValue(FontWeightProperty); + set => SetValue(FontWeightProperty, value); + } + + /// + /// Gets or sets the font stretch. + /// + public FontStretch FontStretch + { + get => GetValue(FontStretchProperty); + set => SetValue(FontStretchProperty, value); + } + + /// + /// Gets or sets the text alignment + /// + public TextAlignment TextAlignment + { + get => GetValue(TextAlignmentProperty); + set => SetValue(TextAlignmentProperty, value); + } + + /// + /// Gets or sets the text wrapping + /// + public TextWrapping TextWrapping + { + get => GetValue(TextWrappingProperty); + set => SetValue(TextWrappingProperty, value); + } + + /// + /// Gets or sets the text trimming + /// + public TextTrimming TextTrimming + { + get => GetValue(TextTrimmingProperty); + set => SetValue(TextTrimmingProperty, value); + } + + /// + /// Gets or sets the line height + /// + public double LineHeight + { + get => GetValue(LineHeightProperty); + set => SetValue(LineHeightProperty, value); + } + + /// + /// Gets or sets the max lines + /// + public int MaxLines + { + get => GetValue(MaxLinesProperty); + set => SetValue(MaxLinesProperty, value); + } + /// /// Gets the control displayed by the presenter. /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 703b851c79..36e5f7236f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -76,19 +76,21 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty LineHeightProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty LineHeightProperty = + AvaloniaProperty.RegisterAttached( nameof(LineHeight), double.NaN, - validate: IsValidLineHeight); + validate: IsValidLineHeight, + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty MaxLinesProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty MaxLinesProperty = + AvaloniaProperty.RegisterAttached( nameof(MaxLines), - validate: IsValidMaxLines); + validate: IsValidMaxLines, + inherits: true); /// /// Defines the property. @@ -110,20 +112,24 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty TextAlignmentProperty = - AvaloniaProperty.Register(nameof(TextAlignment)); + public static readonly AttachedProperty TextAlignmentProperty = + AvaloniaProperty.RegisterAttached(nameof(TextAlignment), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextWrappingProperty = - AvaloniaProperty.Register(nameof(TextWrapping)); + public static readonly AttachedProperty TextWrappingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextWrapping), + inherits: true); /// /// Defines the property. /// - public static readonly StyledProperty TextTrimmingProperty = - AvaloniaProperty.Register(nameof(TextTrimming), defaultValue: TextTrimming.None); + public static readonly AttachedProperty TextTrimmingProperty = + AvaloniaProperty.RegisterAttached(nameof(TextTrimming), + defaultValue: TextTrimming.None, + inherits: true); /// /// Defines the property. @@ -358,6 +364,152 @@ namespace Avalonia.Controls control.SetValue(BaselineOffsetProperty, value); } + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static TextAlignment GetTextAlignment(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextAlignmentProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextAlignment(Control control, TextAlignment alignment) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextAlignmentProperty, alignment); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static TextWrapping GetTextWrapping(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextWrappingProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextWrapping(Control control, TextWrapping wrapping) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextWrappingProperty, wrapping); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static TextTrimming GetTextTrimming(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(TextTrimmingProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetTextTrimming(Control control, TextTrimming trimming) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(TextTrimmingProperty, trimming); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static double GetLineHeight(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(LineHeightProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetLineHeight(Control control, double height) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(LineHeightProperty, height); + } + + /// + /// Reads the attached property from the given element + /// + /// The element to which to read the attached property. + public static int GetMaxLines(Control control) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + return control.GetValue(MaxLinesProperty); + } + + /// + /// Writes the attached property BaselineOffset to the given element. + /// + /// The element to which to write the attached property. + /// The property value to set + public static void SetMaxLines(Control control, int maxLines) + { + if (control == null) + { + throw new ArgumentNullException(nameof(control)); + } + + control.SetValue(MaxLinesProperty, maxLines); + } + + /// /// Renders the to a drawing context. /// diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 8b53804d68..f545206a2f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -43,37 +43,37 @@ 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/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}" /> @@ -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/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 4a0277b4d4..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,37 +270,80 @@ 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); } /// - /// Converts the given HSV color to it's RGB color equivalent. + /// 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. /// /// The color in the HSV color model. /// A new RGB equivalent to the given HSVA values. @@ -320,12 +353,12 @@ namespace Avalonia.Media } /// - /// Converts the given HSVA color channel values to it's 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,130 +490,6 @@ namespace Avalonia.Media (byte)Math.Round(b * 255)); } - /// - /// Converts the given RGB color to it's 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 it's 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) - { - // 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 - - // Normalize RGBA channel values into the 0..1 range used by this algorithm - double r = red / 255.0; - double g = green / 255.0; - double b = blue / 255.0; - double a = alpha / 255.0; - - 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/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