Browse Source

Merge branch 'master' into feature/InlineUIContainer

Benedikt Stebner 3 years ago
parent
commit
ebe13e34ac
50 changed files with 2875 additions and 346 deletions
  1. 29 13
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  2. 0 28
      samples/ControlCatalog.Web/LinkerConfig.xml
  3. 1 1
      samples/ControlCatalog/Pages/CanvasPage.xaml
  4. 3 0
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  5. 19 0
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  6. 6 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  7. 30 1
      src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
  8. 167 1
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  9. 2 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  10. 164 12
      src/Avalonia.Controls/TextBlock.cs
  11. 1 1
      src/Avalonia.Controls/TextBox.cs
  12. 1 1
      src/Avalonia.Input/TouchDevice.cs
  13. 7 7
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  14. 3 3
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  15. 9 9
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  16. 5 5
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  17. 8 8
      src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml
  18. 9 9
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  19. 1 1
      src/Avalonia.Themes.Fluent/Controls/Expander.xaml
  20. 9 9
      src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml
  21. 3 3
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  22. 1 1
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  23. 4 4
      src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml
  24. 3 3
      src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml
  25. 1 1
      src/Avalonia.Themes.Fluent/Controls/Slider.xaml
  26. 3 3
      src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml
  27. 3 3
      src/Avalonia.Themes.Fluent/Controls/TabItem.xaml
  28. 3 3
      src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
  29. 6 6
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  30. 11 11
      src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml
  31. 7 7
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  32. 432 10
      src/Avalonia.Visuals/Media/Color.cs
  33. 476 0
      src/Avalonia.Visuals/Media/HslColor.cs
  34. 84 175
      src/Avalonia.Visuals/Media/HsvColor.cs
  35. 1 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  36. 149 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs
  37. 81 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs
  38. 171 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs
  39. 181 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs
  40. 95 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs
  41. 130 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_SetValue.cs
  42. 11 3
      tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
  43. 65 0
      tests/Avalonia.Benchmarks/Styling/Style_Activation.cs
  44. 89 0
      tests/Avalonia.Benchmarks/Styling/Style_Apply.cs
  45. 74 0
      tests/Avalonia.Benchmarks/Styling/Style_NonActive.cs
  46. 38 0
      tests/Avalonia.Benchmarks/TestBindingObservable.cs
  47. 112 0
      tests/Avalonia.Benchmarks/TestTypes.cs
  48. 1 0
      tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
  49. 42 0
      tests/Avalonia.LeakTests/ControlTests.cs
  50. 124 0
      tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs

+ 29 - 13
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

@@ -1,15 +1,14 @@
 <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
   <PropertyGroup>
     <TargetFramework>net6.0</TargetFramework>
-    <MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
     <Nullable>enable</Nullable>
-    <WasmBuildNative>True</WasmBuildNative>
+    <!--Temporal hack that fixes compilation in VS-->
+    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
+    <EmccTotalMemory>16777216</EmccTotalMemory>
+    <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
+    <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
   </PropertyGroup>
 
-  <ItemGroup>
-    <BlazorLinkerDescriptor Include="LinkerConfig.xml" />
-  </ItemGroup>
-
   <!-- In debug, make builds faster by reducing optimizations -->
   <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
     <WasmNativeStrip>false</WasmNativeStrip>
@@ -23,19 +22,36 @@
     <EmccCompileOptimizationFlag>-O3</EmccCompileOptimizationFlag>
     <EmccLinkOptimizationFlag>-O3</EmccLinkOptimizationFlag>
     <RunAOTCompilation>false</RunAOTCompilation>
+    <DebuggerSupport>false</DebuggerSupport>
+    <EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
+    <EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
+    <EventSourceSupport>false</EventSourceSupport>
+    <HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
+    <InvariantGlobalization>true</InvariantGlobalization>
+    <MetadataUpdaterSupport>false</MetadataUpdaterSupport>
+    <UseNativeHttpHandler>true</UseNativeHttpHandler>
+    <UseSystemResourceKeys>true</UseSystemResourceKeys>
+    <PublishTrimmed>true</PublishTrimmed>
+    <TrimMode>link</TrimMode>
+    <TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0"/>
-    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all"/>
+    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
+    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
   </ItemGroup>
 
-  <Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.targets" />
-  <Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.CompilationTuning.props" />
-
   <ItemGroup>
-    <ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj"/>
-    <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj"/>
+    <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
+    <ProjectReference Include="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.csproj" />
+    <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
   </ItemGroup>
 
+  <Import Project="..\..\build\ReferenceCoreLibraries.props" />
+  <Import Project="..\..\build\BuildTargets.targets" />
+
+  <Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.targets" />
+  <Import Project="..\..\src\Web\Avalonia.Web.Blazor\Avalonia.Web.Blazor.CompilationTuning.props" />
+
 </Project>
+

+ 0 - 28
samples/ControlCatalog.Web/LinkerConfig.xml

@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!--
-  This file specifies which parts of the BCL or Blazor packages must not be
-  stripped by the IL Linker even if they aren't referenced by user code.
--->
-<linker>
-  <assembly fullname="mscorlib">
-    <!--
-      Preserve the methods in WasmRuntime because its methods are called by 
-      JavaScript client-side code to implement timers.
-      Fixes: https://github.com/dotnet/blazor/issues/239
-    -->
-    <type fullname="System.Threading.WasmRuntime"/>
-  </assembly>
-
-  <assembly fullname="System.Core">
-    <!--
-      System.Linq.Expressions* is required by Json.NET and any 
-      expression.Compile caller. The assembly isn't stripped.
-    -->
-    <type fullname="System.Linq.Expressions*"/>
-  </assembly>
-  <!--
-    In this example, the app's entry point assembly is listed. The assembly
-    isn't stripped by the IL Linker.
-  -->
-  <assembly fullname="ControlCatalog" preserve="All" />
-</linker>

+ 1 - 1
samples/ControlCatalog/Pages/CanvasPage.xaml

@@ -14,7 +14,7 @@
           </LinearGradientBrush>
         </Rectangle.OpacityMask>
       </Rectangle>
-      <Rectangle Fill="Green" Stroke="Black" StrokeThickness="2" Width="40" Height="20" Canvas.Left="150" Canvas.Top="10" RadiusX="10" RadiusY="5" />
+      <Rectangle Fill="hsva(240, 83%, 73%, 90%)" Stroke="hsl(5, 85%, 85%)" StrokeThickness="2" Width="40" Height="20" Canvas.Left="150" Canvas.Top="10" RadiusX="10" RadiusY="5" />
       <Ellipse Fill="Green" Width="58" Height="58" Canvas.Left="88" Canvas.Top="100"/>
       <Path Fill="Orange" Data="M 0,0 c 0,0 50,0 50,-50 c 0,0 50,0 50,50 h -50 v 50 l -50,-50 Z" Canvas.Left="30" Canvas.Top="250"/>
       <Path Fill="OrangeRed" Canvas.Left="180" Canvas.Top="250">

+ 3 - 0
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@@ -83,6 +83,9 @@
       <Compile Include="../Avalonia.Visuals/Media/Color.cs">
         <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
       </Compile>
+      <Compile Include="../Avalonia.Visuals/Media/HslColor.cs">
+        <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
+      </Compile>
       <Compile Include="../Avalonia.Visuals/Media/HsvColor.cs">
         <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
       </Compile>

+ 19 - 0
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);
+            }
+        }
 
         /// <summary>
         /// Arranges the content of the <see cref="T:Avalonia.Controls.DataGridRow" />.

+ 6 - 1
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -36,6 +36,11 @@ MembersMustExist : Member 'public Avalonia.AttachedProperty<Avalonia.Media.FontS
 MembersMustExist : Member 'public Avalonia.AttachedProperty<Avalonia.Media.FontWeight> Avalonia.AttachedProperty<Avalonia.Media.FontWeight> Avalonia.Controls.TextBlock.FontWeightProperty' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.AttachedProperty<Avalonia.Media.IBrush> Avalonia.AttachedProperty<Avalonia.Media.IBrush> Avalonia.Controls.TextBlock.ForegroundProperty' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.AttachedProperty<System.Double> Avalonia.AttachedProperty<System.Double> Avalonia.Controls.TextBlock.FontSizeProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Media.TextAlignment> Avalonia.StyledProperty<Avalonia.Media.TextAlignment> Avalonia.Controls.TextBlock.TextAlignmentProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Media.TextTrimming> Avalonia.StyledProperty<Avalonia.Media.TextTrimming> Avalonia.Controls.TextBlock.TextTrimmingProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Media.TextWrapping> Avalonia.StyledProperty<Avalonia.Media.TextWrapping> Avalonia.Controls.TextBlock.TextWrappingProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.StyledProperty<System.Double> Avalonia.StyledProperty<System.Double> Avalonia.Controls.TextBlock.LineHeightProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.StyledProperty<System.Int32> Avalonia.StyledProperty<System.Int32> 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

+ 30 - 1
src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs

@@ -1,7 +1,9 @@
 using System;
 using System.Globalization;
 using System.Linq;
+using Avalonia.Controls.Presenters;
 using Avalonia.Input;
+using Avalonia.Input.GestureRecognizers;
 using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.VisualTree;
@@ -60,6 +62,7 @@ namespace Avalonia.Controls.Primitives
         private Vector _offset;
         private bool _hasInit;
         private bool _suppressUpdateOffset;
+        private ScrollContentPresenter? _parentScroller;
 
         public DateTimePickerPanel()
         {
@@ -255,6 +258,8 @@ namespace Avalonia.Controls.Primitives
                 _suppressUpdateOffset = true;
                 SelectedValue = (int)newSel * Increment + MinimumValue;
                 _suppressUpdateOffset = false;
+
+                System.Diagnostics.Debug.WriteLine($"Offset: {_offset} ItemHeight: {ItemHeight}");
             }
         }
 
@@ -270,7 +275,7 @@ namespace Avalonia.Controls.Primitives
 
         public Size Extent => _extent;
 
-        public Size Viewport => new Size(0, ItemHeight);
+        public Size Viewport => Bounds.Size;
 
         public event EventHandler? ScrollInvalidated;
 
@@ -341,6 +346,20 @@ namespace Avalonia.Controls.Primitives
             return finalSize;
         }
 
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            _parentScroller = this.GetVisualParent() as ScrollContentPresenter;
+            _parentScroller?.AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded);
+        }
+
+        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromVisualTree(e);
+            _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded);
+            _parentScroller = null;
+        }
+
         protected override void OnKeyDown(KeyEventArgs e)
         {
             switch (e.Key)
@@ -554,5 +573,15 @@ namespace Avalonia.Controls.Primitives
         {
             ScrollInvalidated?.Invoke(this, e);
         }
+
+        private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e)
+        {
+            var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight;
+
+            if (snapY != Offset.Y)
+            {
+                Offset = Offset.WithY(snapY);
+            }
+        }
     }
 }

+ 167 - 1
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
         /// </summary>
         public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
             Border.BoxShadowProperty.AddOwner<ContentPresenter>();
