Browse Source

Merge branch 'master' into colorspectrum

robloo 3 years ago
parent
commit
904af3da26
100 changed files with 3085 additions and 440 deletions
  1. 29 1
      Avalonia.sln
  2. 9 0
      build/DevAnalyzers.props
  3. 10 0
      native/Avalonia.Native/src/OSX/window.mm
  4. 29 13
      samples/ControlCatalog.Web/ControlCatalog.Web.csproj
  5. 0 28
      samples/ControlCatalog.Web/LinkerConfig.xml
  6. 8 0
      samples/ControlCatalog/MainView.xaml
  7. 9 0
      samples/ControlCatalog/MainView.xaml.cs
  8. 31 1
      samples/ControlCatalog/Pages/ButtonsPage.xaml
  9. 1 1
      samples/ControlCatalog/Pages/CanvasPage.xaml
  10. 2 0
      samples/ControlCatalog/Pages/ScreenPage.cs
  11. 1 0
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  12. 1 0
      src/Avalonia.Animation/Avalonia.Animation.csproj
  13. 1 0
      src/Avalonia.Base/Avalonia.Base.csproj
  14. 3 0
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  15. 1 0
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  16. 19 0
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  17. 8 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  18. 1 0
      src/Avalonia.Controls/Avalonia.Controls.csproj
  19. 115 8
      src/Avalonia.Controls/Button.cs
  20. 19 0
      src/Avalonia.Controls/ComboBox.cs
  21. 68 0
      src/Avalonia.Controls/Control.cs
  22. 0 28
      src/Avalonia.Controls/DropDown.cs
  23. 15 0
      src/Avalonia.Controls/DropDownButton.cs
  24. 54 13
      src/Avalonia.Controls/HotkeyManager.cs
  25. 2 0
      src/Avalonia.Controls/Image.cs
  26. 9 1
      src/Avalonia.Controls/MenuItem.cs
  27. 167 1
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  28. 2 0
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  29. 1 1
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  30. 29 1
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  31. 1 1
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  32. 5 0
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  33. 22 10
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  34. 166 12
      src/Avalonia.Controls/TextBlock.cs
  35. 1 1
      src/Avalonia.Controls/TextBox.cs
  36. 6 0
      src/Avalonia.Controls/TopLevel.cs
  37. 1 0
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  38. 1 0
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  39. 1 0
      src/Avalonia.Input/Avalonia.Input.csproj
  40. 18 0
      src/Avalonia.Input/IClickableControl.cs
  41. 4 0
      src/Avalonia.Input/Properties/AssemblyInfo.cs
  42. 1 0
      src/Avalonia.Interactivity/Avalonia.Interactivity.csproj
  43. 1 0
      src/Avalonia.Layout/Avalonia.Layout.csproj
  44. 2 0
      src/Avalonia.Native/Avalonia.Native.csproj
  45. 3 1
      src/Avalonia.OpenGL/Avalonia.OpenGL.csproj
  46. 1 0
      src/Avalonia.Styling/Avalonia.Styling.csproj
  47. 1 0
      src/Avalonia.Themes.Default/Controls/CheckBox.xaml
  48. 67 0
      src/Avalonia.Themes.Default/Controls/DropDownButton.xaml
  49. 4 0
      src/Avalonia.Themes.Default/Controls/SplitButton.xaml
  50. 1 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  51. 13 15
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  52. 3 3
      src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
  53. 10 9
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  54. 6 5
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  55. 8 8
      src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml
  56. 9 9
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  57. 103 0
      src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml
  58. 1 1
      src/Avalonia.Themes.Fluent/Controls/Expander.xaml
  59. 1 0
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  60. 9 9
      src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml
  61. 3 3
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  62. 1 1
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  63. 4 4
      src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml
  64. 3 3
      src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml
  65. 1 1
      src/Avalonia.Themes.Fluent/Controls/Slider.xaml
  66. 7 3
      src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml
  67. 3 3
      src/Avalonia.Themes.Fluent/Controls/TabItem.xaml
  68. 3 3
      src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
  69. 6 6
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  70. 11 11
      src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml
  71. 7 7
      src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
  72. 3 1
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  73. 1 0
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  74. 432 10
      src/Avalonia.Visuals/Media/Color.cs
  75. 476 0
      src/Avalonia.Visuals/Media/HslColor.cs
  76. 83 196
      src/Avalonia.Visuals/Media/HsvColor.cs
  77. 6 0
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  78. 6 0
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  79. 18 1
      src/Avalonia.Visuals/Visual.cs
  80. 6 0
      src/Avalonia.Visuals/VisualExtensions.cs
  81. 5 0
      src/Avalonia.Visuals/VisualTree/IVisual.cs
  82. 2 1
      src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
  83. 1 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  84. 1 0
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  85. 1 1
      src/Shared/ModuleInitializer.cs
  86. 2 1
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  87. 1 0
      src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
  88. 1 0
      src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
  89. 2 1
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  90. 2 0
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  91. 17 0
      src/tools/DevAnalyzers/DevAnalyzers.csproj
  92. 40 0
      src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs
  93. 8 0
      src/tools/DevAnalyzers/GlobalSuppressions.cs
  94. 1 1
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  95. 149 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs
  96. 81 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs
  97. 171 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs
  98. 181 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs
  99. 95 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs
  100. 130 0
      tests/Avalonia.Benchmarks/Base/AvaloniaObject_SetValue.cs

+ 29 - 1
Avalonia.sln

@@ -117,6 +117,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
 		build\Base.props = build\Base.props
 		build\Binding.props = build\Binding.props
 		build\CoreLibraries.props = build\CoreLibraries.props
+		build\DevAnalyzers.props = build\DevAnalyzers.props
 		build\EmbedXaml.props = build\EmbedXaml.props
 		build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
 		build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