-        
+
+        /// <summary>
+        /// Defines the <see cref="Foreground"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<IBrush?> ForegroundProperty =
+            TextElement.ForegroundProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="FontFamily"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
+            TextElement.FontFamilyProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="FontSize"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<double> FontSizeProperty =
+            TextElement.FontSizeProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="FontStyle"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<FontStyle> FontStyleProperty =
+            TextElement.FontStyleProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="FontWeight"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<FontWeight> FontWeightProperty =
+            TextElement.FontWeightProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="FontStretch"/> property.
+        /// </summary>
+        public static readonly AttachedProperty<FontStretch> FontStretchProperty =
+            TextElement.FontStretchProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="TextAlignment"/> property
+        /// </summary>
+        public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty =
+            TextBlock.TextAlignmentProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="TextWrapping"/> property
+        /// </summary>
+        public static readonly AttachedProperty<TextWrapping> TextWrappingProperty =
+            TextBlock.TextWrappingProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="TextTrimming"/> property
+        /// </summary>
+        public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty =
+            TextBlock.TextTrimmingProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="LineHeight"/> property
+        /// </summary>
+        public static readonly AttachedProperty<double> LineHeightProperty =
+            TextBlock.LineHeightProperty.AddOwner<ContentPresenter>();
+
+        /// <summary>
+        /// Defines the <see cref="MaxLines"/> property
+        /// </summary>
+        public static readonly AttachedProperty<int> MaxLinesProperty =
+            TextBlock.MaxLinesProperty.AddOwner<ContentPresenter>();
+                
         /// <summary>
         /// Defines the <see cref="Child"/> property.
         /// </summary>
@@ -159,6 +226,105 @@ namespace Avalonia.Controls.Presenters
             set => SetValue(BoxShadowProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets a brush used to paint the text.
+        /// </summary>
+        public IBrush? Foreground
+        {
+            get => GetValue(ForegroundProperty);
+            set => SetValue(ForegroundProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font family.
+        /// </summary>
+        public FontFamily FontFamily
+        {
+            get => GetValue(FontFamilyProperty);
+            set => SetValue(FontFamilyProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font size.
+        /// </summary>
+        public double FontSize
+        {
+            get => GetValue(FontSizeProperty);
+            set => SetValue(FontSizeProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font style.
+        /// </summary>
+        public FontStyle FontStyle
+        {
+            get => GetValue(FontStyleProperty);
+            set => SetValue(FontStyleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font weight.
+        /// </summary>
+        public FontWeight FontWeight
+        {
+            get => GetValue(FontWeightProperty);
+            set => SetValue(FontWeightProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the font stretch.
+        /// </summary>
+        public FontStretch FontStretch
+        {
+            get => GetValue(FontStretchProperty);
+            set => SetValue(FontStretchProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the text alignment
+        /// </summary>
+        public TextAlignment TextAlignment
+        {
+            get => GetValue(TextAlignmentProperty);
+            set => SetValue(TextAlignmentProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the text wrapping
+        /// </summary>
+        public TextWrapping TextWrapping
+        {
+            get => GetValue(TextWrappingProperty);
+            set => SetValue(TextWrappingProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the text trimming
+        /// </summary>
+        public TextTrimming TextTrimming
+        {
+            get => GetValue(TextTrimmingProperty);
+            set => SetValue(TextTrimmingProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the line height
+        /// </summary>
+        public double LineHeight
+        {
+            get => GetValue(LineHeightProperty);
+            set => SetValue(LineHeightProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the max lines
+        /// </summary>
+        public int MaxLines
+        {
+            get => GetValue(MaxLinesProperty);
+            set => SetValue(MaxLinesProperty, value);
+        }
+
         /// <summary>
         /// Gets the control displayed by the presenter.
         /// </summary>

+ 2 - 2
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -389,7 +389,7 @@ namespace Avalonia.Controls.Presenters
                     {
                         var logicalUnits = delta.Y / logicalScrollItemSize.Y;
                         delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y);
-                        dy = logicalUnits * scrollable!.ScrollSize.Height;
+                        dy = logicalUnits;
                     }
                     else
                         dy = delta.Y;
@@ -407,7 +407,7 @@ namespace Avalonia.Controls.Presenters
                     {
                         var logicalUnits = delta.X / logicalScrollItemSize.X;
                         delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X);
-                        dx = logicalUnits * scrollable!.ScrollSize.Width;
+                        dx = logicalUnits;
                     }
                     else
                         dx = delta.X;

+ 164 - 12
src/Avalonia.Controls/TextBlock.cs

@@ -76,19 +76,21 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="LineHeight"/> property.
         /// </summary>
-        public static readonly StyledProperty<double> LineHeightProperty =
-            AvaloniaProperty.Register<TextBlock, double>(
+        public static readonly AttachedProperty<double> LineHeightProperty =
+            AvaloniaProperty.RegisterAttached<TextBlock, Control, double>(
                 nameof(LineHeight),
                 double.NaN,
-                validate: IsValidLineHeight);
+                validate: IsValidLineHeight,
+                inherits: true);
 
         /// <summary>
         /// Defines the <see cref="MaxLines"/> property.
         /// </summary>
-        public static readonly StyledProperty<int> MaxLinesProperty =
-            AvaloniaProperty.Register<TextBlock, int>(
+        public static readonly AttachedProperty<int> MaxLinesProperty =
+            AvaloniaProperty.RegisterAttached<TextBlock, Control, int>(
                 nameof(MaxLines),
-                validate: IsValidMaxLines);
+                validate: IsValidMaxLines,
+                inherits: true);
 
         /// <summary>
         /// Defines the <see cref="Text"/> property.
@@ -110,20 +112,24 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="TextAlignment"/> property.
         /// </summary>
-        public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
-            AvaloniaProperty.Register<TextBlock, TextAlignment>(nameof(TextAlignment));
+        public static readonly AttachedProperty<TextAlignment> TextAlignmentProperty =
+            AvaloniaProperty.RegisterAttached<TextBlock, Control, TextAlignment>(nameof(TextAlignment), 
+                inherits: true);
 
         /// <summary>
         /// Defines the <see cref="TextWrapping"/> property.
         /// </summary>
-        public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
-            AvaloniaProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping));
+        public static readonly AttachedProperty<TextWrapping> TextWrappingProperty =
+            AvaloniaProperty.RegisterAttached<TextBlock, Control, TextWrapping>(nameof(TextWrapping), 
+                inherits: true);
 
         /// <summary>
         /// Defines the <see cref="TextTrimming"/> property.
         /// </summary>
-        public static readonly StyledProperty<TextTrimming> TextTrimmingProperty =
-            AvaloniaProperty.Register<TextBlock, TextTrimming>(nameof(TextTrimming), defaultValue: TextTrimming.None);
+        public static readonly AttachedProperty<TextTrimming> TextTrimmingProperty =
+            AvaloniaProperty.RegisterAttached<TextBlock, Control, TextTrimming>(nameof(TextTrimming), 
+                defaultValue: TextTrimming.None,
+                inherits: true);
 
         /// <summary>
         /// Defines the <see cref="TextDecorations"/> property.
@@ -358,6 +364,152 @@ namespace Avalonia.Controls
             control.SetValue(BaselineOffsetProperty, value);
         }
 
+        /// <summary>
+        /// Reads the attached property from the given element
+        /// </summary>
+        /// <param name="control">The element to which to read the attached property.</param>
+        public static TextAlignment GetTextAlignment(Control control)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            return control.GetValue(TextAlignmentProperty);
+        }
+
+        /// <summary>
+        /// Writes the attached property BaselineOffset to the given element.
+        /// </summary>
+        /// <param name="control">The element to which to write the attached property.</param>
+        /// <param name="alignment">The property value to set</param>
+        public static void SetTextAlignment(Control control, TextAlignment alignment)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            control.SetValue(TextAlignmentProperty, alignment);
+        }
+
+        /// <summary>
+        /// Reads the attached property from the given element
+        /// </summary>
+        /// <param name="control">The element to which to read the attached property.</param>
+        public static TextWrapping GetTextWrapping(Control control)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            return control.GetValue(TextWrappingProperty);
+        }
+
+        /// <summary>
+        /// Writes the attached property BaselineOffset to the given element.
+        /// </summary>
+        /// <param name="control">The element to which to write the attached property.</param>
+        /// <param name="wrapping">The property value to set</param>
+        public static void SetTextWrapping(Control control, TextWrapping wrapping)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            control.SetValue(TextWrappingProperty, wrapping);
+        }
+
+        /// <summary>
+        /// Reads the attached property from the given element
+        /// </summary>
+        /// <param name="control">The element to which to read the attached property.</param>
+        public static TextTrimming GetTextTrimming(Control control)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            return control.GetValue(TextTrimmingProperty);
+        }
+
+        /// <summary>
+        /// Writes the attached property BaselineOffset to the given element.
+        /// </summary>
+        /// <param name="control">The element to which to write the attached property.</param>
+        /// <param name="trimming">The property value to set</param>
+        public static void SetTextTrimming(Control control, TextTrimming trimming)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            control.SetValue(TextTrimmingProperty, trimming);
+        }
+
+        /// <summary>
+        /// Reads the attached property from the given element
+        /// </summary>
+        /// <param name="control">The element to which to read the attached property.</param>
+        public static double GetLineHeight(Control control)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            return control.GetValue(LineHeightProperty);
+        }
+
+        /// <summary>
+        /// Writes the attached property BaselineOffset to the given element.
+        /// </summary>
+        /// <param name="control">The element to which to write the attached property.</param>
+        /// <param name="height">The property value to set</param>
+        public static void SetLineHeight(Control control, double height)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            control.SetValue(LineHeightProperty, height);
+        }
+
+        /// <summary>
+        /// Reads the attached property from the given element
+        /// </summary>
+        /// <param name="control">The element to which to read the attached property.</param>
+        public static int GetMaxLines(Control control)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            return control.GetValue(MaxLinesProperty);
+        }
+
+        /// <summary>
+        /// Writes the attached property BaselineOffset to the given element.
+        /// </summary>
+        /// <param name="control">The element to which to write the attached property.</param>
+        /// <param name="maxLines">The property value to set</param>
+        public static void SetMaxLines(Control control, int maxLines)
+        {
+            if (control == null)
+            {
+                throw new ArgumentNullException(nameof(control));
+            }
+
+            control.SetValue(MaxLinesProperty, maxLines);
+        }
+
+
         /// <summary>
         /// Renders the <see cref="TextBlock"/> to a drawing context.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls/TextBox.cs

@@ -1057,7 +1057,7 @@ namespace Avalonia.Controls
 
                                 SetTextInternal(editedText);
 
-                                CaretIndex = end;
+                                CaretIndex = start;
                             } 
                         }
                         

+ 1 - 1
src/Avalonia.Input/TouchDevice.cs

@@ -32,7 +32,7 @@ namespace Avalonia.Input
 
         public void ProcessRawEvent(RawInputEventArgs ev)
         {
-            if (_disposed)
+            if (ev.Handled || _disposed)
                 return;
             var args = (RawTouchEventArgs)ev;
             if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))

+ 7 - 7
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@@ -43,37 +43,37 @@
   <Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
   </Style>
 
   <Style Selector="Button:pressed  /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
   </Style>
 
   <Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
   </Style>
 
   <Style Selector="Button.accent /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
     <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForeground}" />
+    <Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
   </Style>
 
   <Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
   </Style>
 
   <Style Selector="Button.accent:pressed  /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
   </Style>
 
   <Style Selector="Button, RepeatButton, ToggleButton, DropDownButton">
@@ -92,6 +92,6 @@
   <Style Selector="Button.accent:disabled /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
   </Style>
 </Styles>

+ 3 - 3
src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml

@@ -61,17 +61,17 @@
             </Style>
             <Style Selector="Button.CalendarHeader:pointerover /template/ ContentPresenter#Text">
               <Setter Property="BorderBrush" Value="{DynamicResource CalendarViewNavigationButtonBorderBrushPointerOver}" />
-              <Setter Property="TextElement.Foreground" Value="{DynamicResource CalendarViewNavigationButtonForegroundPointerOver}"/>
+              <Setter Property="Foreground" Value="{DynamicResource CalendarViewNavigationButtonForegroundPointerOver}"/>
               <!-- Prevent base button template:pointerover from overriding background here -->
               <Setter Property="Background" Value="{DynamicResource CalendarViewNavigationButtonBackground}"/>
             </Style>
             <Style Selector="Button.CalendarHeader:pressed /template/ ContentPresenter#Text">
-              <Setter Property="TextElement.Foreground" Value="{DynamicResource CalendarViewNavigationButtonForegroundPressed}"/>
+              <Setter Property="Foreground" Value="{DynamicResource CalendarViewNavigationButtonForegroundPressed}"/>
               <!-- Prevent base button template:pointerover from overriding background here -->
               <Setter Property="Background" Value="{DynamicResource CalendarViewNavigationButtonBackground}"/>
             </Style>
             <Style Selector="Button.CalendarHeader:disabled /template/ ContentPresenter">
-              <Setter Property="TextElement.Foreground" Value="{DynamicResource CalendarViewWeekDayForegroundDisabled}"/>
+              <Setter Property="Foreground" Value="{DynamicResource CalendarViewWeekDayForegroundDisabled}"/>
             </Style>
           </Border.Styles>
           <!--  To keep calendar from resizing when switching DisplayMode

+ 9 - 9
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@@ -76,7 +76,7 @@
 
   <!-- Unchecked PointerOver State -->
   <Style Selector="CheckBox:pointerover /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
   </Style>
 
   <Style Selector="CheckBox:pointerover /template/ Border#PART_Border">
@@ -95,7 +95,7 @@
 
   <!-- Unchecked Pressed State -->
   <Style Selector="CheckBox:pressed /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
   </Style>
 
   <Style Selector="CheckBox:pressed /template/ Border#PART_Border">
@@ -114,7 +114,7 @@
 
   <!-- Unchecked Disabled state -->
   <Style Selector="CheckBox:disabled /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
   </Style>
 
   <Style Selector="CheckBox:disabled /template/ Border#PART_Border">
@@ -157,7 +157,7 @@
 
   <!-- Checked PointerOver State -->
   <Style Selector="CheckBox:checked:pointerover /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
   </Style>
 
   <Style Selector="CheckBox:checked:pointerover /template/ Border#PART_Border">
@@ -176,7 +176,7 @@
 
   <!-- Checked Pressed State -->
   <Style Selector="CheckBox:checked:pressed /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
   </Style>
 
   <Style Selector="CheckBox:checked:pressed /template/ Border#PART_Border">
@@ -195,7 +195,7 @@
 
   <!-- Checked Disabled State -->
   <Style Selector="CheckBox:checked:disabled /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
   </Style>
 
   <Style Selector="CheckBox:checked:disabled /template/ Border#PART_Border">
@@ -237,7 +237,7 @@
 
   <!-- Indeterminate PointerOver State -->
   <Style Selector="CheckBox:indeterminate:pointerover /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
   </Style>
 
   <Style Selector="CheckBox:indeterminate:pointerover /template/ Border#PART_Border">
@@ -256,7 +256,7 @@
 
   <!-- Indeterminate Pressed State -->
   <Style Selector="CheckBox:indeterminate:pressed /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
   </Style>
 
   <Style Selector="CheckBox:indeterminate:pressed /template/ Border#PART_Border">
@@ -275,7 +275,7 @@
 
   <!-- Indeterminate Disabled State -->
   <Style Selector="CheckBox:indeterminate:disabled /template/ ContentPresenter#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
   </Style>
 
   <Style Selector="CheckBox:indeterminate:disabled /template/ Border#PART_Border">

+ 5 - 5
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@@ -54,7 +54,7 @@
                               Grid.Column="0"
                               Grid.ColumnSpan="2"
                               IsVisible="False"
-                              TextElement.FontWeight="{DynamicResource ComboBoxHeaderThemeFontWeight}"
+                              FontWeight="{DynamicResource ComboBoxHeaderThemeFontWeight}"
                               Margin="{DynamicResource ComboBoxTopHeaderMargin}"
                               VerticalAlignment="Top" />
             <Border x:Name="Background"
@@ -178,11 +178,11 @@
   </Style>
 
   <Style Selector="ComboBox:disabled /template/ ContentPresenter#HeaderContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
   </Style>
 
   <Style Selector="ComboBox:disabled /template/ ContentControl#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
   </Style>
 
   <Style Selector="ComboBox:disabled /template/ TextBlock#PlaceholderTextBlock">
@@ -200,7 +200,7 @@
   </Style>
 
   <Style Selector="ComboBox:focus-visible /template/ ContentControl#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundFocused}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundFocused}" />
   </Style>
 
   <Style Selector="ComboBox:focus-visible /template/ TextBlock#PlaceholderTextBlock">
@@ -213,7 +213,7 @@
 
   <!--  Focus Pressed State  -->
   <Style Selector="ComboBox:focused:pressed /template/ ContentControl#ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundFocusedPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundFocusedPressed}" />
   </Style>
 
   <Style Selector="ComboBox:focused:pressed /template/ TextBlock#PlaceholderTextBlock">

+ 8 - 8
src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml

@@ -25,7 +25,7 @@
     <Setter Property="Template">
       <ControlTemplate>
         <ContentPresenter Name="PART_ContentPresenter"
-                          TextElement.Foreground="{TemplateBinding Foreground}"
+                          Foreground="{TemplateBinding Foreground}"
                           Background="{TemplateBinding Background}"
                           BorderBrush="{TemplateBinding BorderBrush}"
                           BorderThickness="{TemplateBinding BorderThickness}"
@@ -43,48 +43,48 @@
   <Style Selector="ComboBoxItem:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ComboBoxItemBackgroundPointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxItemForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundPointerOver}" />
   </Style>
 
   <!--  Disabled state  -->
   <Style Selector="ComboBoxItem:disabled /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ComboBoxItemBackgroundDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxItemForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundDisabled}" />
   </Style>
 
   <!--  Pressed state  -->
   <Style Selector="ComboBoxItem:pressed /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ComboBoxItemBackgroundPressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxItemForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundPressed}" />
   </Style>
 
   <!--  Selected state  -->
   <Style Selector="ComboBoxItem:selected /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelected}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelected}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxItemForegroundSelected}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelected}" />
   </Style>
 
   <!--  Selected Disabled state  -->
   <Style Selector="ComboBoxItem:selected:disabled /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelectedDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelectedDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedDisabled}" />
   </Style>
 
   <!--  Selected PointerOver state  -->
   <Style Selector="ComboBoxItem:selected:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelectedPointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelectedPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPointerOver}" />
   </Style>
 
   <!--  Selected Pressed state  -->
   <Style Selector="ComboBoxItem:selected:pressed /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ComboBoxItemBackgroundSelectedPressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ComboBoxItemBorderBrushSelectedPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxItemForegroundSelectedPressed}" />
   </Style>
 </Styles>

+ 9 - 9
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@@ -43,7 +43,7 @@
   </Style>
   <Style Selector="ListBoxItem.DateTimePickerItem:selected /template/ ContentPresenter">
     <Setter Property="Background" Value="Transparent" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
   </Style>
   <Style Selector="ListBoxItem.DateTimePickerItem.MonthItem">
     <Setter Property="Padding" Value="{DynamicResource DatePickerFlyoutPresenterMonthPadding}"/>
@@ -70,7 +70,7 @@
                   BorderBrush="{DynamicResource DateTimePickerFlyoutButtonBorderBrush}"
                   BorderThickness="{DynamicResource DateTimeFlyoutButtonBorderThickness}"
                   Content="{TemplateBinding Content}"
-                  TextElement.Foreground="{DynamicResource SystemControlHighlightAltBaseHighBrush}"
+                  Foreground="{DynamicResource SystemControlHighlightAltBaseHighBrush}"
                   ContentTemplate="{TemplateBinding ContentTemplate}"
                   Padding="{TemplateBinding Padding}"
                   HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
@@ -84,13 +84,13 @@
   <Style Selector=":is(Button).DateTimeFlyoutButtonStyle:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource DateTimePickerFlyoutButtonBackgroundPointerOver}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource DateTimePickerFlyoutButtonBorderBrushPointerOver}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource DateTimePickerFlyoutButtonForegroundPointerOver}"/>
+    <Setter Property="Foreground" Value="{DynamicResource DateTimePickerFlyoutButtonForegroundPointerOver}"/>
   </Style>
 
   <Style Selector=":is(Button).DateTimeFlyoutButtonStyle:pressed /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource DateTimePickerFlyoutButtonBackgroundPressed}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource DateTimePickerFlyoutButtonBorderBrushPressed}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource DateTimePickerFlyoutButtonForegroundPressed}"/>
+    <Setter Property="Foreground" Value="{DynamicResource DateTimePickerFlyoutButtonForegroundPressed}"/>
   </Style>
 
 
@@ -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 @@
     </Setter>
   </Style>
   <Style Selector="DatePicker /template/ ContentPresenter#HeaderContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource DatePickerHeaderForeground}"/>
+    <Setter Property="Foreground" Value="{DynamicResource DatePickerHeaderForeground}"/>
   </Style>
   <Style Selector="DatePicker:disabled /template/ Rectangle">
     <Setter Property="Fill" Value="{DynamicResource DatePickerSpacerFillDisabled}"/>
@@ -221,19 +221,19 @@
   <Style Selector="DatePicker /template/ Button#FlyoutButton:pointerover /template/ ContentPresenter">
     <Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushPointerOver}"/>
     <Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundPointerOver}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource DatePickerButtonForegroundPointerOver}"/>
+    <Setter Property="Foreground" Value="{DynamicResource DatePickerButtonForegroundPointerOver}"/>
   </Style>
 
   <Style Selector="DatePicker /template/ Button#FlyoutButton:pressed /template/ ContentPresenter">
     <Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushPressed}"/>
     <Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundPressed}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource DatePickerButtonForegroundPressed}"/>