@@ -234,7 +235,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport",
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -2238,6 +2241,30 @@ Global
 		{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU
 		{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -2303,6 +2330,7 @@ Global
 		{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 9 - 0
build/DevAnalyzers.props

@@ -0,0 +1,9 @@
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <ProjectReference Include="$(MSBuildThisFileDirectory)..\src\Tools\DevAnalyzers\DevAnalyzers.csproj"
+                      PrivateAssets="all"
+                      ReferenceOutputAssembly="false"
+                      OutputItemType="Analyzer"
+                      SetTargetFramework="TargetFramework=netstandard2.0"/>
+  </ItemGroup>
+</Project>

+ 10 - 0
native/Avalonia.Native/src/OSX/window.mm

@@ -2461,6 +2461,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     
     if(_parent != nullptr)
     {
+        auto cparent = dynamic_cast<WindowImpl*>(_parent.getRaw());
+        
+        if(cparent != nullptr)
+        {
+            if(cparent->WindowState() == Maximized)
+            {
+                cparent->SetWindowState(Normal);
+            }
+        }
+        
         _parent->GetPosition(&position);
         _parent->BaseEvents->PositionChanged(position);
     }

+ 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>

+ 8 - 0
samples/ControlCatalog/MainView.xaml

@@ -193,6 +193,14 @@
                 <WindowTransparencyLevel>Mica</WindowTransparencyLevel>
               </ComboBox.Items>
             </ComboBox>
+            <ComboBox x:Name="FlowDirection"
+                      HorizontalAlignment="Stretch"
+                      SelectedIndex="0">
+              <ComboBox.Items>
+                <FlowDirection>LeftToRight</FlowDirection>
+                <FlowDirection>RightToLeft</FlowDirection>
+              </ComboBox.Items>
+            </ComboBox>
             <ComboBox HorizontalAlignment="Stretch"
                       Items="{Binding WindowStates}"
                       SelectedItem="{Binding WindowState}" />

+ 9 - 0
samples/ControlCatalog/MainView.xaml.cs

@@ -76,6 +76,15 @@ namespace ControlCatalog
                 }
             };
 
+            var flowDirections = this.Find<ComboBox>("FlowDirection");
+            flowDirections.SelectionChanged += (sender, e) =>
+            {
+                if (flowDirections.SelectedItem is FlowDirection flowDirection)
+                {
+                    this.FlowDirection = flowDirection;
+                }
+            };
+
             var decorations = this.Find<ComboBox>("Decorations");
             decorations.SelectionChanged += (sender, e) =>
             {

+ 31 - 1
samples/ControlCatalog/Pages/ButtonsPage.xaml

@@ -147,6 +147,35 @@
       </StackPanel>
     </Border>
 
+    <!-- DropDownButton -->
+    <Border Classes="header-border">
+      <StackPanel Orientation="Vertical"
+                  Spacing="4">
+        <TextBlock Text="DropDownButton"
+                   Classes="header" />
+        <TextBlock TextWrapping="Wrap">A button with an added drop-down chevron to visually indicate it has a flyout with additional actions.</TextBlock>
+      </StackPanel>
+    </Border>
+
+    <Border Classes="thin"
+            Padding="15">
+      <StackPanel Orientation="Vertical"
+                  Spacing="8">
+        <DropDownButton Flyout="{StaticResource SharedMenuFlyout}">
+          <TextBlock Text="Drop Down Button" />
+        </DropDownButton>
+        <DropDownButton Padding="0,0,8,0">
+          <Border Background="Teal"
+                  HorizontalAlignment="Stretch"
+                  VerticalAlignment="Stretch"
+                  Height="32"
+                  Width="32" />
+        </DropDownButton>
+        <DropDownButton IsEnabled="False">Disabled</DropDownButton>
+        <DropDownButton />
+      </StackPanel>
+    </Border>
+
     <!-- SplitButton -->
     <Border Classes="header-border">
       <StackPanel Orientation="Vertical"
@@ -169,7 +198,8 @@
           <TextBlock Text="Disabled" />
         </SplitButton>
         <SplitButton Flyout="{StaticResource SharedMenuFlyout}"
-                     Content="Re-themed">
+                     Content="Re-themed"
+                     Foreground="White">
           <SplitButton.Styles>
             <Style>
               <Style.Resources>

+ 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">

+ 2 - 0
samples/ControlCatalog/Pages/ScreenPage.cs

@@ -13,6 +13,8 @@ namespace ControlCatalog.Pages
     {
         private double _leftMost;
 
+        protected override bool BypassFlowDirectionPolicies => true;
+
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
             base.OnAttachedToVisualTree(e);

+ 1 - 0
src/Android/Avalonia.Android/Avalonia.Android.csproj

@@ -16,4 +16,5 @@
     <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
   </ItemGroup>
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Avalonia.Animation/Avalonia.Animation.csproj

@@ -11,4 +11,5 @@
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Avalonia.Base/Avalonia.Base.csproj

@@ -12,4 +12,5 @@
   <Import Project="..\..\build\System.Memory.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 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>

+ 1 - 0
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@@ -23,4 +23,5 @@
   <Import Project="..\..\build\JetBrains.Annotations.props" />
   <Import Project="..\..\build\BuildTargets.targets" />
   <Import Project="..\..\build\ApiDiff.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 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" />.

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

@@ -1,4 +1,6 @@
 Compat issues with assembly Avalonia.Controls:
+TypesMustExist : Type 'Avalonia.Controls.DropDown' does not exist in the implementation but it does exist in the contract.
+TypesMustExist : Type 'Avalonia.Controls.DropDownItem' does not exist in the implementation but it does exist in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract.
@@ -34,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.
@@ -87,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: 88
+Total Issues: 95

+ 1 - 0
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -19,4 +19,5 @@
   <Import Project="..\..\build\JetBrains.Annotations.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 115 - 8
src/Avalonia.Controls/Button.cs

@@ -29,11 +29,14 @@ namespace Avalonia.Controls
     }
 
     /// <summary>
-    /// A button control.
+    /// A standard button control.
     /// </summary>
-    [PseudoClasses(":pressed")]
-    public class Button : ContentControl, ICommandSource
+    [PseudoClasses(pcFlyoutOpen, pcPressed)]
+    public class Button : ContentControl, ICommandSource, IClickableControl
     {
+        protected const string pcPressed    = ":pressed";
+        protected const string pcFlyoutOpen = ":flyout-open";
+
         /// <summary>
         /// Defines the <see cref="ClickMode"/> property.
         /// </summary>
@@ -92,6 +95,7 @@ namespace Avalonia.Controls
         private ICommand? _command;
         private bool _commandCanExecute = true;
         private KeyGesture? _hotkey;
+        private bool _isFlyoutOpen = false;
 
         /// <summary>
         /// Initializes static members of the <see cref="Button"/> class.
@@ -107,7 +111,6 @@ namespace Avalonia.Controls
         /// </summary>
         public Button()
         {
-            UpdatePseudoClasses(IsPressed);
         }
 
         /// <summary>
@@ -328,11 +331,30 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Opens the button's flyout.
+        /// </summary>
         protected virtual void OpenFlyout()
         {
             Flyout?.ShowAt(this);
         }
 
+        /// <summary>
+        /// Invoked when the button's flyout is opened.
+        /// </summary>
+        protected virtual void OnFlyoutOpened()
+        {
+            // Available for derived types
+        }
+
+        /// <summary>
+        /// Invoked when the button's flyout is closed.
+        /// </summary>
+        protected virtual void OnFlyoutClosed()
+        {
+            // Available for derived types
+        }
+
         /// <inheritdoc/>
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
@@ -382,6 +404,14 @@ namespace Avalonia.Controls
             IsPressed = false;
         }
 
+        /// <inheritdoc/>
+        protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+        {
+            UnregisterFlyoutEvents(Flyout);
+            RegisterFlyoutEvents(Flyout);
+            UpdatePseudoClasses();
+        }
+
         /// <inheritdoc/>
         protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
         {
@@ -442,17 +472,26 @@ namespace Avalonia.Controls
             }
             else if (change.Property == IsPressedProperty)
             {
-                UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
+                UpdatePseudoClasses();
             }
             else if (change.Property == FlyoutProperty)
             {
+                var oldFlyout = change.OldValue.GetValueOrDefault() as FlyoutBase;
+                var newFlyout = change.NewValue.GetValueOrDefault() as FlyoutBase;
+
                 // If flyout is changed while one is already open, make sure we 
                 // close the old one first
-                if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout &&
+                if (oldFlyout != null &&
                     oldFlyout.IsOpen)
                 {
                     oldFlyout.Hide();
                 }
+
+                // Must unregister events here while a reference to the old flyout still exists
+                UnregisterFlyoutEvents(oldFlyout);
+
+                RegisterFlyoutEvents(newFlyout);
+                UpdatePseudoClasses();
             }
         }
 
@@ -493,6 +532,32 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Registers all flyout events.
+        /// </summary>
+        /// <param name="flyout">The flyout to connect events to.</param>
+        private void RegisterFlyoutEvents(FlyoutBase? flyout)
+        {
+            if (flyout != null)
+            {
+                flyout.Opened += Flyout_Opened;
+                flyout.Closed += Flyout_Closed;
+            }
+        }
+
+        /// <summary>
+        /// Explicitly unregisters all flyout events.
+        /// </summary>
+        /// <param name="flyout">The flyout to disconnect events from.</param>
+        private void UnregisterFlyoutEvents(FlyoutBase? flyout)
+        {
+            if (flyout != null)
+            {
+                flyout.Opened -= Flyout_Opened;
+                flyout.Closed -= Flyout_Closed;
+             }
+        }
+
         /// <summary>
         /// Starts listening for the Enter key when the button <see cref="IsDefault"/>.
         /// </summary>
@@ -560,11 +625,53 @@ namespace Avalonia.Controls
         /// <summary>
         /// Updates the visual state of the control by applying latest PseudoClasses.
         /// </summary>
-        private void UpdatePseudoClasses(bool isPressed)
+        private void UpdatePseudoClasses()
         {
-            PseudoClasses.Set(":pressed", isPressed);
+            PseudoClasses.Set(pcFlyoutOpen, _isFlyoutOpen);
+            PseudoClasses.Set(pcPressed, IsPressed);
         }
 
         void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
+
+        void IClickableControl.RaiseClick() => OnClick();
+        
+        /// <summary>
+        /// Event handler for when the button's flyout is opened.
+        /// </summary>
+        private void Flyout_Opened(object? sender, EventArgs e)
+        {
+            var flyout = sender as FlyoutBase;
+
+            // It is possible to share flyouts among multiple controls including Button.
+            // This can cause a problem here since all controls that share a flyout receive
+            // the same Opened/Closed events at the same time.
+            // For Button that means they all would be updating their pseudoclasses accordingly.
+            // In other words, all Buttons with a shared Flyout would have the backgrounds changed together.
+            // To fix this, only continue here if the Flyout target matches this Button instance.
+            if (object.ReferenceEquals(flyout?.Target, this))
+            {
+                _isFlyoutOpen = true;
+                UpdatePseudoClasses();
+
+                OnFlyoutOpened();
+            }
+        }
+
+        /// <summary>
+        /// Event handler for when the button's flyout is closed.
+        /// </summary>
+        private void Flyout_Closed(object? sender, EventArgs e)
+        {
+            var flyout = sender as FlyoutBase;
+
+            // See comments in Flyout_Opened
+            if (object.ReferenceEquals(flyout?.Target, this))
+            {
+                _isFlyoutOpen = false;
+                UpdatePseudoClasses();
+
+                OnFlyoutClosed();
+            }
+        }
     }
 }