+    <Setter Property="Foreground" Value="{DynamicResource DatePickerButtonForegroundPressed}"/>
   </Style>
 
   <Style Selector="DatePicker /template/ Button#FlyoutButton:disabled /template/ ContentPresenter">
     <Setter Property="BorderBrush" Value="{DynamicResource DatePickerButtonBorderBrushDisabled}"/>
     <Setter Property="Background" Value="{DynamicResource DatePickerButtonBackgroundDisabled}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource DatePickerButtonForegroundDisabled}"/>
+    <Setter Property="Foreground" Value="{DynamicResource DatePickerButtonForegroundDisabled}"/>
   </Style>
 
   <!-- Changes foreground for watermark text when SelectedDate is null-->

+ 1 - 1
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}" />
             <Border x:Name="ExpandCollapseChevronBorder"
                     Grid.Column="1"
                     Width="32"

+ 9 - 9
src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml

@@ -35,13 +35,13 @@
   </Style>
 
   <Style Selector="ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextBlock.FontWeight" Value="Normal" />
-    <Setter Property="TextBlock.FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
+    <Setter Property="FontWeight" Value="Normal" />
+    <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
   </Style>
 
   <!--  Disabled State  -->
   <Style Selector="ListBoxItem:disabled /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlDisabledBaseMediumLowBrush}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlDisabledBaseMediumLowBrush}" />
   </Style>
 
   <!--  PointerOver State  -->
@@ -49,7 +49,7 @@
     <Setter Property="Background" Value="{DynamicResource SystemControlHighlightListLowBrush}" />
   </Style>
   <Style Selector="ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
   </Style>
 
   <!--  Pressed State  -->
@@ -57,7 +57,7 @@
     <Setter Property="Background" Value="{DynamicResource SystemControlHighlightListMediumBrush}" />
   </Style>
   <Style Selector="ListBoxItem:pressed /template/ ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
   </Style>
 
   <!--  Selected State  -->
@@ -65,7 +65,7 @@
     <Setter Property="Background" Value="{DynamicResource SystemControlHighlightListAccentLowBrush}" />
   </Style>
   <Style Selector="ListBoxItem:selected /template/ ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
   </Style>
 
   <!--  Selected Unfocused State  -->
@@ -73,7 +73,7 @@
     <Setter Property="Background" Value="{DynamicResource SystemControlHighlightListAccentLowBrush}" />
   </Style>
   <Style Selector="ListBoxItem:selected:not(:focus) /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
   </Style>
 
   <!--  Selected PointerOver State  -->
@@ -81,7 +81,7 @@
     <Setter Property="Background" Value="{DynamicResource SystemControlHighlightListAccentMediumBrush}" />
   </Style>
   <Style Selector="ListBoxItem:selected:pointerover /template/ ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
   </Style>
 
   <!--  Selected Pressed State  -->
@@ -89,6 +89,6 @@
     <Setter Property="Background" Value="{DynamicResource SystemControlHighlightListAccentHighBrush}" />
   </Style>
   <Style Selector="ListBoxItem:selected:pressed /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlHighlightAltBaseHighBrush}" />
   </Style>
 </Styles>

+ 3 - 3
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@@ -218,7 +218,7 @@
     <Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundPointerOver}" />
   </Style>
   <Style Selector="MenuItem:selected /template/ ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutItemForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemForegroundPointerOver}" />
   </Style>
   <Style Selector="MenuItem:selected /template/ TextBlock#PART_InputGestureText">
     <Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundPointerOver}" />
@@ -232,7 +232,7 @@
     <Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundPressed}" />
   </Style>
   <Style Selector="MenuItem:pressed /template/ Border#PART_LayoutRoot:pointerover ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutItemForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemForegroundPressed}" />
   </Style>
   <Style Selector="MenuItem:pressed /template/ Border#PART_LayoutRoot:pointerover TextBlock#PART_InputGestureText">
     <Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundPressed}" />
@@ -245,7 +245,7 @@
     <Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundDisabled}" />
   </Style>
   <Style Selector="MenuItem:disabled /template/ ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource MenuFlyoutItemForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemForegroundDisabled}" />
   </Style>
   <Style Selector="MenuItem:disabled /template/ TextBlock#PART_InputGestureText">
     <Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundDisabled}" />

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

@@ -12,7 +12,7 @@
         IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter}}"
         Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">
         <Menu.Styles>
-          <Style x:DataType="NativeMenuItem" Selector="MenuItem">
+          <Style x:CompileBindings="False" Selector="MenuItem">
             <Setter Property="Header" Value="{Binding Header}"/>
             <Setter Property="InputGesture" Value="{Binding Gesture}"/>
             <Setter Property="Items" Value="{Binding Menu.Items}"/>

+ 4 - 4
src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml

@@ -49,7 +49,7 @@
             <ContentPresenter Name="PART_ContentPresenter"
                               Content="{TemplateBinding Content}"
                               ContentTemplate="{TemplateBinding ContentTemplate}"
-                              TextElement.Foreground="{TemplateBinding Foreground}"
+                              Foreground="{TemplateBinding Foreground}"
                               Margin="{TemplateBinding Padding}"
                               RecognizesAccessKey="True"
                               HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
@@ -81,7 +81,7 @@
 
   <!-- PointerOver State -->
   <Style Selector="RadioButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource RadioButtonForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource RadioButtonForegroundPointerOver}" />
   </Style>
 
   <Style Selector="RadioButton:pointerover /template/ Border#RootBorder">
@@ -107,7 +107,7 @@
 
   <!-- Pressed State -->
   <Style Selector="RadioButton:pressed /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource RadioButtonForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource RadioButtonForegroundPressed}" />
   </Style>
 
   <Style Selector="RadioButton:pressed /template/ Border#RootBorder">
@@ -133,7 +133,7 @@
 
   <!-- Disabled State -->
   <Style Selector="RadioButton:disabled /template/ ContentPresenter#PART_ContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource RadioButtonForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource RadioButtonForegroundDisabled}" />
   </Style>
 
   <Style Selector="RadioButton:disabled /template/ Border#RootBorder">

+ 3 - 3
src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml

@@ -46,18 +46,18 @@
   <Style Selector="RepeatButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource RepeatButtonBackgroundPointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource RepeatButtonBorderBrushPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource RepeatButtonForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource RepeatButtonForegroundPointerOver}" />
   </Style>
 
   <Style Selector="RepeatButton:pressed  /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource RepeatButtonBackgroundPressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource RepeatButtonBorderBrushPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource RepeatButtonForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource RepeatButtonForegroundPressed}" />
   </Style>
 
   <Style Selector="RepeatButton:disabled /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource RepeatButtonBackgroundDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource RepeatButtonBorderBrushDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource RepeatButtonForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource RepeatButtonForegroundDisabled}" />
   </Style>
 </Styles>

+ 1 - 1
src/Avalonia.Themes.Fluent/Controls/Slider.xaml

@@ -208,7 +208,7 @@
   <!-- Disabled State -->
 
   <Style Selector="Slider:disabled /template/ ContentPresenter#HeaderContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SliderHeaderForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource SliderHeaderForegroundDisabled}" />
   </Style>
   
   <Style Selector="Slider:disabled /template/ RepeatButton#PART_DecreaseButton">

+ 3 - 3
src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml

@@ -126,7 +126,7 @@
          SplitButton /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter">
     <Setter Property="Border.Background" Value="{DynamicResource SplitButtonBackgroundPointerOver}" />
     <Setter Property="Border.BorderBrush" Value="{DynamicResource SplitButtonBorderBrushPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SplitButtonForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPointerOver}" />
   </Style>
   <Style Selector="SplitButton /template/ Button#PART_SecondaryButton:pointerover PathIcon">
     <Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPointerOver}" />
@@ -137,7 +137,7 @@
          SplitButton /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter">
     <Setter Property="Border.Background" Value="{DynamicResource SplitButtonBackgroundPressed}" />
     <Setter Property="Border.BorderBrush" Value="{DynamicResource SplitButtonBorderBrushPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
   </Style>
   <Style Selector="SplitButton /template/ Button#PART_SecondaryButton:pressed PathIcon">
     <Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
@@ -207,7 +207,7 @@
          SplitButton:checked /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter">
     <Setter Property="Border.Background" Value="{DynamicResource SplitButtonBackgroundCheckedPressed}" />
     <Setter Property="Border.BorderBrush" Value="{DynamicResource SplitButtonBorderBrushCheckedPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />
   </Style>
   <Style Selector="SplitButton:checked /template/ Button#PART_SecondaryButton:pressed PathIcon">
     <Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />

+ 3 - 3
src/Avalonia.Themes.Fluent/Controls/TabItem.xaml

@@ -37,9 +37,9 @@
                               Content="{TemplateBinding Header}"
                               HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                               VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
-                              TextElement.FontFamily="{TemplateBinding FontFamily}"
-                              TextElement.FontSize="{TemplateBinding FontSize}"
-                              TextElement.FontWeight="{TemplateBinding FontWeight}" />
+                              FontFamily="{TemplateBinding FontFamily}"
+                              FontSize="{TemplateBinding FontSize}"
+                              FontWeight="{TemplateBinding FontWeight}" />
             <Border Name="PART_SelectedPipe"
                     Background="{DynamicResource TabItemHeaderSelectedPipeFill}" />
           </Panel>

+ 3 - 3
src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml

@@ -36,9 +36,9 @@
                               Content="{TemplateBinding Content}"
                               HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                               VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
-                              TextElement.FontFamily="{TemplateBinding FontFamily}"
-                              TextElement.FontSize="{TemplateBinding FontSize}"
-                              TextElement.FontWeight="{TemplateBinding FontWeight}" />
+                              FontFamily="{TemplateBinding FontFamily}"
+                              FontSize="{TemplateBinding FontSize}"
+                              FontWeight="{TemplateBinding FontWeight}" />
             <Border Name="PART_SelectedPipe"
                     Background="{DynamicResource TabItemHeaderSelectedPipeFill}" />
           </Panel>

+ 6 - 6
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@@ -51,7 +51,7 @@
                    ContentTemplate="{TemplateBinding HeaderTemplate}"
                    Margin="{DynamicResource TimePickerTopHeaderMargin}"
                    MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
-                   TextElement.Foreground="{DynamicResource TimePickerHeaderForeground}"
+                   Foreground="{DynamicResource TimePickerHeaderForeground}"
                    HorizontalAlignment="Stretch"
                    VerticalAlignment="Top" />
 
@@ -77,7 +77,7 @@
                                   BorderThickness="{TemplateBinding BorderThickness}"
                                   CornerRadius="{TemplateBinding CornerRadius}"
                                   Content="{TemplateBinding Content}"
-                                  TextElement.Foreground="{TemplateBinding Foreground}"
+                                  Foreground="{TemplateBinding Foreground}"
                                   HorizontalContentAlignment="Stretch"
                                   VerticalContentAlignment="Stretch" />
                 </ControlTemplate>
@@ -139,7 +139,7 @@
   </Style>
 
   <Style Selector="TimePicker:disabled /template/ ContentPresenter#HeaderContentPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TimePickerHeaderForegroundDisabled}"/>
+    <Setter Property="Foreground" Value="{DynamicResource TimePickerHeaderForegroundDisabled}"/>
   </Style>
   <Style Selector="TimePicker:disabled /template/ Rectangle">
     <Setter Property="Fill" Value="{DynamicResource TimePickerSpacerFillDisabled}"/>
@@ -148,19 +148,19 @@
   <Style Selector="TimePicker /template/ Button#FlyoutButton:pointerover /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundPointerOver}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushPointerOver}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TimePickerButtonForegroundPointerOver}"/>
+    <Setter Property="Foreground" Value="{DynamicResource TimePickerButtonForegroundPointerOver}"/>
   </Style>
 
   <Style Selector="TimePicker /template/ Button:pressed /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundPressed}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushPressed}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TimePickerButtonForegroundPressed}"/>
+    <Setter Property="Foreground" Value="{DynamicResource TimePickerButtonForegroundPressed}"/>
   </Style>
 
   <Style Selector="TimePicker /template/ Button:disabled /template/ ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource TimePickerButtonBackgroundDisabled}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource TimePickerButtonBorderBrushDisabled}"/>
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TimePickerButtonForegroundDisabled}"/>
+    <Setter Property="Foreground" Value="{DynamicResource TimePickerButtonForegroundDisabled}"/>
   </Style>
 
   <Style Selector="TimePicker:hasnotime /template/ Button#FlyoutButton TextBlock">

+ 11 - 11
src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml

@@ -48,66 +48,66 @@
   <Style Selector="ToggleButton:pointerover /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundPointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundPointerOver}" />
   </Style>
 
   <Style Selector="ToggleButton:pressed  /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundPressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundPressed}" />
   </Style>
 
   <Style Selector="ToggleButton:disabled /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundDisabled}" />
   </Style>
 
   <Style Selector="ToggleButton:checked /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundChecked}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushChecked}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundChecked}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundChecked}" />
   </Style>
 
   <Style Selector="ToggleButton:checked:pointerover /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedPointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPointerOver}" />
   </Style>
 
   <Style Selector="ToggleButton:checked:pressed /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedPressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPressed}" />
   </Style>
 
   <Style Selector="ToggleButton:checked:disabled /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedDisabled}" />
   </Style>
 
   <Style Selector="ToggleButton:indeterminate /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundIndeterminate}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushIndeterminate}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminate}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminate}" />
   </Style>
 
   <Style Selector="ToggleButton:indeterminate:pointerover /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundIndeterminatePointerOver}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushIndeterminatePointerOver}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminatePointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminatePointerOver}" />
   </Style>
 
   <Style Selector="ToggleButton:indeterminate:pressed /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundIndeterminatePressed}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushIndeterminatePressed}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminatePressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminatePressed}" />
   </Style>
 
   <Style Selector="ToggleButton:indeterminate:disabled /template/ ContentPresenter#PART_ContentPresenter">
     <Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundIndeterminateDisabled}" />
     <Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushIndeterminateDisabled}" />
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminateDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource ToggleButtonForegroundIndeterminateDisabled}" />
   </Style>
 </Styles>

+ 7 - 7
src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml

@@ -107,7 +107,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushPointerOver}" />
   </Style>
   <Style Selector="TreeViewItem /template/ Border#PART_LayoutRoot:pointerover > ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TreeViewItemForegroundPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundPointerOver}" />
   </Style>
 
   <!--  Pressed state  -->
@@ -116,7 +116,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushPressed}" />
   </Style>
   <Style Selector="TreeViewItem:pressed /template/ Border#PART_LayoutRoot:pointerover > ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TreeViewItemForegroundPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundPressed}" />
   </Style>
 
   <!--  Disabled state  -->
@@ -125,7 +125,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushDisabled}" />
   </Style>
   <Style Selector="TreeViewItem:disabled /template/ Border#PART_LayoutRoot > ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TreeViewItemForegroundDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundDisabled}" />
   </Style>
 
   <!--  Selected state  -->
@@ -134,7 +134,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushSelected}" />
   </Style>
   <Style Selector="TreeViewItem:selected /template/ Border#PART_LayoutRoot > ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TreeViewItemForegroundSelected}" />
+    <Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundSelected}" />
   </Style>
 
   <!--  Selected PointerOver state  -->
@@ -143,7 +143,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushSelectedPointerOver}" />
   </Style>
   <Style Selector="TreeViewItem:selected /template/ Border#PART_LayoutRoot:pointerover > ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TreeViewItemForegroundSelectedPointerOver}" />
+    <Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundSelectedPointerOver}" />
   </Style>
 
   <!--  Selected Pressed state  -->
@@ -152,7 +152,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushSelectedPressed}" />
   </Style>
   <Style Selector="TreeViewItem:pressed:selected /template/ Border#PART_LayoutRoot:pointerover > ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TreeViewItemForegroundSelectedPressed}" />
+    <Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundSelectedPressed}" />
   </Style>
 
   <!--  Disabled Selected state  -->
@@ -161,7 +161,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource TreeViewItemBorderBrushSelectedDisabled}" />
   </Style>
   <Style Selector="TreeViewItem:disabled:selected /template/ Border#PART_LayoutRoot > ContentPresenter#PART_HeaderPresenter">
-    <Setter Property="TextElement.Foreground" Value="{DynamicResource TreeViewItemForegroundSelectedDisabled}" />
+    <Setter Property="Foreground" Value="{DynamicResource TreeViewItemForegroundSelectedDisabled}" />
   </Style>
 
   <!--  ExpandCollapseChevron Group states  -->

+ 432 - 10
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<Color>
     {
+        private const double byteToDouble = 1.0 / 255;
+
         static Color()
         {
 #if !BUILDTASK
@@ -41,6 +50,13 @@ namespace Avalonia.Media
         /// </summary>
         public byte B { get; }
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Color"/> struct.
+        /// </summary>
+        /// <param name="a">The alpha component.</param>
+        /// <param name="r">The red component.</param>
+        /// <param name="g">The green component.</param>
+        /// <param name="b">The blue component.</param>
         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<char> s, out Color color)
+        private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
         {
             static bool TryParseCore(ReadOnlySpan<char> 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;
+        }
+
         /// <summary>
         /// Returns the string representation of the color.
         /// </summary>
@@ -272,15 +433,24 @@ namespace Avalonia.Media
             return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B;
         }
 
+        /// <summary>
+        /// Returns the HSL color model equivalent of this RGB color.
+        /// </summary>
+        /// <returns>The HSL equivalent color.</returns>
+        public HslColor ToHsl()
+        {
+            // Don't use the HslColor(Color) constructor to avoid an extra HslColor
+            return Color.ToHsl(R, G, B, A);
+        }
+
         /// <summary>
         /// Returns the HSV color model equivalent of this RGB color.
         /// </summary>
         /// <returns>The HSV equivalent color.</returns>
         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);
         }
 
         /// <inheritdoc/>
@@ -289,11 +459,13 @@ namespace Avalonia.Media
             return A == other.A && R == other.R && G == other.G && B == other.B;
         }
 
+        /// <inheritdoc/>
         public override bool Equals(object? obj)
         {
             return obj is Color other && Equals(other);
         }
 
+        /// <inheritdoc/>
         public override int GetHashCode()
         {
             unchecked
@@ -306,11 +478,261 @@ namespace Avalonia.Media
             }
         }
 
+        /// <summary>
+        /// Converts the given RGB color to its HSL color equivalent.
+        /// </summary>
+        /// <param name="color">The color in the RGB color model.</param>
+        /// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
+        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));
+        }
+
+        /// <summary>
+        /// Converts the given RGBA color component values to their HSL color equivalent.
+        /// </summary>
+        /// <param name="red">The Red component in the RGB color model.</param>
+        /// <param name="green">The Green component in the RGB color model.</param>
+        /// <param name="blue">The Blue component in the RGB color model.</param>
+        /// <param name="alpha">The Alpha component.</param>
+        /// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
+        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));
+        }
+
+        /// <summary>
+        /// Converts the given RGBA color component values to their HSL color equivalent.
+        /// </summary>
+        /// <remarks>
+        /// 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.
+        /// </remarks>
+        /// <param name="r">The Red component in the RGB color model within the range 0..1.</param>
+        /// <param name="g">The Green component in the RGB color model within the range 0..1.</param>
+        /// <param name="b">The Blue component in the RGB color model within the range 0..1.</param>
+        /// <param name="a">The Alpha component in the RGB color model within the range 0..1.</param>
+        /// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
+        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);
+        }
+
+        /// <summary>
+        /// Converts the given RGB color to its HSV color equivalent.
+        /// </summary>
+        /// <param name="color">The color in the RGB color model.</param>
+        /// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
+        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));
+        }
+
+        /// <summary>
+        /// Converts the given RGBA color component values to their HSV color equivalent.
+        /// </summary>
+        /// <param name="red">The Red component in the RGB color model.</param>
+        /// <param name="green">The Green component in the RGB color model.</param>
+        /// <param name="blue">The Blue component in the RGB color model.</param>
+        /// <param name="alpha">The Alpha component.</param>
+        /// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
+        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));
+        }
+
+        /// <summary>
+        /// Converts the given RGBA color component values to their HSV color equivalent.
+        /// </summary>
+        /// <remarks>
+        /// 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.
+        /// </remarks>
+        /// <param name="r">The Red component in the RGB color model within the range 0..1.</param>
+        /// <param name="g">The Green component in the RGB color model within the range 0..1.</param>
+        /// <param name="b">The Blue component in the RGB color model within the range 0..1.</param>
+        /// <param name="a">The Alpha component in the RGB color model within the range 0..1.</param>
+        /// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
+        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);
+        }
+
+        /// <summary>
+        /// Indicates whether the values of two specified <see cref="Color"/> objects are equal.
+        /// </summary>
+        /// <param name="left">The first object to compare.</param>
+        /// <param name="right">The second object to compare.</param>
+        /// <returns>True if left and right are equal; otherwise, false.</returns>
         public static bool operator ==(Color left, Color right)
         {
             return left.Equals(right);
         }
 