+ 19 - 0
src/Avalonia.Controls/ComboBox.cs

@@ -184,6 +184,25 @@ namespace Avalonia.Controls
             this.UpdateSelectionBoxItem(SelectedItem);
         }
 
+        // Because the SelectedItem isn't connected to the visual tree
+        public override void InvalidateMirrorTransform()
+        {
+            base.InvalidateMirrorTransform();
+
+            if (SelectedItem is Control selectedControl)
+            {
+                selectedControl.InvalidateMirrorTransform();
+
+                foreach (var visual in selectedControl.GetVisualDescendants())
+                {
+                    if (visual is Control childControl)
+                    {
+                        childControl.InvalidateMirrorTransform();
+                    }
+                }
+            }
+        }
+
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         {

+ 68 - 0
src/Avalonia.Controls/Control.cs

@@ -160,6 +160,16 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
 
+        /// <summary>
+        /// Gets a value indicating whether control bypass FlowDirecton policies.
+        /// </summary>
+        /// <remarks>
+        /// Related to FlowDirection system and returns false as default, so if 
+        /// <see cref="FlowDirection"/> is RTL then control will get a mirror presentation. 
+        /// For controls that want to avoid this behavior, override this property and return true.
+        /// </remarks>
+        protected virtual bool BypassFlowDirectionPolicies => false;
+
         /// <inheritdoc/>
         void ISetterValue.Initialize(ISetter setter)
         {
@@ -219,6 +229,14 @@ namespace Avalonia.Controls
             base.OnDetachedFromVisualTreeCore(e);
         }
 
+        /// <inheritdoc/>
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+
+            InvalidateMirrorTransform();
+        }
+
         /// <inheritdoc/>
         protected override void OnGotFocus(GotFocusEventArgs e)
         {
@@ -329,5 +347,55 @@ namespace Avalonia.Controls
                 }
             }
         }
+
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == FlowDirectionProperty)
+            {
+                InvalidateMirrorTransform();
+                
+                foreach (var visual in VisualChildren)
+                {
+                    if (visual is Control child)
+                    {
+                        child.InvalidateMirrorTransform();
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Computes the <see cref="IVisual.HasMirrorTransform"/> value according to the 
+        /// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
+        /// </summary>
+        public virtual void InvalidateMirrorTransform()
+        {
+            var flowDirection = this.FlowDirection;
+            var parentFlowDirection = FlowDirection.LeftToRight;
+
+            bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
+            bool parentBypassFlowDirectionPolicies = false;
+
+            var parent = this.FindAncestorOfType<Control>();
+            if (parent != null)
+            {
+                parentFlowDirection = parent.FlowDirection;
+                parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
+            }
+            else if (Parent is Control logicalParent)
+            {
+                parentFlowDirection = logicalParent.FlowDirection;
+                parentBypassFlowDirectionPolicies = logicalParent.BypassFlowDirectionPolicies;
+            }
+
+            bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
+            bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
+
+            bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
+
+            HasMirrorTransform = shouldApplyMirrorTransform;
+        }
     }
 }

+ 0 - 28
src/Avalonia.Controls/DropDown.cs

@@ -1,28 +0,0 @@
-using System;
-using Avalonia.Logging;
-using Avalonia.Styling;
-
-namespace Avalonia.Controls
-{
-    [Obsolete("Use ComboBox")]
-    public class DropDown : ComboBox, IStyleable
-    {
-        public DropDown()
-        {
-            Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDown is deprecated: Use ComboBox");
-        }
-
-        Type IStyleable.StyleKey => typeof(ComboBox);
-    }
-
-    [Obsolete("Use ComboBoxItem")]
-    public class DropDownItem : ComboBoxItem, IStyleable
-    {
-        public DropDownItem()
-        {
-            Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(this, "DropDownItem is deprecated: Use ComboBoxItem");
-        }
-
-        Type IStyleable.StyleKey => typeof(ComboBoxItem);
-    }
-}

+ 15 - 0
src/Avalonia.Controls/DropDownButton.cs

@@ -0,0 +1,15 @@
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// A button with an added drop-down chevron to visually indicate it has a flyout with additional actions.
+    /// </summary>
+    public class DropDownButton : Button
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="DropDownButton"/> class.
+        /// </summary>
+        public DropDownButton()
+        {
+        }
+    }
+}

+ 54 - 13
src/Avalonia.Controls/HotkeyManager.cs

@@ -12,21 +12,61 @@ namespace Avalonia.Controls
 
         class HotkeyCommandWrapper : ICommand
         {
-            public HotkeyCommandWrapper(ICommandSource? control)
+            readonly WeakReference reference;
+
+            public HotkeyCommandWrapper(IControl control)
             {
-                CommandSource = control;
+                reference = new WeakReference(control);
             }
 
-            public readonly ICommandSource? CommandSource;
+            public ICommand? GetCommand()
+            {
+                if (reference.Target is { } target)
+                {
+                    if (target is ICommandSource commandSource && commandSource.Command is { } command)
+                    {
+                        return command;
+                    }
+                    else if (target is IClickableControl { })
+                    {
+                        return this;
+                    }
+                }
+                return null;
+            }
 
-            private ICommand? GetCommand() => CommandSource?.Command;
+            public bool CanExecute(object? parameter)
+            {
+                if (reference.Target is { } target)
+                {
+                    if (target is ICommandSource commandSource && commandSource.Command is { } command)
+                    {
+                        return commandSource.IsEffectivelyEnabled
+                            && command.CanExecute(commandSource.CommandParameter) == true;
+                    }
+                    else if (target is IClickableControl clickable)
+                    {
+                        return clickable.IsEffectivelyEnabled;
+                    }
+                }
+                return false;
+            }
 
-            public bool CanExecute(object? parameter) =>
-                CommandSource?.Command?.CanExecute(CommandSource.CommandParameter) == true
-                && CommandSource.IsEffectivelyEnabled;
+            public void Execute(object? parameter)
+            {
+                if (reference.Target is { } target)
+                {
+                    if (target is ICommandSource commandSource && commandSource.Command is { } command)
+                    {
+                        command.Execute(commandSource.CommandParameter);
+                    }
+                    else if (target is IClickableControl { IsEffectivelyEnabled: true } clickable)
+                    {
+                        clickable.RaiseClick();
+                    }
+                }
+            }
 
-            public void Execute(object? parameter) =>
-                GetCommand()?.Execute(CommandSource?.CommandParameter);
 
 #pragma warning disable 67 // Event not used
             public event EventHandler? CanExecuteChanged;
@@ -47,7 +87,7 @@ namespace Avalonia.Controls
             public Manager(IControl control)
             {
                 _control = control;
-                _wrapper = new HotkeyCommandWrapper(_control as ICommandSource);
+                _wrapper = new HotkeyCommandWrapper(_control);
             }
 
             public void Init()
@@ -104,13 +144,14 @@ namespace Avalonia.Controls
         {
             HotKeyProperty.Changed.Subscribe(args =>
             {
-                if (args.NewValue.Value is null) return;
+                if (args.NewValue.Value is null)
+                    return;
 
                 var control = args.Sender as IControl;
-                if (control is not ICommandSource)
+                if (control is not IClickableControl)
                 {
                     Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control,
-                        $"The element {args.Sender.GetType().Name} does not implement ICommandSource and does not support binding a HotKey ({args.NewValue}).");
+                        $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue}).");
                     return;
                 }
 

+ 2 - 0
src/Avalonia.Controls/Image.cs

@@ -66,6 +66,8 @@ namespace Avalonia.Controls
             set { SetValue(StretchDirectionProperty, value); }
         }
 
+        protected override bool BypassFlowDirectionPolicies => true;
+        
         /// <summary>
         /// Renders the control.
         /// </summary>

+ 9 - 1
src/Avalonia.Controls/MenuItem.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Controls
     /// </summary>
     [TemplatePart("PART_Popup", typeof(Popup))]
     [PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")]
-    public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource
+    public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource, IClickableControl
     {
         /// <summary>
         /// Defines the <see cref="Command"/> property.
@@ -705,6 +705,14 @@ namespace Avalonia.Controls
 
         void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
 
+        void IClickableControl.RaiseClick()
+        {
+            if (IsEffectivelyEnabled)
+            {
+                RaiseEvent(new RoutedEventArgs(ClickEvent));
+            }
+        }
+
         /// <summary>
         /// A dependency resolver which returns a <see cref="MenuItemAccessKeyHandler"/>.
         /// </summary>

+ 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 - 0
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -282,6 +282,8 @@ namespace Avalonia.Controls.Presenters
             }
         }
 
+        protected override bool BypassFlowDirectionPolicies => true;
+
         /// <summary>
         /// Creates the <see cref="TextLayout"/> used to render the text.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@@ -76,7 +76,7 @@ namespace Avalonia.Controls.Primitives
             Rect? rect = null)
         {
             _positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot()!, target, placement, offset, anchor,
-                gravity, constraintAdjustment, rect);
+                gravity, constraintAdjustment, rect, FlowDirection);
             UpdatePosition();
         }
 

+ 29 - 1
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@@ -46,6 +46,7 @@ Copyright © 2019 Nikita Tsukanov
 
 using System;
 using Avalonia.VisualTree;
+using Avalonia.Media;
 
 namespace Avalonia.Controls.Primitives.PopupPositioning
 {
@@ -444,7 +445,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             TopLevel topLevel,
             IVisual target, PlacementMode placement, Point offset,
             PopupAnchor anchor, PopupGravity gravity,
-            PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect)
+            PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect,
+            FlowDirection flowDirection)
         {
             // We need a better way for tracking the last pointer position
 #pragma warning disable CS0618 // Type or member is obsolete
@@ -503,6 +505,32 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
                 else
                     throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
             }
+
+            // Invert coordinate system if FlowDirection is RTL
+            if (flowDirection == FlowDirection.RightToLeft)
+            {
+                if ((positionerParameters.Anchor & PopupAnchor.Right) == PopupAnchor.Right)
+                {
+                    positionerParameters.Anchor ^= PopupAnchor.Right;
+                    positionerParameters.Anchor |= PopupAnchor.Left;
+                }
+                else if ((positionerParameters.Anchor & PopupAnchor.Left) == PopupAnchor.Left)
+                {
+                    positionerParameters.Anchor ^= PopupAnchor.Left;
+                    positionerParameters.Anchor |= PopupAnchor.Right;
+                }
+
+                if ((positionerParameters.Gravity & PopupGravity.Right) == PopupGravity.Right)
+                {
+                    positionerParameters.Gravity ^= PopupGravity.Right;
+                    positionerParameters.Gravity |= PopupGravity.Left;
+                }
+                else if ((positionerParameters.Gravity & PopupGravity.Left) == PopupGravity.Left)
+                {
+                    positionerParameters.Gravity ^= PopupGravity.Left;
+                    positionerParameters.Gravity |= PopupGravity.Right;
+                }
+            }
         }
     }
 

+ 1 - 1
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -93,7 +93,7 @@ namespace Avalonia.Controls.Primitives
             Rect? rect = null)
         {
             _positionerParameters.ConfigurePosition(ParentTopLevel, target,
-                placement, offset, anchor, gravity, constraintAdjustment, rect);
+                placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection);
 
             if (_positionerParameters.Size != default)
                 UpdatePosition();

+ 5 - 0
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -356,6 +356,11 @@ namespace Avalonia.Controls.Primitives
             base.OnDetachedFromLogicalTree(e);
         }
 
+        /// <summary>
+        /// Called when the control's template is applied.
+        /// In simple terms, this means the method is called just before the control is displayed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
         protected virtual void OnApplyTemplate(TemplateAppliedEventArgs e)
         {
         }

+ 22 - 10
src/Avalonia.Controls/SplitButton/SplitButton.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Windows.Input;
 using Avalonia.Controls.Metadata;
-using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Interactivity;
@@ -318,16 +317,9 @@ namespace Avalonia.Controls
                 }
 
                 // Must unregister events here while a reference to the old flyout still exists
-                if (oldFlyout != null)
-                {
-                    UnregisterFlyoutEvents(oldFlyout);
-                }
-
-                if (newFlyout != null)
-                {
-                    RegisterFlyoutEvents(newFlyout);
-                }
+                UnregisterFlyoutEvents(oldFlyout);
 
+                RegisterFlyoutEvents(newFlyout);
                 UpdatePseudoClasses();
             }
 