+        /// <summary>
+        /// Indicates whether the values of two specified <see cref="Color"/> objects are not equal.
+        /// </summary>
+        /// <param name="left">The first object to compare.</param>
+        /// <param name="right">The second object to compare.</param>
+        /// <returns>True if left and right are not equal; otherwise, false.</returns>
         public static bool operator !=(Color left, Color right)
         {
             return !left.Equals(right);

+ 476 - 0
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
+{
+    /// <summary>
+    /// Defines a color using the hue/saturation/lightness (HSL) model.
+    /// </summary>
+#if !BUILDTASK
+    public
+#endif
+    readonly struct HslColor : IEquatable<HslColor>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HslColor"/> struct.
+        /// </summary>
+        /// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
+        /// <param name="hue">The Hue component in the range from 0..360.
+        /// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
+        /// <param name="saturation">The Saturation component in the range from 0..1.</param>
+        /// <param name="lightness">The Lightness component in the range from 0..1.</param>
+        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);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HslColor"/> struct.
+        /// </summary>
+        /// <remarks>
+        /// 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.
+        /// </remarks>
+        /// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
+        /// <param name="hue">The Hue component in the range from 0..360.
+        /// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
+        /// <param name="saturation">The Saturation component in the range from 0..1.</param>
+        /// <param name="lightness">The Lightness component in the range from 0..1.</param>
+        /// <param name="clampValues">Whether to clamp component values to their required ranges.</param>
+        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;
+            }
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="HslColor"/> struct.
+        /// </summary>
+        /// <param name="color">The RGB color to convert to HSL.</param>
+        public HslColor(Color color)
+        {
+            var hsl = Color.ToHsl(color);
+
+            A = hsl.A;
+            H = hsl.H;
+            S = hsl.S;
+            L = hsl.L;
+        }
+
+        /// <summary>
+        /// Gets the Alpha (transparency) component in the range from 0..1.
+        /// </summary>
+        public double A { get; }
+
+        /// <summary>
+        /// Gets the Hue component in the range from 0..360.
+        /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+        /// </summary>
+        public double H { get; }
+
+        /// <summary>
+        /// Gets the Saturation component in the range from 0..1.
+        /// </summary>
+        public double S { get; }
+
+        /// <summary>
+        /// Gets the Lightness component in the range from 0..1.
+        /// </summary>
+        public double L { get; }
+
+        /// <inheritdoc/>
+        public bool Equals(HslColor other)
+        {
+            return other.A == A &&
+                   other.H == H &&
+                   other.S == S &&
+                   other.L == L;
+        }
+
+        /// <inheritdoc/>
+        public override bool Equals(object? obj)
+        {
+            if (obj is HslColor hslColor)
+            {
+                return Equals(hslColor);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Gets a hashcode for this object.
+        /// Hashcode is not guaranteed to be unique.
+        /// </summary>
+        /// <returns>The hashcode for this object.</returns>
+        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;
+            }
+        }
+
+        /// <summary>
+        /// Returns the RGB color model equivalent of this HSL color.
+        /// </summary>
+        /// <returns>The RGB equivalent color.</returns>
+        public Color ToRgb()
+        {
+            // Use the by-component conversion method directly for performance
+            return HslColor.ToRgb(H, S, L, A);
+        }
+
+        /// <inheritdoc/>
+        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();
+        }
+
+        /// <summary>
+        /// Parses an HSL color string.
+        /// </summary>
+        /// <param name="s">The HSL color string to parse.</param>
+        /// <returns>The parsed <see cref="HslColor"/>.</returns>
+        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}'.");
+        }
+
+        /// <summary>
+        /// Parses an HSL color string.
+        /// </summary>
+        /// <param name="s">The HSL color string to parse.</param>
+        /// <param name="hslColor">The parsed <see cref="HslColor"/>.</param>
+        /// <returns>True if parsing was successful; otherwise, false.</returns>
+        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;
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="HslColor"/> from individual color component values.
+        /// </summary>
+        /// <remarks>
+        /// This exists for symmetry with the <see cref="Color"/> struct; however, the
+        /// appropriate constructor should commonly be used instead.
+        /// </remarks>
+        /// <param name="a">The Alpha (transparency) component in the range from 0..1.</param>
+        /// <param name="h">The Hue component in the range from 0..360.</param>
+        /// <param name="s">The Saturation component in the range from 0..1.</param>
+        /// <param name="l">The Lightness component in the range from 0..1.</param>
+        /// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
+        public static HslColor FromAhsl(double a, double h, double s, double l)
+        {
+            return new HslColor(a, h, s, l);
+        }
+
+        /// <summary>
+        /// Creates a new <see cref="HslColor"/> from individual color component values.
+        /// </summary>
+        /// <remarks>
+        /// This exists for symmetry with the <see cref="Color"/> struct; however, the
+        /// appropriate constructor should commonly be used instead.
+        /// </remarks>
+        /// <param name="h">The Hue component in the range from 0..360.</param>
+        /// <param name="s">The Saturation component in the range from 0..1.</param>
+        /// <param name="l">The Lightness component in the range from 0..1.</param>
+        /// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
+        public static HslColor FromHsl(double h, double s, double l)
+        {
+            return new HslColor(1.0, h, s, l);
+        }
+
+        /// <summary>
+        /// Converts the given HSL color to its RGB color equivalent.
+        /// </summary>
+        /// <param name="hslColor">The color in the HSL color model.</param>
+        /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
+        public static Color ToRgb(HslColor hslColor)
+        {
+            return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
+        }
+
+        /// <summary>
+        /// Converts the given HSLA color component values to their RGB color equivalent.
+        /// </summary>
+        /// <param name="hue">The Hue component in the HSL color model in the range from 0..360.</param>
+        /// <param name="saturation">The Saturation component in the HSL color model in the range from 0..1.</param>
+        /// <param name="lightness">The Lightness component in the HSL color model in the range from 0..1.</param>
+        /// <param name="alpha">The Alpha component in the range from 0..1.</param>
+        /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
+        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)));
+        }
+
+        /// <summary>
+        /// Indicates whether the values of two specified <see cref="HslColor"/> objects are equal.
+        /// </summary>
+        /// <param name="left">The first object to compare.</param>
+        /// <param name="right">The second object to compare.</param>
+        /// <returns>True if left and right are equal; otherwise, false.</returns>
+        public static bool operator ==(HslColor left, HslColor right)
+        {
+            return left.Equals(right);
+        }
+
+        /// <summary>
+        /// Indicates whether the values of two specified <see cref="HslColor"/> objects are not equal.
+        /// </summary>
+        /// <param name="left">The first object to compare.</param>
+        /// <param name="right">The second object to compare.</param>
+        /// <returns>True if left and right are not equal; otherwise, false.</returns>
+        public static bool operator !=(HslColor left, HslColor right)
+        {
+            return !(left == right);
+        }
+
+        /// <summary>
+        /// Explicit conversion from an <see cref="HslColor"/> to a <see cref="Color"/>.
+        /// </summary>
+        /// <param name="hslColor">The <see cref="HslColor"/> to convert.</param>
+        public static explicit operator Color(HslColor hslColor)
+        {
+            return hslColor.ToRgb();
+        }
+    }
+}

+ 84 - 175
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
         /// <summary>
         /// Initializes a new instance of the <see cref="HsvColor"/> struct.
         /// </summary>
-        /// <param name="alpha">The Alpha (transparency) channel value in the range from 0..1.</param>
-        /// <param name="hue">The Hue channel value in the range from 0..360.
+        /// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
+        /// <param name="hue">The Hue component in the range from 0..360.
         /// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
-        /// <param name="saturation">The Saturation channel value in the range from 0..1.</param>
-        /// <param name="value">The Value channel value in the range from 0..1.</param>
+        /// <param name="saturation">The Saturation component in the range from 0..1.</param>
+        /// <param name="value">The Value component in the range from 0..1.</param>
         public HsvColor(
             double alpha,
             double hue,
@@ -49,14 +49,14 @@ namespace Avalonia.Media
         /// </summary>
         /// <remarks>
         /// 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.
         /// </remarks>
-        /// <param name="alpha">The Alpha (transparency) channel value in the range from 0..1.</param>
-        /// <param name="hue">The Hue channel value in the range from 0..360.
+        /// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
+        /// <param name="hue">The Hue component in the range from 0..360.
         /// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
-        /// <param name="saturation">The Saturation channel value in the range from 0..1.</param>
-        /// <param name="value">The Value channel value in the range from 0..1.</param>
-        /// <param name="clampValues">Whether to clamp channel values to their required ranges.</param>
+        /// <param name="saturation">The Saturation component in the range from 0..1.</param>
+        /// <param name="value">The Value component in the range from 0..1.</param>
+        /// <param name="clampValues">Whether to clamp component values to their required ranges.</param>
         internal HsvColor(
             double alpha,
             double hue,
@@ -89,7 +89,7 @@ namespace Avalonia.Media
         /// <param name="color">The RGB color to convert to HSV.</param>
         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
         }
 
         /// <summary>
-        /// Gets the Alpha (transparency) channel value in the range from 0..1.
+        /// Gets the Alpha (transparency) component in the range from 0..1.
         /// </summary>
         public double A { get; }
 
         /// <summary>
-        /// 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.
         /// </summary>
         public double H { get; }
 
         /// <summary>
-        /// Gets the Saturation channel value in the range from 0..1.
+        /// Gets the Saturation component in the range from 0..1.
         /// </summary>
         public double S { get; }
 
         /// <summary>
-        /// Gets the Value channel value in the range from 0..1.
+        /// Gets the Value component in the range from 0..1.
         /// </summary>
         public double V { get; }
 
@@ -165,7 +165,7 @@ namespace Avalonia.Media
         /// <returns>The RGB equivalent color.</returns>
         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;
         }
 
         /// <summary>
-        /// Creates a new <see cref="HsvColor"/> from individual color channel values.
+        /// Creates a new <see cref="HsvColor"/> from individual color component values.
         /// </summary>
         /// <remarks>
         /// This exists for symmetry with the <see cref="Color"/> struct; however, the
         /// appropriate constructor should commonly be used instead.
         /// </remarks>
-        /// <param name="a">The Alpha (transparency) channel value in the range from 0..1.</param>
-        /// <param name="h">The Hue channel value in the range from 0..360.</param>
-        /// <param name="s">The Saturation channel value in the range from 0..1.</param>
-        /// <param name="v">The Value channel value in the range from 0..1.</param>
-        /// <returns>A new <see cref="HsvColor"/> built from the individual color channel values.</returns>
+        /// <param name="a">The Alpha (transparency) component in the range from 0..1.</param>
+        /// <param name="h">The Hue component in the range from 0..360.</param>
+        /// <param name="s">The Saturation component in the range from 0..1.</param>
+        /// <param name="v">The Value component in the range from 0..1.</param>
+        /// <returns>A new <see cref="HsvColor"/> built from the individual color component values.</returns>
         public static HsvColor FromAhsv(double a, double h, double s, double v)
         {
             return new HsvColor(a, h, s, v);
         }
 
         /// <summary>
-        /// Converts the given HSV color to it's RGB color equivalent.
+        /// Creates a new <see cref="HsvColor"/> from individual color component values.
+        /// </summary>
+        /// <remarks>
+        /// This exists for symmetry with the <see cref="Color"/> struct; however, the
+        /// appropriate constructor should commonly be used instead.
+        /// </remarks>
+        /// <param name="h">The Hue component in the range from 0..360.</param>
+        /// <param name="s">The Saturation component in the range from 0..1.</param>
+        /// <param name="v">The Value component in the range from 0..1.</param>
+        /// <returns>A new <see cref="HsvColor"/> built from the individual color component values.</returns>
+        public static HsvColor FromHsv(double h, double s, double v)
+        {
+            return new HsvColor(1.0, h, s, v);
+        }
+
+        /// <summary>
+        /// Converts the given HSV color to its RGB color equivalent.
         /// </summary>
         /// <param name="hsvColor">The color in the HSV color model.</param>
         /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSVA values.</returns>
@@ -320,12 +353,12 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        /// 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.
         /// </summary>