@@ -419,6 +411,22 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Invoked when the split button's flyout is opened.
+        /// </summary>
+        protected virtual void OnFlyoutOpened()
+        {
+            // Available for derived types
+        }
+
+        /// <summary>
+        /// Invoked when the split button's flyout is closed.
+        /// </summary>
+        protected virtual void OnFlyoutClosed()
+        {
+            // Available for derived types
+        }
+
         ////////////////////////////////////////////////////////////////////////
         // Event Handling
         ////////////////////////////////////////////////////////////////////////
@@ -468,6 +476,8 @@ namespace Avalonia.Controls
             {
                 _isFlyoutOpen = true;
                 UpdatePseudoClasses();
+
+                OnFlyoutOpened();
             }
         }
 
@@ -483,6 +493,8 @@ namespace Avalonia.Controls
             {
                 _isFlyoutOpen = false;
                 UpdatePseudoClasses();
+
+                OnFlyoutClosed();
             }
         }
     }

+ 166 - 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.
@@ -318,6 +324,8 @@ namespace Avalonia.Controls
             set => SetValue(TextDecorationsProperty, value);
         }
         
+        protected override bool BypassFlowDirectionPolicies => true;
+
         /// <summary>
         /// The BaselineOffset property provides an adjustment to baseline offset
         /// </summary>
@@ -356,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;
                             } 
                         }
                         

+ 6 - 0
src/Avalonia.Controls/TopLevel.cs

@@ -350,6 +350,12 @@ namespace Avalonia.Controls
         /// </summary>
         protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this);
 
+        public override void InvalidateMirrorTransform()
+        {
+        }
+        
+        protected override bool BypassFlowDirectionPolicies => true;
+        
         /// <summary>
         /// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>.
         /// </summary>

+ 1 - 0
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@@ -34,4 +34,5 @@
   <Import Project="..\..\build\BuildTargets.targets" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@@ -15,4 +15,5 @@
   </ItemGroup>
 
   <Import Project="..\..\build\ApiDiff.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Avalonia.Input/Avalonia.Input.csproj

@@ -15,4 +15,5 @@
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 18 - 0
src/Avalonia.Input/IClickableControl.cs

@@ -0,0 +1,18 @@
+using System;
+using Avalonia.Interactivity;
+
+namespace Avalonia.Input
+{
+    /// <summary>
+    /// 
+    /// </summary>
+    internal interface IClickableControl
+    {
+        event EventHandler<RoutedEventArgs> Click;
+        void RaiseClick();
+        /// <summary>
+        /// Gets a value indicating whether this control and all its parents are enabled.
+        /// </summary>
+        bool IsEffectivelyEnabled { get; }
+    }
+}

+ 4 - 0
src/Avalonia.Input/Properties/AssemblyInfo.cs

@@ -1,6 +1,10 @@
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.TextInput")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")]
+
+[assembly: InternalsVisibleTo("Avalonia.Controls,  PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests,  PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

+ 1 - 0
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

@@ -11,4 +11,5 @@
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Avalonia.Layout/Avalonia.Layout.csproj

@@ -10,4 +10,5 @@
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 2 - 0
src/Avalonia.Native/Avalonia.Native.csproj

@@ -28,4 +28,6 @@
     <PackageReference Include="MicroCom.CodeGenerator.MSBuild" Version="0.10.4" PrivateAssets="all" />
     <MicroComIdl Include="avn.idl" CSharpInteropPath="Interop.Generated.cs" />
   </ItemGroup>
+
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 3 - 1
src/Avalonia.OpenGL/Avalonia.OpenGL.csproj

@@ -9,5 +9,7 @@
       <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
       <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
       <ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
-    </ItemGroup>    
+    </ItemGroup>
+
+    <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Avalonia.Styling/Avalonia.Styling.csproj

@@ -10,4 +10,5 @@
   </ItemGroup>
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Avalonia.Themes.Default/Controls/CheckBox.xaml

@@ -26,6 +26,7 @@
                     Stretch="Uniform"
                     HorizontalAlignment="Center"
                     VerticalAlignment="Center"
+                    FlowDirection="LeftToRight"
                     Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/>
               <Rectangle Name="indeterminateMark"
                          Fill="{DynamicResource HighlightBrush}"

+ 67 - 0
src/Avalonia.Themes.Default/Controls/DropDownButton.xaml

@@ -0,0 +1,67 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="DropDownButton">
+    <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowBrush}"/>
+    <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
+    <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
+    <Setter Property="HorizontalContentAlignment" Value="Center"/>
+    <Setter Property="VerticalContentAlignment" Value="Center"/>
+    <Setter Property="Padding" Value="4"/>
+    <Setter Property="Template">
+      <Setter.Value>
+        <ControlTemplate>
+          <Border Name="RootBorder"
+                  Background="{TemplateBinding Background}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  BorderThickness="{TemplateBinding BorderThickness}"
+                  CornerRadius="{TemplateBinding CornerRadius}"
+                  ClipToBounds="True">
+            <Grid Name="InnerGrid">
+              <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="*" />
+                <ColumnDefinition Width="Auto" />
+              </Grid.ColumnDefinitions>
+
+              <ContentPresenter Name="PART_ContentPresenter"
+                                Grid.Column="0"
+                                Content="{TemplateBinding Content}"
+                                ContentTemplate="{TemplateBinding ContentTemplate}"
+                                Padding="{TemplateBinding Padding}"
+                                RecognizesAccessKey="True"
+                                HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
+                                VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
+
+              <PathIcon Name="DropDownGlyph"
+                        Grid.Column="1"
+                        UseLayoutRounding="False"
+                        IsHitTestVisible="False"
+                        Height="12"
+                        Width="12"
+                        Margin="0,0,10,0"
+                        HorizontalAlignment="Right"
+                        VerticalAlignment="Center" />
+
+            </Grid>
+          </Border>
+        </ControlTemplate>
+      </Setter.Value>
+    </Setter>
+  </Style>
+
+  <Style Selector="DropDownButton /template/ PathIcon#DropDownGlyph">
+    <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
+    <Setter Property="Data" Value="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z" />
+  </Style>
+
+  <Style Selector="DropDownButton:pointerover /template/ Border#RootBorder">
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
+  </Style>
+
+  <Style Selector="DropDownButton:pressed  /template/ Border#RootBorder">
+    <Setter Property="Background" Value="{DynamicResource ThemeControlHighBrush}"/>
+  </Style>
+
+  <Style Selector="DropDownButton:disabled">
+    <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
+  </Style>
+</Styles>

+ 4 - 0
src/Avalonia.Themes.Default/Controls/SplitButton.xaml

@@ -16,6 +16,7 @@
     <x:Double x:Key="SplitButtonPrimaryButtonSize">24</x:Double>
     <x:Double x:Key="SplitButtonSecondaryButtonSize">24</x:Double>
     <x:Double x:Key="SplitButtonSeparatorWidth">1</x:Double>
+    <x:Double x:Key="SplitButtonMinHeight">24</x:Double>
     <Thickness x:Key="SplitButtonBorderThemeThickness">1</Thickness>
 
     <converters:MarginMultiplierConverter x:Key="PrimaryButtonBorderMultiplier" Left="True" Top="True" Bottom="True" Indent="1" />
@@ -59,8 +60,11 @@
     <Setter Property="Foreground" Value="{DynamicResource SplitButtonForeground}" />
     <Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrush}" />
     <Setter Property="BorderThickness" Value="{DynamicResource SplitButtonBorderThemeThickness}" />
+    <Setter Property="MinHeight" Value="{DynamicResource SplitButtonMinHeight}" />
     <Setter Property="HorizontalAlignment" Value="Left" />
     <Setter Property="VerticalAlignment" Value="Center" />
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Center" />
     <!--<Setter Property="UseSystemFocusVisuals" Value="True" />
     <Setter Property="FocusVisualMargin" Value="-3" />-->
     <Setter Property="KeyboardNavigation.IsTabStop" Value="True" />

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -15,6 +15,7 @@
   <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/ComboBox.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/ComboBoxItem.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/ContentControl.xaml"/>
+  <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/DropDownButton.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/Label.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/GridSplitter.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Default/Controls/ItemsControl.xaml"/>

+ 13 - 15
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@@ -4,7 +4,7 @@
       <StackPanel Spacing="20">
         <Button Content="Click Me!" />
         <Button Classes="accent" Content="Click Me!" />
-      </StackPanel>      
+      </StackPanel>
     </Border>
   </Design.PreviewWith>
   <Styles.Resources>
@@ -19,9 +19,7 @@
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
     <Setter Property="Padding" Value="{DynamicResource ButtonPadding}" />
     <Setter Property="HorizontalAlignment" Value="Left" />
-    <Setter Property="VerticalAlignment" Value="Center" />    
-    <Setter Property="FontWeight" Value="Normal" />
-    <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
+    <Setter Property="VerticalAlignment" Value="Center" />
     <!--<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
     <Setter Property="FocusVisualMargin" Value="-3" />-->
     <Setter Property="Template">
@@ -45,40 +43,40 @@
   <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">
+  <Style Selector="Button, RepeatButton, ToggleButton, DropDownButton">
     <Setter Property="RenderTransform" Value="none" />
     <Setter Property="Transitions">
       <Transitions>
@@ -87,13 +85,13 @@
     </Setter>
   </Style>
 
-  <Style Selector="Button:pressed, RepeatButton:pressed, ToggleButton:pressed">
+  <Style Selector="Button:pressed, RepeatButton:pressed, ToggleButton:pressed, DropDownButton:pressed">
     <Setter Property="RenderTransform" Value="scale(0.98)" />    
   </Style>
 
   <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

+ 10 - 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">
@@ -152,11 +152,12 @@
     <Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
     <Setter Property="Width" Value="9" />
     <Setter Property="Opacity" Value="1" />
+    <Setter Property="FlowDirection" Value="LeftToRight" />
   </Style>
 
   <!-- 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">
@@ -175,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">
@@ -194,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">
@@ -236,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">
@@ -255,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">
@@ -274,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">

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

@@ -37,6 +37,7 @@
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
     <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
     <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="MinHeight" Value="{DynamicResource ComboBoxMinHeight}" />
     <Setter Property="HorizontalContentAlignment" Value="Stretch" />
     <Setter Property="VerticalContentAlignment" Value="Center" />
     <Setter Property="HorizontalAlignment" Value="Left" />
@@ -53,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"
@@ -177,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">
@@ -199,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">
@@ -212,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-->

+ 103 - 0
src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml

@@ -0,0 +1,103 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        x:CompileBindings="True">
+
+  <Design.PreviewWith>
+    <Border Padding="20">
+      <StackPanel Spacing="20">
+        <DropDownButton Content="Click Me!" />
+        <DropDownButton Content="Disabled" IsEnabled="False" />
+      </StackPanel>
+    </Border>
+  </Design.PreviewWith>
+
+  <Styles.Resources>
+    <x:Double x:Key="DropDownButtonMinHeight">32</x:Double>
+  </Styles.Resources>
+  
+  <Style Selector="DropDownButton">
+    <Setter Property="Background" Value="{DynamicResource ButtonBackground}" />
+    <Setter Property="Foreground" Value="{DynamicResource ButtonForeground}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrush}" />
+    <Setter Property="BorderThickness" Value="{DynamicResource ButtonBorderThemeThickness}" />
+    <Setter Property="Padding" Value="{DynamicResource ButtonPadding}" />
+    <Setter Property="MinHeight" Value="{DynamicResource DropDownButtonMinHeight}" />
+    <Setter Property="HorizontalAlignment" Value="Left" />
+    <Setter Property="VerticalAlignment" Value="Center" />
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Center" />
+    <!--<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
+    <Setter Property="FocusVisualMargin" Value="-3" />-->
+    <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+    <Setter Property="Template">
+      <Setter.Value>
+        <ControlTemplate>
+          <Border x:Name="RootBorder"
+                  Background="{TemplateBinding Background}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  BorderThickness="{TemplateBinding BorderThickness}"
+                  CornerRadius="{TemplateBinding CornerRadius}"
+                  ClipToBounds="True">
+            <Grid x:Name="InnerGrid">
+              <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="*" />
+                <ColumnDefinition Width="Auto" />
+              </Grid.ColumnDefinitions>
+
+              <ContentPresenter x:Name="PART_ContentPresenter"
+                                Grid.Column="0"
+                                Content="{TemplateBinding Content}"
+                                ContentTemplate="{TemplateBinding ContentTemplate}"
+                                Padding="{TemplateBinding Padding}"
+                                RecognizesAccessKey="True"
+                                HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
+                                VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
+
+              <PathIcon x:Name="DropDownGlyph"
+                        Grid.Column="1"
+                        UseLayoutRounding="False"
+                        IsHitTestVisible="False"
+                        Height="12"
+                        Width="12"
+                        Margin="0,0,10,0"
+                        HorizontalAlignment="Right"
+                        VerticalAlignment="Center" />
+
+            </Grid>
+          </Border>
+        </ControlTemplate>
+      </Setter.Value>
+    </Setter>
+  </Style>
+
+  <!--  Normal State  -->
+  <Style Selector="DropDownButton /template/ PathIcon#DropDownGlyph">
+    <Setter Property="Foreground" Value="{DynamicResource ComboBoxDropDownGlyphForeground}" />
+    <Setter Property="Data" Value="M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z" />
+  </Style>
+
+  <!--  PointerOver State  -->
+  <Style Selector="DropDownButton:pointerover /template/ Border#RootBorder">
+    <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
+    <Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
+  </Style>
+
+  <!--  Pressed State  -->
+  <Style Selector="DropDownButton:pressed  /template/ Border#RootBorder">
+    <Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
+    <Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
+  </Style>
+
+  <!--  Disabled State  -->
+  <Style Selector="DropDownButton:disabled /template/ Border#RootBorder">
+    <Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
+    <Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
+  </Style>
+  <Style Selector="DropDownButton:disabled /template/ PathIcon#DropDownGlyph">
+    <Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
+  </Style>
+
+</Styles>

+ 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"

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@@ -15,6 +15,7 @@
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/ComboBox.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/ContentControl.xaml"/>
+  <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/DropDownButton.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/Label.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/GridSplitter.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/ItemsControl.xaml"/>

+ 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">

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

@@ -24,6 +24,7 @@
     <x:Double x:Key="SplitButtonPrimaryButtonSize">32</x:Double>
     <x:Double x:Key="SplitButtonSecondaryButtonSize">32</x:Double>
     <x:Double x:Key="SplitButtonSeparatorWidth">1</x:Double>
+    <x:Double x:Key="SplitButtonMinHeight">32</x:Double>
 
     <converters:MarginMultiplierConverter x:Key="PrimaryButtonBorderMultiplier" Left="True" Top="True" Bottom="True" Indent="1" />
     <converters:MarginMultiplierConverter x:Key="SecondaryButtonBorderMultiplier" Right="True" Top="True" Bottom="True" Indent="1" />