-        /// <param name="hue">The hue channel value in the HSV color model in the range from 0..360.</param>
-        /// <param name="saturation">The saturation channel value in the HSV color model in the range from 0..1.</param>
-        /// <param name="value">The value channel value in the HSV color model in the range from 0..1.</param>
-        /// <param name="alpha">The alpha channel value in the range from 0..1.</param>
+        /// <param name="hue">The Hue component in the HSV color model in the range from 0..360.</param>
+        /// <param name="saturation">The Saturation component in the HSV color model in the range from 0..1.</param>
+        /// <param name="value">The Value component in the HSV color model in the range from 0..1.</param>
+        /// <param name="alpha">The Alpha component in the range from 0..1.</param>
         /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSVA values.</returns>
         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));
         }
 
-        /// <summary>
-        /// Converts the given RGB color to it's HSV color equivalent.
-        /// </summary>
-        /// <param name="color">The color in the RGB color model.</param>
-        /// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
-        public static HsvColor FromRgb(Color color)
-        {
-            return HsvColor.FromRgb(color.R, color.G, color.B, color.A);
-        }
-
-        /// <summary>
-        /// Converts the given RGBA color channel values to it's HSV color equivalent.
-        /// </summary>
-        /// <param name="red">The red channel value in the RGB color model.</param>
-        /// <param name="green">The green channel value in the RGB color model.</param>
-        /// <param name="blue">The blue channel value in the RGB color model.</param>
-        /// <param name="alpha">The alpha channel value.</param>
-        /// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
-        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);
-        }
-
         /// <summary>
         /// Indicates whether the values of two specified <see cref="HsvColor"/> objects are equal.
         /// </summary>

+ 1 - 1
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -19,7 +19,7 @@
     <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
   </ItemGroup>
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Properties\" />

+ 149 - 0
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<string?> s_stringSource = new();
+        private static TestBindingObservable<Struct1> s_struct1Source = new();
+        private static TestBindingObservable<Struct2> s_struct2Source = new();
+        private static TestBindingObservable<Struct3> s_struct3Source = new();
+        private static TestBindingObservable<Struct4> s_struct4Source = new();
+        private static TestBindingObservable<Struct5> s_struct5Source = new();
+        private static TestBindingObservable<Struct6> s_struct6Source = new();
+        private static TestBindingObservable<Struct7> s_struct7Source = new();
+        private static TestBindingObservable<Struct8> 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<string?> StringProperty =
+                AvaloniaProperty.Register<TestClass, string?>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
+        }
+    }
+}

+ 81 - 0
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<string> StringProperty =
+                AvaloniaProperty.Register<TestClass, string>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("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; }
+        }
+    }
+}

+ 171 - 0
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<string?>)e;
+                    result += ev.NewValue.Value?.Length ?? 0;
+                }
+                else if (e.Property == TestClass.Struct1Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct1>)e;
+                    result += ev.NewValue.Value.Int1;
+                }
+                else if (e.Property == TestClass.Struct2Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct2>)e;
+                    result += ev.NewValue.Value.Int1;
+                }
+                else if (e.Property == TestClass.Struct3Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct3>)e;
+                    result += ev.NewValue.Value.Int1;
+                }
+                else if (e.Property == TestClass.Struct4Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct4>)e;
+                    result += ev.NewValue.Value.Int1;
+                }
+                else if (e.Property == TestClass.Struct5Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct5>)e;
+                    result += ev.NewValue.Value.Int1;
+                }
+                else if (e.Property == TestClass.Struct6Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct6>)e;
+                    result += ev.NewValue.Value.Int1;
+                }
+                else if (e.Property == TestClass.Struct7Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct7>)e;
+                    result += ev.NewValue.Value.Int1;
+                }
+                else if (e.Property == TestClass.Struct8Property)
+                {
+                    var ev = (AvaloniaPropertyChangedEventArgs<Struct8>)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<string> StringProperty =
+                AvaloniaProperty.Register<TestClass, string>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
+        }
+    }
+}

+ 181 - 0
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<string> StringProperty =
+                AvaloniaProperty.Register<TestClass, string>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("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; }
+        }
+    }
+}

+ 95 - 0
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<string> StringProperty =
+                AvaloniaProperty.Register<TestClass, string>("String", inherits: true);
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1", inherits: true);
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2", inherits: true);
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3", inherits: true);
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4", inherits: true);
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5", inherits: true);
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6", inherits: true);
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7", inherits: true);
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("Struct8", inherits: true);
+        }
+    }
+}

+ 130 - 0
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<string> StringProperty =
+                AvaloniaProperty.Register<TestClass, string>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("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; }
+        }
+    }
+}

+ 11 - 3
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)
             {

+ 65 - 0
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<TestClass>().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<string?> StringProperty =
+                AvaloniaProperty.Register<TestClass, string?>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
+        }
+    }
+}

+ 89 - 0
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<Style> _styles = new();
+
+        public Style_Apply()
+        {
+            RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+        }
+
+        [Params(1, 5, 50)]
+        public int MatchingStyles { get; set; }
+
+
+        [Params(1, 5, 50)]
+        public int NonMatchingStyles { get; set; }
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _styles.Clear();
+
+            for (var i = 0; i < MatchingStyles; ++i)
+            {
+                _styles.Add(new Style(x => x.OfType<TestClass>())
+                {
+                    Setters = { new Setter(TestClass.StringProperty, "foo") }
+                });
+            }
+
+            for (var i = 0; i < NonMatchingStyles; ++i)
+            {
+                _styles.Add(new Style(x => x.OfType<TestClass2>().Class("missing"))
+                {
+                    Setters = { new Setter(TestClass.StringProperty, "foo") }
+                });
+            }
+        }
+
+        [Benchmark]
+        public void Apply_Simple_Styles()
+        {
+            var target = new TestClass();
+
+            target.BeginBatchUpdate();
+
+            foreach (var style in _styles)
+                style.TryAttach(target, null);
+
+            target.EndBatchUpdate();
+        }
+
+        private class TestClass : Control
+        {
+            public static readonly StyledProperty<string?> StringProperty =
+                AvaloniaProperty.Register<TestClass, string?>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
+        }
+
+        private class TestClass2 : Control
+        {
+        }
+    }
+}

+ 74 - 0
tests/Avalonia.Benchmarks/Styling/Style_NonActive.cs

@@ -0,0 +1,74 @@
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Styling
+{
+    [MemoryDiagnoser]
+    public class Style_NonActive
+    {
+        private TestClass _target = null!;
+
+        public Style_NonActive()
+        {
+            RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+        }
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _target = new TestClass();
+
+            var style = new Style(x => x.OfType<TestClass>().Class("foo"))
+            {
+                Setters = { new Setter(TestClass.StringProperty, "foo") }
+            };
+
+            style.TryAttach(_target, null);
+            _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 void Toggle_NonActive_Style_Activation()
+        {
+            for (var i = 0; i < 100; ++i)
+            {
+                _target.Classes.Add("foo");
+                _target.Classes.Remove("foo");
+            }
+        }
+
+        private class TestClass : Control
+        {
+            public static readonly StyledProperty<string?> StringProperty =
+                AvaloniaProperty.Register<TestClass, string?>("String");
+            public static readonly StyledProperty<Struct1> Struct1Property =
+                AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
+            public static readonly StyledProperty<Struct2> Struct2Property =
+                AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
+            public static readonly StyledProperty<Struct3> Struct3Property =
+                AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
+            public static readonly StyledProperty<Struct4> Struct4Property =
+                AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
+            public static readonly StyledProperty<Struct5> Struct5Property =
+                AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
+            public static readonly StyledProperty<Struct6> Struct6Property =
+                AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
+            public static readonly StyledProperty<Struct7> Struct7Property =
+                AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
+            public static readonly StyledProperty<Struct8> Struct8Property =
+                AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
+        }
+    }
+}

+ 38 - 0
tests/Avalonia.Benchmarks/TestBindingObservable.cs

@@ -0,0 +1,38 @@
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.Benchmarks
+{
+    internal class TestBindingObservable<T> : IObservable<BindingValue<T?>>, IDisposable
+    {
+        private T? _value;
+        private IObserver<BindingValue<T?>>? _observer;
+
+        public TestBindingObservable(T? initialValue = default) => _value = initialValue;
+
+        public IDisposable Subscribe(IObserver<BindingValue<T?>> observer)
+        {
+            if (_observer is object)
+                throw new InvalidOperationException("The observable can only be subscribed once.");
+
+            _observer = observer;
+            observer.OnNext(_value);
+            return this;
+        }
+
+        public void Dispose() => _observer = null;
+        public void OnNext(T? value) => _observer?.OnNext(value);
+
+        public void PublishCompleted()
+        {
+            _observer?.OnCompleted();
+            _observer = null;
+        }
+
+        protected void PublishError(Exception error)
+        {
+            _observer?.OnError(error);
+            _observer = null;
+        }
+    }
+}

+ 112 - 0
tests/Avalonia.Benchmarks/TestTypes.cs

@@ -0,0 +1,112 @@
+using System;
+
+namespace Avalonia.Benchmarks
+{
+    internal record struct Struct1
+    {
+        public Struct1(int value)
+        {
+            Int1 = value;
+        }
+
+        public int Int1;
+    }
+
+    internal record struct Struct2
+    {
+        public Struct2(int value)
+        {
+            Int1 = Int2 = value;
+        }
+
+        public int Int1;
+        public int Int2;
+    }
+
+    internal record struct Struct3
+    {
+        public Struct3(int value)
+        {
+            Int1 = Int2 = Int3 = value;
+        }
+
+        public int Int1;
+        public int Int2;
+        public int Int3;
+    }
+
+    internal record struct Struct4
+    {
+        public Struct4(int value)
+        {
+            Int1 = Int2 = Int3 = Int4 = value;
+        }
+
+        public int Int1;
+        public int Int2;
+        public int Int3;
+        public int Int4;
+    }
+
+    internal record struct Struct5
+    {
+        public Struct5(int value)
+        {
+            Int1 = Int2 = Int3 = Int4 = Int5 = value;
+        }
+
+        public int Int1;
+        public int Int2;
+        public int Int3;
+        public int Int4;
+        public int Int5;
+    }
+
+    internal record struct Struct6
+    {
+        public Struct6(int value)
+        {
+            Int1 = Int2 = Int3 = Int4 = Int5 = Int6 = value;
+        }
+
+        public int Int1;
+        public int Int2;
+        public int Int3;
+        public int Int4;
+        public int Int5;
+        public int Int6;
+    }
+
+    internal record struct Struct7
+    {
+        public Struct7(int value)
+        {
+            Int1 = Int2 = Int3 = Int4 = Int5 = Int6 = Int7 = value;
+        }
+
+        public int Int1;
+        public int Int2;
+        public int Int3;
+        public int Int4;
+        public int Int5;
+        public int Int6;
+        public int Int7;
+    }
+
+    internal record struct Struct8
+    {
+        public Struct8(int value)
+        {
+            Int1 = Int2 = Int3 = Int4 = Int5 = Int6 = Int7 = Int8 = value;
+        }
+
+        public int Int1;
+        public int Int2;
+        public int Int3;
+        public int Int4;
+        public int Int5;
+        public int Int6;
+        public int Int7;
+        public int Int8;
+    }
+}

+ 1 - 0
tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