@@ -35,8 +36,11 @@
     <Setter Property="Foreground" Value="{DynamicResource SplitButtonForeground}" />
     <Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrush}" />
     <Setter Property="BorderThickness" Value="{DynamicResource SplitButtonBorderThemeThickness}" />
+    <Setter Property="MinHeight" Value="{DynamicResource SplitButtonMinHeight}" />
     <Setter Property="HorizontalAlignment" Value="Left" />
     <Setter Property="VerticalAlignment" Value="Center" />
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Center" />
     <!--<Setter Property="UseSystemFocusVisuals" Value="True" />
     <Setter Property="FocusVisualMargin" Value="-3" />-->
     <Setter Property="KeyboardNavigation.IsTabStop" Value="True" />
@@ -122,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}" />
@@ -133,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}" />
@@ -203,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  -->

+ 3 - 1
src/Avalonia.Visuals/ApiCompatBaseline.txt

@@ -182,5 +182,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFo
 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' is present in the contract but not in the implementation.
 MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable<System.Int32>)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform.get()' is present in the implementation but not in the contract.
 MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice<T>..ctor(System.ReadOnlyMemory<T>, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 184
+Total Issues: 186

+ 1 - 0
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@@ -19,4 +19,5 @@
   <Import Project="..\..\build\System.Memory.props" />
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
+  <Import Project="..\..\build\DevAnalyzers.props" />
 </Project>

+ 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();
+        }
+    }
+}

+ 83 - 196
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,35 +270,78 @@ namespace Avalonia.Media
             else if (components.Length == 4) // HSVA
             {
                 if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
-                    double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
-                    double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value) &&
-                    double.TryParse(components[3], NumberStyles.Number, CultureInfo.InvariantCulture, out double alpha))
+                    TryInternalParse(components[1], out double saturation) &&
+                    TryInternalParse(components[2], out double value) &&
+                    TryInternalParse(components[3], out double alpha))
                 {
                     hsvColor = new HsvColor(alpha, hue, saturation, value);
                     return true;
                 }
             }
 
+            // Local function to specially parse a double value with an optional percentage sign
+            bool TryInternalParse(string inString, out double outDouble)
+            {
+                // The percent sign, if it exists, must be at the end of the number
+                int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
+
+                if (percentIndex >= 0)
+                {
+                    var result = double.TryParse(
+                        inString.Substring(0, percentIndex),
+                        NumberStyles.Number,
+                        CultureInfo.InvariantCulture,
+                        out double percentage);
+
+                    outDouble = percentage / 100.0;
+                    return result;
+                }
+                else
+                {
+                    return double.TryParse(
+                        inString,
+                        NumberStyles.Number,
+                        CultureInfo.InvariantCulture,
+                        out outDouble);
+                }
+            }
+
             return false;
         }
 
         /// <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>
+        /// 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>
@@ -320,12 +353,12 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        /// Converts the given HSVA color channel values to its 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,152 +490,6 @@ namespace Avalonia.Media
                 (byte)Math.Round(b * 255));
         }
 
-        /// <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 FromRgb(Color color)
-        {
-            return HsvColor.FromRgb(color.R, color.G, color.B, color.A);
-        }
-
-        /// <summary>
-        /// Converts the given RGBA color channel values to its 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)
-        {
-            // Normalize RGBA channel values into the 0..1 range
-            return HsvColor.FromRgb(
-                (red / 255.0),
-                (green / 255.0),
-                (blue / 255.0),
-                (alpha / 255.0));
-        }
-
-        // TODO: Mark the below method Internal and make Internals visible to Avalonia.Controls...
-
-        /// <summary>
-        /// Converts the given RGBA color channel values to its HSV color equivalent.
-        /// </summary>
-        /// <remarks>
-        /// Warning: No bounds checks or clamping is done on the input channel values.
-        /// This method is for internal-use only and the caller must ensure bounds.
-        /// </remarks>
-        /// <param name="r">The red channel value in the RGB color model within the range 0..1.</param>
-        /// <param name="g">The green channel value in the RGB color model within the range 0..1.</param>
-        /// <param name="b">The blue channel value in the RGB color model within the range 0..1.</param>
-        /// <param name="a">The alpha channel value in the RGB color model within the range 0..1.</param>
-        /// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
-        public static HsvColor FromRgb(
-            double r,
-            double g,
-            double b,
-            double a = 1.0)
-        {
-            // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
-            // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
-            // This was used because it is the best documented and likely most optimized for performance
-            // Alpha channel support was added
-
-            double hue;
-            double saturation;
-            double value;
-
-            double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
-            double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
-
-            // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255).
-            // Conceptually speaking, it represents how much color is present.
-            // If at least one of R, G, B is 255, then there exists as much color as there can be.
-            // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds
-            // to black (i.e., the absence of any color).
-            value = max;
-
-            // The "chroma" of the color is a value directly proportional to the extent to which
-            // the color diverges from greyscale.  If, for example, we have RGB = (255, 255, 0),
-            // then the chroma is maximized - this is a pure yellow, no gray of any kind.
-            // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero
-            // implies that this color is pure greyscale, with no actual hue to be found.
-            var chroma = max - min;
-
-            // If the chrome is zero, then hue is technically undefined - a greyscale color
-            // has no hue.  For the sake of convenience, we'll just set hue to zero, since
-            // it will be unused in this circumstance.  Since the color is purely gray,
-            // saturation is also equal to zero - you can think of saturation as basically
-            // a measure of hue intensity, such that no hue at all corresponds to a
-            // nonexistent intensity.
-            if (chroma == 0)
-            {
-                hue = 0.0;
-                saturation = 0.0;
-            }
-            else
-            {
-                // In this block, hue is properly defined, so we'll extract both hue
-                // and saturation information from the RGB color.
-
-                // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees.
-                // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red.
-                // Every other hue is somewhere between either red and green, green and blue, and blue and red,
-                // so every other hue can be thought of as an angle on this color wheel.
-                // These if/else statements determines where on this color wheel our color lies.
-                if (r == max)
-                {
-                    // If the red channel is the most pronounced channel, then we exist
-                    // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees
-                    // where red dominates.  We figure out where in that section we are exactly
-                    // by considering whether the green or the blue channel is greater - by subtracting green from blue,
-                    // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then
-                    // we'll nudge ourselves closer to -60.  We then divide by chroma (which will actually make the result larger,
-                    // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue
-                    // even if we're very close to greyscale.
-                    hue = 60 * (g - b) / chroma;
-                }
-                else if (g == max)
-                {
-                    // We do the exact same for the case where the green channel is the most pronounced channel,
-                    // only this time we want to see if we should tilt towards the blue direction or the red direction.
-                    // We add 120 to center our value in the green third of the color wheel.
-                    hue = 120 + (60 * (b - r) / chroma);
-                }
-                else // blue == max
-                {
-                    // And we also do the exact same for the case where the blue channel is the most pronounced channel,
-                    // only this time we want to see if we should tilt towards the red direction or the green direction.
-                    // We add 240 to center our value in the blue third of the color wheel.
-                    hue = 240 + (60 * (r - g) / chroma);
-                }
-
-                // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero -
-                // this will bump red values from within -60 to -1 to 300 to 359.  The hue is the same at both values.
-                if (hue < 0.0)
-                {
-                    hue += 360.0;
-                }
-
-                // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is.
-                // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value).
-                // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1,
-                // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be.
-                // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5,
-                // meaning that this color is partially washed out.
-                // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue.
-                saturation = chroma / value;
-            }
-
-            return new HsvColor(a, hue, saturation, value, false);
-        }
-
         /// <summary>
         /// Indicates whether the values of two specified <see cref="HsvColor"/> objects are equal.
         /// </summary>

+ 6 - 0
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -285,6 +285,12 @@ namespace Avalonia.Rendering
                     renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
                 }
 
+                if (visual.HasMirrorTransform)
+                {
+                    var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+                    renderTransform *= mirrorMatrix;
+                }
+
                 m = renderTransform * m;
 
                 if (clipToBounds)

+ 6 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -195,6 +195,12 @@ namespace Avalonia.Rendering.SceneGraph
                     renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
                 }
 
+                if (visual.HasMirrorTransform)
+                {
+                    var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+                    renderTransform *= mirrorMatrix;
+                }
+
                 m = renderTransform * m;
 
                 using (contextImpl.BeginUpdate(node))

+ 18 - 1
src/Avalonia.Visuals/Visual.cs

@@ -68,6 +68,12 @@ namespace Avalonia
         public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
             AvaloniaProperty.Register<Visual, IBrush?>(nameof(OpacityMask));
 
+        /// <summary>
+        /// Defines the <see cref="HasMirrorTransform"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Visual, bool> HasMirrorTransformProperty =
+            AvaloniaProperty.RegisterDirect<Visual, bool>(nameof(HasMirrorTransform), o => o.HasMirrorTransform);
+
         /// <summary>
         /// Defines the <see cref="RenderTransform"/> property.
         /// </summary>
@@ -96,6 +102,7 @@ namespace Avalonia
         private TransformedBounds? _transformedBounds;
         private IRenderRoot? _visualRoot;
         private IVisual? _visualParent;
+        private bool _hasMirrorTransform;
 
         /// <summary>
         /// Initializes static members of the <see cref="Visual"/> class.
@@ -107,7 +114,8 @@ namespace Avalonia
                 ClipProperty,
                 ClipToBoundsProperty,
                 IsVisibleProperty,
-                OpacityProperty);
+                OpacityProperty,
+                HasMirrorTransformProperty);
             RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
             ZIndexProperty.Changed.Subscribe(ZIndexChanged);
         }
@@ -219,6 +227,15 @@ namespace Avalonia
             set { SetValue(OpacityMaskProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether to apply mirror transform on this control.
+        /// </summary>
+        public bool HasMirrorTransform 
+        { 
+            get { return _hasMirrorTransform; }
+            protected set { SetAndRaise(HasMirrorTransformProperty, ref _hasMirrorTransform, value); }
+        }
+
         /// <summary>
         /// Gets or sets the render transform of the control.
         /// </summary>

+ 6 - 0
src/Avalonia.Visuals/VisualExtensions.cs

@@ -110,6 +110,12 @@ namespace Avalonia
                     result *= renderTransform;
                 }
 
+                if (v.HasMirrorTransform)
+                {
+                    var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, v.Bounds.Width, 0);
+                    result *= mirrorMatrix;
+                }
+
                 var topLeft = v.Bounds.TopLeft;
 
                 if (topLeft != default)

+ 5 - 0
src/Avalonia.Visuals/VisualTree/IVisual.cs

@@ -75,6 +75,11 @@ namespace Avalonia.VisualTree
         /// </summary>
         IBrush? OpacityMask { get; set; }
 
+        /// <summary>
+        /// Gets a value indicating whether to apply mirror transform on this control.
+        /// </summary>
+        bool HasMirrorTransform { get; }
+
         /// <summary>
         /// Gets or sets the render transform of the control.
         /// </summary>

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj

@@ -12,5 +12,6 @@
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
-  </ItemGroup>  
+  </ItemGroup>
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -68,4 +68,5 @@
   </ItemGroup>
   <Import Project="..\..\..\build\Rx.props" />
   <Import Project="..\..\..\build\ApiDiff.props" />
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@@ -18,4 +18,5 @@
   <Import Project="..\..\..\build\System.Memory.props" />
   <Import Project="..\..\..\build\ApiDiff.props" />
   <Import Project="..\..\..\build\NullableEnable.props" />
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 1
src/Shared/ModuleInitializer.cs

@@ -3,7 +3,7 @@ namespace System.Runtime.CompilerServices
 #if !NET5_0_OR_GREATER
     internal class ModuleInitializerAttribute : Attribute
     {
-        
+
     }
 #endif
 }

+ 2 - 1
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@@ -16,5 +16,6 @@
   </ItemGroup>
   
   <Import Project="..\..\..\build\SkiaSharp.props" />
-  <Import Project="..\..\..\build\HarfBuzzSharp.props" />  
+  <Import Project="..\..\..\build\HarfBuzzSharp.props" />
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

@@ -18,4 +18,5 @@
   <Import Project="..\..\..\build\SharpDX.props" />
   <Import Project="..\..\..\build\HarfBuzzSharp.props" />
   <Import Project="..\..\..\build\JetBrains.Annotations.props" />
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 1 - 0
src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj

@@ -18,4 +18,5 @@
   </ItemGroup>
 
   <Import Project="..\..\..\build\SharpDX.props" />
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 2 - 1
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@@ -16,5 +16,6 @@
     <MicroComIdl Include="WinRT\winrt.idl" CSharpInteropPath="WinRT\WinRT.Generated.cs" />
     <MicroComIdl Include="Win32Com\win32.idl" CSharpInteropPath="Win32Com\Win32.Generated.cs" />
   </ItemGroup>
-  <Import Project="$(MSBuildThisFileDirectory)\..\..\..\build\System.Drawing.Common.props" />    
+  <Import Project="$(MSBuildThisFileDirectory)\..\..\..\build\System.Drawing.Common.props" />
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 2 - 0
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@@ -19,4 +19,6 @@
       <BuiltProjectOutputGroupOutput Remove="$(ProjectDir)$(OutDir)$(_DeploymentTargetApplicationManifestFileName)" />
     </ItemGroup>
   </Target>
+
+  <Import Project="..\..\..\build\DevAnalyzers.props" />
 </Project>

+ 17 - 0
src/tools/DevAnalyzers/DevAnalyzers.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <LangVersion>10</LangVersion>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
+  </ItemGroup>
+
+</Project>

+ 40 - 0
src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs

@@ -0,0 +1,40 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace DevAnalyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class GenericVirtualAnalyzer : DiagnosticAnalyzer
+{
+    public const string DiagnosticId = "AVADEV1001";
+
+    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+        DiagnosticId,
+        "Do not use generic virtual methods",
+        "Method '{0}' is a generic virtual method",
+        "Performance",
+        DiagnosticSeverity.Warning,
+        isEnabledByDefault: true,
+        description: "Generic virtual methods affect JIT startup time adversly and should be avoided.");
+
+    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+    public override void Initialize(AnalysisContext context)
+    {
+        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+        context.EnableConcurrentExecution();
+        context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
+    }
+
+    private static void AnalyzeMethod(SymbolAnalysisContext context)
+    {
+        var symbol = (IMethodSymbol)context.Symbol;
+
+        if (symbol.IsGenericMethod &&
+            (symbol.IsVirtual || symbol.ContainingType.TypeKind == TypeKind.Interface))
+        {
+            context.ReportDiagnostic(Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name));
+        }
+    }
+}

+ 8 - 0
src/tools/DevAnalyzers/GlobalSuppressions.cs

@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]

+ 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; }
+        }
+    }
+}

Some files were not shown because too many files changed in this diff