@@ -9,6 +9,7 @@
   <Import Project="..\..\build\NetFX.props" />
   <Import Project="..\..\build\SharedVersion.props" />
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
     <ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />

+ 42 - 0
tests/Avalonia.LeakTests/ControlTests.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
 using System.Runtime.Remoting.Contexts;
 using Avalonia.Controls;
@@ -24,11 +25,52 @@ namespace Avalonia.LeakTests
     [DotMemoryUnit(FailIfRunWithoutSupport = false)]
     public class ControlTests
     {
+        // Need to have the collection as field, so GC will not free it
+        private readonly ObservableCollection<string> _observableCollection = new();
+        
         public ControlTests(ITestOutputHelper atr)
         {
             DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
         }
 
+ 
+        [Fact]
+        public void DataGrid_Is_Freed()
+        {
+            using (Start())
+            {
+                // When attached to INotifyCollectionChanged, DataGrid will subscribe to it's events, potentially causing leak
+                Func<Window> run = () =>
+                {
+                    var window = new Window
+                    {
+                        Content = new DataGrid
+                        {
+                            Items = _observableCollection
+                        }
+                    };
+
+                    window.Show();
+
+                    // Do a layout and make sure that DataGrid gets added to visual tree.
+                    window.LayoutManager.ExecuteInitialLayoutPass();
+                    Assert.IsType<DataGrid>(window.Presenter.Child);
+
+                    // Clear the content and ensure the DataGrid is removed.
+                    window.Content = null;
+                    window.LayoutManager.ExecuteLayoutPass();
+                    Assert.Null(window.Presenter.Child);
+
+                    return window;
+                };
+
+                var result = run();
+
+                dotMemory.Check(memory =>
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<DataGrid>()).ObjectsCount));
+            }
+        }
+
         [Fact]
         public void Canvas_Is_Freed()
         {

+ 124 - 0
tests/Avalonia.Visuals.UnitTests/Media/ColorTests.cs

@@ -199,5 +199,129 @@ namespace Avalonia.Visuals.UnitTests.Media
         {
             Assert.False(Color.TryParse(input, out _));
         }
+
+        [Fact]
+        public void Try_Parse_HslColor()
+        {
+            // Inline data requires constants, so the data is handled internally here
+            var data = new Tuple<string, HslColor>[]
+            {
+                // HSV
+                Tuple.Create("hsl(0, 0, 0)",         new HslColor(1, 0, 0, 0)),
+                Tuple.Create("hsl(0, 0%, 0%)",       new HslColor(1, 0, 0, 0)),
+                Tuple.Create("hsl(180, 0.5, 0.5)",   new HslColor(1, 180, 0.5, 0.5)),
+                Tuple.Create("hsl(180, 50%, 50%)",   new HslColor(1, 180, 0.5, 0.5)),
+                Tuple.Create("hsl(360, 1.0, 1.0)",   new HslColor(1, 0, 1, 1)),         // Wraps Hue to zero
+                Tuple.Create("hsl(360, 100%, 100%)", new HslColor(1, 0, 1, 1)),         // Wraps Hue to zero
+
+                Tuple.Create("hsl(-1000, -1000, -1000)",   new HslColor(1, 0, 0, 0)),   // Clamps to min
+                Tuple.Create("hsl(-1000, -1000%, -1000%)", new HslColor(1, 0, 0, 0)),   // Clamps to min
+                Tuple.Create("hsl(1000, 1000, 1000)",      new HslColor(1, 0, 1, 1)),   // Clamps to max
+                Tuple.Create("hsl(1000, 1000%, 1000%)",    new HslColor(1, 0, 1, 1)),   // Clamps to max
+
+                Tuple.Create("hsl(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)),
+                Tuple.Create("hsl(300, 80%, 20%)", new HslColor(1.0, 300, 0.8, 0.2)),
+
+                // HSVA
+                Tuple.Create("hsla(0, 0, 0, 0)",            new HslColor(0, 0, 0, 0)),
+                Tuple.Create("hsla(0, 0%, 0%, 0%)",         new HslColor(0, 0, 0, 0)),
+                Tuple.Create("hsla(180, 0.5, 0.5, 0.5)",    new HslColor(0.5, 180, 0.5, 0.5)),
+                Tuple.Create("hsla(180, 50%, 50%, 50%)",    new HslColor(0.5, 180, 0.5, 0.5)),
+                Tuple.Create("hsla(360, 1.0, 1.0, 1.0)",    new HslColor(1, 0, 1, 1)),          // Wraps Hue to zero
+                Tuple.Create("hsla(360, 100%, 100%, 100%)", new HslColor(1, 0, 1, 1)),          // Wraps Hue to zero
+
+                Tuple.Create("hsla(-1000, -1000, -1000, -1000)",    new HslColor(0, 0, 0, 0)),  // Clamps to min
+                Tuple.Create("hsla(-1000, -1000%, -1000%, -1000%)", new HslColor(0, 0, 0, 0)),  // Clamps to min
+                Tuple.Create("hsla(1000, 1000, 1000, 1000)",        new HslColor(1, 0, 1, 1)),  // Clamps to max (Hue wraps to zero)
+                Tuple.Create("hsla(1000, 1000%, 1000%, 1000%)",     new HslColor(1, 0, 1, 1)),  // Clamps to max (Hue wraps to zero)
+
+                Tuple.Create("hsla(300, 0.9, 0.2, 0.8)", new HslColor(0.8, 300, 0.9, 0.2)),
+                Tuple.Create("hsla(300, 90%, 20%, 0.8)", new HslColor(0.8, 300, 0.9, 0.2)),
+            };
+
+            foreach (var dataPoint in data)
+            {
+                Assert.True(HslColor.TryParse(dataPoint.Item1, out HslColor parsedHslColor));
+                Assert.True(dataPoint.Item2 == parsedHslColor);
+            }
+        }
+
+        [Fact]
+        public void Try_Parse_HsvColor()
+        {
+            // Inline data requires constants, so the data is handled internally here
+            var data = new Tuple<string, HsvColor>[]
+            {
+                // HSV
+                Tuple.Create("hsv(0, 0, 0)",         new HsvColor(1, 0, 0, 0)),
+                Tuple.Create("hsv(0, 0%, 0%)",       new HsvColor(1, 0, 0, 0)),
+                Tuple.Create("hsv(180, 0.5, 0.5)",   new HsvColor(1, 180, 0.5, 0.5)),
+                Tuple.Create("hsv(180, 50%, 50%)",   new HsvColor(1, 180, 0.5, 0.5)),
+                Tuple.Create("hsv(360, 1.0, 1.0)",   new HsvColor(1, 0, 1, 1)),         // Wraps Hue to zero
+                Tuple.Create("hsv(360, 100%, 100%)", new HsvColor(1, 0, 1, 1)),         // Wraps Hue to zero
+
+                Tuple.Create("hsv(-1000, -1000, -1000)",   new HsvColor(1, 0, 0, 0)),   // Clamps to min
+                Tuple.Create("hsv(-1000, -1000%, -1000%)", new HsvColor(1, 0, 0, 0)),   // Clamps to min
+                Tuple.Create("hsv(1000, 1000, 1000)",      new HsvColor(1, 0, 1, 1)),   // Clamps to max
+                Tuple.Create("hsv(1000, 1000%, 1000%)",    new HsvColor(1, 0, 1, 1)),   // Clamps to max
+
+                Tuple.Create("hsv(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)),
+                Tuple.Create("hsv(300, 80%, 20%)", new HsvColor(1.0, 300, 0.8, 0.2)),
+
+                // HSVA
+                Tuple.Create("hsva(0, 0, 0, 0)",            new HsvColor(0, 0, 0, 0)),
+                Tuple.Create("hsva(0, 0%, 0%, 0%)",         new HsvColor(0, 0, 0, 0)),
+                Tuple.Create("hsva(180, 0.5, 0.5, 0.5)",    new HsvColor(0.5, 180, 0.5, 0.5)),
+                Tuple.Create("hsva(180, 50%, 50%, 50%)",    new HsvColor(0.5, 180, 0.5, 0.5)),
+                Tuple.Create("hsva(360, 1.0, 1.0, 1.0)",    new HsvColor(1, 0, 1, 1)),          // Wraps Hue to zero
+                Tuple.Create("hsva(360, 100%, 100%, 100%)", new HsvColor(1, 0, 1, 1)),          // Wraps Hue to zero
+
+                Tuple.Create("hsva(-1000, -1000, -1000, -1000)",    new HsvColor(0, 0, 0, 0)),  // Clamps to min
+                Tuple.Create("hsva(-1000, -1000%, -1000%, -1000%)", new HsvColor(0, 0, 0, 0)),  // Clamps to min
+                Tuple.Create("hsva(1000, 1000, 1000, 1000)",        new HsvColor(1, 0, 1, 1)),  // Clamps to max (Hue wraps to zero)
+                Tuple.Create("hsva(1000, 1000%, 1000%, 1000%)",     new HsvColor(1, 0, 1, 1)),  // Clamps to max (Hue wraps to zero)
+
+                Tuple.Create("hsva(300, 0.9, 0.2, 0.8)", new HsvColor(0.8, 300, 0.9, 0.2)),
+                Tuple.Create("hsva(300, 90%, 20%, 0.8)", new HsvColor(0.8, 300, 0.9, 0.2)),
+            };
+
+            foreach (var dataPoint in data)
+            {
+                Assert.True(HsvColor.TryParse(dataPoint.Item1, out HsvColor parsedHsvColor));
+                Assert.True(dataPoint.Item2 == parsedHsvColor);
+            }
+        }
+
+        [Fact]
+        public void Try_Parse_All_Formats_With_Conversion()
+        {
+            // Inline data requires constants, so the data is handled internally here
+            var data = new Tuple<string, Color>[]
+            {
+                // RGB
+                Tuple.Create("White",   new Color(0xff, 0xff, 0xff, 0xff)),
+                Tuple.Create("#123456", new Color(0xff, 0x12, 0x34, 0x56)),
+
+                Tuple.Create("rgb(100, 30, 45)",       new Color(255, 100, 30, 45)),
+                Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(229, 100, 30, 45)),
+                Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 100, 30, 45)),
+
+                // HSL
+                Tuple.Create("hsl(296, 85%, 12%)",         new Color(255, 53, 5, 57)),
+                Tuple.Create("hsla(296, 0.85, 0.12, 0.9)", new Color(230, 53, 5, 57)),
+                Tuple.Create("hsla(296, 85%, 12%, 90%)",   new Color(230, 53, 5, 57)),
+
+                // HSV
+                Tuple.Create("hsv(240, 83%, 78%)",         new Color(255, 34, 34, 199)),
+                Tuple.Create("hsva(240, 0.83, 0.78, 0.9)", new Color(230, 34, 34, 199)),
+                Tuple.Create("hsva(240, 83%, 78%, 90%)",   new Color(230, 34, 34, 199)),
+            };
+
+            foreach (var dataPoint in data)
+            {
+                Assert.True(Color.TryParse(dataPoint.Item1, out Color parsedColor));
+                Assert.True(dataPoint.Item2 == parsedColor);
+            }
+        }
     }
 }