Przeglądaj źródła

Merge branch 'master' into fixes/referencetype-binding-null-behaviour

Steven Kirk 2 lat temu
rodzic
commit
b2795de0f9
100 zmienionych plików z 1693 dodań i 994 usunięć
  1. 15 10
      .editorconfig
  2. 1 0
      Avalonia.Desktop.slnf
  3. 13 0
      Avalonia.sln
  4. 3 3
      build/HarfBuzzSharp.props
  5. 1 1
      build/ImageSharp.props
  6. 1 1
      build/Moq.props
  7. 1 0
      build/SharedVersion.props
  8. 3 3
      build/SkiaSharp.props
  9. 7 8
      build/XUnit.props
  10. 1 1
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  11. 0 7
      samples/BindingDemo/App.xaml
  12. 2 2
      samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj
  13. 0 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  14. 20 4
      samples/ControlCatalog/App.xaml
  15. 8 40
      samples/ControlCatalog/App.xaml.cs
  16. 1 0
      samples/ControlCatalog/ControlCatalog.csproj
  17. 17 6
      samples/ControlCatalog/MainView.xaml
  18. 14 25
      samples/ControlCatalog/MainView.xaml.cs
  19. 2 4
      samples/ControlCatalog/Models/CatalogTheme.cs
  20. 15 15
      samples/ControlCatalog/Pages/DateTimePickerPage.xaml
  21. 11 11
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  22. 0 1
      samples/ControlCatalog/Pages/GesturePage.cs
  23. 1 1
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  24. 8 8
      samples/ControlCatalog/Pages/SplitViewPage.xaml
  25. 1 1
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  26. 79 0
      samples/ControlCatalog/Pages/ThemePage.axaml
  27. 37 0
      samples/ControlCatalog/Pages/ThemePage.axaml.cs
  28. 3 2
      samples/GpuInterop/MainWindow.axaml.cs
  29. 3 3
      samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs
  30. 3 4
      samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs
  31. 25 25
      samples/GpuInterop/VulkanDemo/VulkanContent.cs
  32. 6 6
      samples/GpuInterop/VulkanDemo/VulkanContext.cs
  33. 0 12
      samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs
  34. 9 9
      samples/GpuInterop/VulkanDemo/VulkanImage.cs
  35. 4 4
      samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs
  36. 1 4
      samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs
  37. 1 1
      samples/IntegrationTestApp/App.axaml
  38. 30 24
      samples/IntegrationTestApp/MainWindow.axaml
  39. 91 0
      samples/IntegrationTestApp/MainWindow.axaml.cs
  40. 0 1
      samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj
  41. 3 2
      samples/MobileSandbox/App.xaml
  42. 1 1
      samples/PlatformSanityChecks/App.xaml
  43. 1 1
      samples/Previewer/App.xaml
  44. 14 0
      samples/RenderDemo/MainWindow.xaml
  45. 19 4
      samples/RenderDemo/MainWindow.xaml.cs
  46. 31 14
      samples/RenderDemo/ViewModels/MainWindowViewModel.cs
  47. 10 2
      samples/SampleControls/HamburgerMenu/HamburgerMenu.cs
  48. 24 10
      samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml
  49. 1 1
      samples/Sandbox/App.axaml
  50. 27 12
      src/Avalonia.Base/Animation/Animatable.cs
  51. 4 7
      src/Avalonia.Base/Animation/KeySpline.cs
  52. 15 23
      src/Avalonia.Base/AvaloniaObject.cs
  53. 13 5
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  54. 112 0
      src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs
  55. 13 0
      src/Avalonia.Base/Collections/IAvaloniaDictionary.cs
  56. 14 0
      src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs
  57. 6 0
      src/Avalonia.Base/Controls/IResourceDictionary.cs
  58. 4 3
      src/Avalonia.Base/Controls/IResourceNode.cs
  59. 85 6
      src/Avalonia.Base/Controls/ResourceDictionary.cs
  60. 109 16
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  61. 1 1
      src/Avalonia.Base/Data/InstancedBinding.cs
  62. 0 3
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  63. 5 13
      src/Avalonia.Base/Input/DragEventArgs.cs
  64. 1 1
      src/Avalonia.Base/Input/KeyGesture.cs
  65. 19 25
      src/Avalonia.Base/Input/KeyboardNavigationHandler.cs
  66. 15 33
      src/Avalonia.Base/Input/Navigation/TabNavigation.cs
  67. 12 8
      src/Avalonia.Base/Layout/LayoutManager.cs
  68. 3 3
      src/Avalonia.Base/LogicalTree/LogicalExtensions.cs
  69. 2 7
      src/Avalonia.Base/Media/Color.cs
  70. 1 1
      src/Avalonia.Base/Media/DrawingContext.cs
  71. 13 13
      src/Avalonia.Base/Media/DrawingGroup.cs
  72. 3 3
      src/Avalonia.Base/Media/DrawingImage.cs
  73. 2 2
      src/Avalonia.Base/Media/FontFamily.cs
  74. 1 4
      src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs
  75. 26 12
      src/Avalonia.Base/Media/FormattedText.cs
  76. 3 3
      src/Avalonia.Base/Media/GeometryDrawing.cs
  77. 1 1
      src/Avalonia.Base/Media/GlyphRun.cs
  78. 6 6
      src/Avalonia.Base/Media/GlyphRunDrawing.cs
  79. 1 1
      src/Avalonia.Base/Media/HslColor.cs
  80. 1 1
      src/Avalonia.Base/Media/HsvColor.cs
  81. 1 2
      src/Avalonia.Base/Media/IVisualBrush.cs
  82. 6 18
      src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs
  83. 3 4
      src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs
  84. 7 7
      src/Avalonia.Base/Media/TextDecoration.cs
  85. 1 3
      src/Avalonia.Base/Media/TextFormatting/ITextSource.cs
  86. 5 11
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  87. 14 22
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  88. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
  89. 111 98
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  90. 37 17
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  91. 7 8
      src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs
  92. 398 294
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  93. 30 0
      src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs
  94. 3 4
      src/Avalonia.Base/Media/VisualBrush.cs
  95. 2 2
      src/Avalonia.Base/Metadata/AmbientAttribute.cs
  96. 1 1
      src/Avalonia.Base/Metadata/ContentAttribute.cs
  97. 2 2
      src/Avalonia.Base/Metadata/DataTypeAttribute.cs
  98. 1 1
      src/Avalonia.Base/Metadata/DependsOnAttribute.cs
  99. 2 2
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  100. 1 1
      src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs

+ 15 - 10
.editorconfig

@@ -55,16 +55,17 @@ dotnet_naming_symbols.constant_fields.required_modifiers = const
 
 dotnet_naming_style.pascal_case_style.capitalization = pascal_case
 
-# static fields should have s_ prefix
-dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
-dotnet_naming_rule.static_fields_should_have_prefix.symbols  = static_fields
-dotnet_naming_rule.static_fields_should_have_prefix.style    = static_prefix_style
+# private static fields should have s_ prefix
+dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_have_prefix.symbols  = private_static_fields
+dotnet_naming_rule.private_static_fields_should_have_prefix.style    = private_static_prefix_style
 
-dotnet_naming_symbols.static_fields.applicable_kinds   = field
-dotnet_naming_symbols.static_fields.required_modifiers = static
+dotnet_naming_symbols.private_static_fields.applicable_kinds   = field
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
 
-dotnet_naming_style.static_prefix_style.required_prefix = s_
-dotnet_naming_style.static_prefix_style.capitalization = camel_case
+dotnet_naming_style.private_static_prefix_style.required_prefix = s_
+dotnet_naming_style.private_static_prefix_style.capitalization = camel_case
 
 # internal and private fields should be _camelCase
 dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
@@ -117,7 +118,7 @@ csharp_space_after_dot = false
 csharp_space_after_keywords_in_control_flow_statements = true
 csharp_space_after_semicolon_in_for_statement = true
 csharp_space_around_binary_operators = before_and_after
-csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_around_declaration_statements = false
 csharp_space_before_colon_in_inheritance_clause = true
 csharp_space_before_comma = false
 csharp_space_before_dot = false
@@ -145,10 +146,14 @@ dotnet_diagnostic.CS1591.severity = suggestion
 
 # CS0162: Remove unreachable code
 dotnet_diagnostic.CS0162.severity = error
+# CA1018: Mark attributes with AttributeUsageAttribute
+dotnet_diagnostic.CA1018.severity = error
 # CA1304: Specify CultureInfo
 dotnet_diagnostic.CA1304.severity = warning
 # CA1802: Use literals where appropriate
 dotnet_diagnostic.CA1802.severity = warning
+# CA1813: Avoid unsealed attributes
+dotnet_diagnostic.CA1813.severity = error
 # CA1815: Override equals and operator equals on value types
 dotnet_diagnostic.CA1815.severity = warning
 # CA1820: Test for empty strings using string length
@@ -207,5 +212,5 @@ indent_size = 2
 # Shell scripts
 [*.sh]
 end_of_line = lf
-[*.{cmd, bat}]
+[*.{cmd,bat}]
 end_of_line = crlf

+ 1 - 0
Avalonia.Desktop.slnf

@@ -15,6 +15,7 @@
       "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
       "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
       "src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.csproj",
+      "src\\Avalonia.Controls.ItemsRepeater\\Avalonia.Controls.ItemsRepeater.csproj",
       "src\\Avalonia.Controls\\Avalonia.Controls.csproj",
       "src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj",
       "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj",

+ 13 - 0
Avalonia.sln

@@ -233,6 +233,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -548,6 +552,14 @@ Global
 		{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -613,6 +625,7 @@ Global
 		{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+		{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 3 - 3
build/HarfBuzzSharp.props

@@ -1,7 +1,7 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="HarfBuzzSharp" Version="2.8.2.1-preview.108" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.1-preview.108" />
-    <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2.1-preview.108" />
+    <PackageReference Include="HarfBuzzSharp" Version="2.8.2.3" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
+    <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="2.8.2.3" />
   </ItemGroup>
 </Project>

+ 1 - 1
build/ImageSharp.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="SixLabors.ImageSharp" Version="2.1.1" />
+    <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
   </ItemGroup>
 </Project>

+ 1 - 1
build/Moq.props

@@ -1,5 +1,5 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="Moq" Version="4.14.1" />
+    <PackageReference Include="Moq" Version="4.18.4" />
   </ItemGroup>
 </Project>

+ 1 - 0
build/SharedVersion.props

@@ -3,6 +3,7 @@
   <PropertyGroup>
     <Product>Avalonia</Product>
     <Version>11.0.999</Version>
+    <Authors>Avalonia Team</Authors>
     <Copyright>Copyright 2022 &#169; The AvaloniaUI Project</Copyright>
     <PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
     <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>

+ 3 - 3
build/SkiaSharp.props

@@ -1,7 +1,7 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="SkiaSharp" Version="2.88.1" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1" />
-    <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.1" />
+    <PackageReference Include="SkiaSharp" Version="2.88.3" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
+    <PackageReference Condition="'$(IncludeWasmSkia)' == 'true'" Include="SkiaSharp.NativeAssets.WebAssembly" Version="2.88.3" />
   </ItemGroup>
 </Project>

+ 7 - 8
build/XUnit.props

@@ -1,13 +1,12 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="xunit" Version="2.4.1" />
-    <PackageReference Include="xunit.abstractions" Version="2.0.3" />
-    <PackageReference Include="xunit.assert" Version="2.4.1" />
-    <PackageReference Include="xunit.core" Version="2.4.1" />
-    <PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
-    <PackageReference Include="xunit.extensibility.execution" Version="2.4.1" />
-    <PackageReference Include="xunit.runner.console" Version="2.4.1" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
+    <PackageReference Include="xunit" Version="2.4.2" />
+    <PackageReference Include="xunit.assert" Version="2.4.2" />
+    <PackageReference Include="xunit.core" Version="2.4.2" />
+    <PackageReference Include="xunit.extensibility.core" Version="2.4.2" />
+    <PackageReference Include="xunit.extensibility.execution" Version="2.4.2" />
+    <PackageReference Include="xunit.runner.console" Version="2.4.2" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
     <PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
   </ItemGroup>

+ 1 - 1
native/Avalonia.Native/src/OSX/WindowImpl.mm

@@ -66,7 +66,7 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
         _isModal = isDialog;
 
         WindowBaseImpl::Show(activate, isDialog);
-
+        GetWindowState(&_actualWindowState);
         HideOrShowTrafficLights();
 
         return SetWindowState(_lastWindowState);

+ 0 - 7
samples/BindingDemo/App.xaml

@@ -2,13 +2,6 @@
     xmlns="https://github.com/avaloniaui" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     x:Class="BindingDemo.App">
-  <Application.Resources>
-    <ResourceDictionary>
-      <ResourceDictionary.MergedDictionaries>
-        <ResourceInclude Source="avares://Avalonia.Themes.Simple/Accents/BaseLight.xaml"/>
-      </ResourceDictionary.MergedDictionaries>
-    </ResourceDictionary>
-  </Application.Resources>
   <Application.Styles>
     <FluentTheme />
    </Application.Styles>

+ 2 - 2
samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj

@@ -9,8 +9,8 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.0-rc.1.22427.2" />
-    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.0-rc.1.22427.2" PrivateAssets="all" />
+    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.2" />
+    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.2" PrivateAssets="all" />
   </ItemGroup>
 
   <ItemGroup>

+ 0 - 1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@@ -31,7 +31,6 @@
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
-    <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
     <!-- For native controls test -->
     <PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
   </ItemGroup>

+ 20 - 4
samples/ControlCatalog/App.xaml

@@ -6,18 +6,34 @@
              x:Class="ControlCatalog.App">
   <Application.Resources>
     <ResourceDictionary>
+      <!-- Custom controls defined in other assemblies -->
       <ResourceDictionary.MergedDictionaries>
         <ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
       </ResourceDictionary.MergedDictionaries>
 
+      <!-- Resources used only in the control catalog -->
+      <ResourceDictionary.ThemeDictionaries>
+        <ResourceDictionary x:Key="Default">
+          <Color x:Key="CatalogBaseLowColor">#33000000</Color>
+          <Color x:Key="CatalogBaseMediumColor">#99000000</Color>
+          <Color x:Key="CatalogChromeMediumColor">#FFE6E6E6</Color>
+          <Color x:Key="CatalogBaseHighColor">#FF000000</Color>
+        </ResourceDictionary>
+        <ResourceDictionary x:Key="Dark">
+          <Color x:Key="CatalogBaseLowColor">#33FFFFFF</Color>
+          <Color x:Key="CatalogBaseMediumColor">#99FFFFFF</Color>
+          <Color x:Key="CatalogChromeMediumColor">#FF1F1F1F</Color>
+          <Color x:Key="CatalogBaseHighColor">#FFFFFFFF</Color>
+        </ResourceDictionary>
+      </ResourceDictionary.ThemeDictionaries>
+      <Color x:Key="SystemAccentColor">#FF0078D7</Color>
+      <Color x:Key="SystemAccentColorDark1">#FF005A9E</Color>
+
+      <!-- Styles attached dynamically depending on current theme (simple or fluent) -->
       <StyleInclude x:Key="DataGridFluent" Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
       <StyleInclude x:Key="DataGridSimple" Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" />
       <StyleInclude x:Key="ColorPickerFluent" Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
       <StyleInclude x:Key="ColorPickerSimple" Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml" />
-      <ResourceInclude x:Key="FluentAccentColors" Source="avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml" />
-      <ResourceInclude x:Key="FluentBaseLightColors" Source="avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml" />
-      <ResourceInclude x:Key="FluentBaseDarkColors" Source="avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml" />
-      <ResourceInclude x:Key="FluentBaseColors" Source="avares://Avalonia.Themes.Fluent/Accents/Base.xaml" />
     </ResourceDictionary>
   </Application.Resources>
   <Application.Styles>

+ 8 - 40
samples/ControlCatalog/App.xaml.cs

@@ -16,7 +16,6 @@ namespace ControlCatalog
         private readonly Styles _themeStylesContainer = new();
         private FluentTheme? _fluentTheme;
         private SimpleTheme? _simpleTheme;
-        private IResourceDictionary? _fluentBaseLightColors, _fluentBaseDarkColors;
         private IStyle? _colorPickerFluent, _colorPickerSimple;
         private IStyle? _dataGridFluent, _dataGridSimple;
         
@@ -33,16 +32,12 @@ namespace ControlCatalog
 
             _fluentTheme = new FluentTheme();
             _simpleTheme = new SimpleTheme();
-            _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentAccentColors"]!);
-            _simpleTheme.Resources.MergedDictionaries.Add((IResourceDictionary)Resources["FluentBaseColors"]!);
             _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!;
             _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!;
             _dataGridFluent = (IStyle)Resources["DataGridFluent"]!;
             _dataGridSimple = (IStyle)Resources["DataGridSimple"]!;
-            _fluentBaseLightColors = (IResourceDictionary)Resources["FluentBaseLightColors"]!;
-            _fluentBaseDarkColors = (IResourceDictionary)Resources["FluentBaseDarkColors"]!;
             
-            SetThemeVariant(CatalogTheme.FluentLight);
+            SetCatalogThemes(CatalogTheme.Fluent);
         }
 
         public override void OnFrameworkInitializationCompleted()
@@ -61,19 +56,12 @@ namespace ControlCatalog
 
         private CatalogTheme _prevTheme;
         public static CatalogTheme CurrentTheme => ((App)Current!)._prevTheme; 
-        public static void SetThemeVariant(CatalogTheme theme)
+        public static void SetCatalogThemes(CatalogTheme theme)
         {
             var app = (App)Current!;
             var prevTheme = app._prevTheme;
             app._prevTheme = theme;
-            var shouldReopenWindow = theme switch
-            {
-                CatalogTheme.FluentLight => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight,
-                CatalogTheme.FluentDark => prevTheme is CatalogTheme.SimpleDark or CatalogTheme.SimpleLight,
-                CatalogTheme.SimpleLight => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight,
-                CatalogTheme.SimpleDark => prevTheme is CatalogTheme.FluentDark or CatalogTheme.FluentLight,
-                _ => throw new ArgumentOutOfRangeException(nameof(theme), theme, null)
-            };
+            var shouldReopenWindow = prevTheme != theme;
             
             if (app._themeStylesContainer.Count == 0)
             {
@@ -81,36 +69,16 @@ namespace ControlCatalog
                 app._themeStylesContainer.Add(new Style());
                 app._themeStylesContainer.Add(new Style());
             }
-            
-            if (theme == CatalogTheme.FluentLight)
-            {
-                app._fluentTheme!.Mode = FluentThemeMode.Light;
-                app._themeStylesContainer[0] = app._fluentTheme;
-                app._themeStylesContainer[1] = app._colorPickerFluent!;
-                app._themeStylesContainer[2] = app._dataGridFluent!;
-            }
-            else if (theme == CatalogTheme.FluentDark)
+
+            if (theme == CatalogTheme.Fluent)
             {
-                app._fluentTheme!.Mode = FluentThemeMode.Dark;
-                app._themeStylesContainer[0] = app._fluentTheme;
+                app._themeStylesContainer[0] = app._fluentTheme!;
                 app._themeStylesContainer[1] = app._colorPickerFluent!;
                 app._themeStylesContainer[2] = app._dataGridFluent!;
             }
-            else if (theme == CatalogTheme.SimpleLight)
-            {
-                app._simpleTheme!.Mode = SimpleThemeMode.Light;
-                app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseDarkColors!);
-                app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseLightColors!);
-                app._themeStylesContainer[0] = app._simpleTheme;
-                app._themeStylesContainer[1] = app._colorPickerSimple!;
-                app._themeStylesContainer[2] = app._dataGridSimple!;
-            }
-            else if (theme == CatalogTheme.SimpleDark)
+            else if (theme == CatalogTheme.Simple)
             {
-                app._simpleTheme!.Mode = SimpleThemeMode.Dark;
-                app._simpleTheme.Resources.MergedDictionaries.Remove(app._fluentBaseLightColors!);
-                app._simpleTheme.Resources.MergedDictionaries.Add(app._fluentBaseDarkColors!);
-                app._themeStylesContainer[0] = app._simpleTheme;
+                app._themeStylesContainer[0] = app._simpleTheme!;
                 app._themeStylesContainer[1] = app._colorPickerSimple!;
                 app._themeStylesContainer[2] = app._dataGridSimple!;
             }

+ 1 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -26,6 +26,7 @@
     <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

+ 17 - 6
samples/ControlCatalog/MainView.xaml

@@ -14,8 +14,8 @@
         <Setter Property="HorizontalAlignment" Value="Left" />
       </Style>
     </Grid.Styles>
-      <controls:HamburgerMenu Name="Sidebar">
-        <TabItem Header="Composition">
+    <controls:HamburgerMenu Name="Sidebar">
+      <TabItem Header="Composition">
         <pages:CompositionPage/>
       </TabItem>
       <TabItem Header="Acrylic">
@@ -168,6 +168,9 @@
       <TabItem Header="TextBlock">
         <pages:TextBlockPage />
       </TabItem>
+      <TabItem Header="Theme Variants">
+        <pages:ThemePage />
+      </TabItem>
       <TabItem Header="ToggleSwitch">
         <pages:ToggleSwitchPage />
       </TabItem>
@@ -201,14 +204,22 @@
                 <SystemDecorations>Full</SystemDecorations>
               </ComboBox.Items>
             </ComboBox>
+            <ComboBox x:Name="ThemeVariants"
+                      HorizontalAlignment="Stretch"
+                      DisplayMemberBinding="{Binding Key, x:DataType=ThemeVariant}"
+                      SelectedIndex="0">
+              <ComboBox.Items>
+                <ThemeVariant>Default</ThemeVariant>
+                <ThemeVariant>Light</ThemeVariant>
+                <ThemeVariant>Dark</ThemeVariant>
+              </ComboBox.Items>
+            </ComboBox>
             <ComboBox x:Name="Themes"
                       HorizontalAlignment="Stretch"
                       SelectedIndex="0">
               <ComboBox.Items>
-                <models:CatalogTheme>FluentLight</models:CatalogTheme>
-                <models:CatalogTheme>FluentDark</models:CatalogTheme>
-                <models:CatalogTheme>SimpleLight</models:CatalogTheme>
-                <models:CatalogTheme>SimpleDark</models:CatalogTheme>
+                <models:CatalogTheme>Fluent</models:CatalogTheme>
+                <models:CatalogTheme>Simple</models:CatalogTheme>
               </ComboBox.Items>
             </ComboBox>
             <ComboBox x:Name="TransparencyLevels"

+ 14 - 25
samples/ControlCatalog/MainView.xaml.cs

@@ -9,6 +9,7 @@ using Avalonia.Media;
 using Avalonia.Media.Immutable;
 using Avalonia.Platform;
 using Avalonia.VisualTree;
+using Avalonia.Styling;
 using ControlCatalog.Models;
 using ControlCatalog.Pages;
 
@@ -42,16 +43,16 @@ namespace ControlCatalog
             {
                 if (themes.SelectedItem is CatalogTheme theme)
                 {
-                    App.SetThemeVariant(theme);
-                    
-                    ((TopLevel?)this.GetVisualRoot())?.PlatformImpl?.SetFrameThemeVariant(theme switch
-                    {
-                        CatalogTheme.FluentLight => PlatformThemeVariant.Light,
-                        CatalogTheme.FluentDark => PlatformThemeVariant.Dark,
-                        CatalogTheme.SimpleLight => PlatformThemeVariant.Light,
-                        CatalogTheme.SimpleDark => PlatformThemeVariant.Dark,
-                        _ => throw new ArgumentOutOfRangeException()
-                    });
+                    App.SetCatalogThemes(theme);
+                }
+            };
+            var themeVariants = this.Get<ComboBox>("ThemeVariants");
+            themeVariants.SelectedItem = Application.Current!.RequestedThemeVariant;
+            themeVariants.SelectionChanged += (sender, e) =>
+            {
+                if (themeVariants.SelectedItem is ThemeVariant themeVariant)
+                {
+                    Application.Current!.RequestedThemeVariant = themeVariant;
                 }
             };
 
@@ -118,25 +119,13 @@ namespace ControlCatalog
 
         private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e)
         {
-            var themes = this.Get<ComboBox>("Themes");
-            var currentTheme = (CatalogTheme?)themes.SelectedItem ?? CatalogTheme.FluentLight;
-            var newTheme = (currentTheme, e.ThemeVariant) switch
-            {
-                (CatalogTheme.FluentDark, PlatformThemeVariant.Light) => CatalogTheme.FluentLight,
-                (CatalogTheme.FluentLight, PlatformThemeVariant.Dark) => CatalogTheme.FluentDark,
-                (CatalogTheme.SimpleDark, PlatformThemeVariant.Light) => CatalogTheme.SimpleLight,
-                (CatalogTheme.SimpleLight, PlatformThemeVariant.Dark) => CatalogTheme.SimpleDark,
-                _ => currentTheme
-            };
-            themes.SelectedItem = newTheme;
-
             Application.Current!.Resources["SystemAccentColor"] = e.AccentColor1;
             Application.Current.Resources["SystemAccentColorDark1"] = ChangeColorLuminosity(e.AccentColor1, -0.3);
             Application.Current.Resources["SystemAccentColorDark2"] = ChangeColorLuminosity(e.AccentColor1, -0.5);
             Application.Current.Resources["SystemAccentColorDark3"] = ChangeColorLuminosity(e.AccentColor1, -0.7);
-            Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, -0.3);
-            Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, -0.5);
-            Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, -0.7);
+            Application.Current.Resources["SystemAccentColorLight1"] = ChangeColorLuminosity(e.AccentColor1, 0.3);
+            Application.Current.Resources["SystemAccentColorLight2"] = ChangeColorLuminosity(e.AccentColor1, 0.5);
+            Application.Current.Resources["SystemAccentColorLight3"] = ChangeColorLuminosity(e.AccentColor1, 0.7);
 
             static Color ChangeColorLuminosity(Color color, double luminosityFactor)
             {

+ 2 - 4
samples/ControlCatalog/Models/CatalogTheme.cs

@@ -2,9 +2,7 @@
 {
     public enum CatalogTheme
     {
-        FluentLight,
-        FluentDark,
-        SimpleLight,
-        SimpleDark
+        Fluent,
+        Simple
     }
 }

+ 15 - 15
samples/ControlCatalog/Pages/DateTimePickerPage.xaml

@@ -15,11 +15,11 @@
                 Spacing="16">
       <TextBlock FontSize="18">A simple DatePicker</TextBlock>
       <StackPanel Orientation="Vertical">
-        <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                 BorderThickness="1" Padding="15">
           <DatePicker />
         </Border>
-        <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
           <TextBlock Padding="15">
             <TextBlock.Text>
               <x:String>
@@ -31,7 +31,7 @@
       </StackPanel>
       
       
-      <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+      <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
               BorderThickness="1" Padding="15">
         <DatePicker >
           <DataValidationErrors.Error>
@@ -42,12 +42,12 @@
       
       <TextBlock FontSize="18">A DatePicker with day formatted and year hidden.</TextBlock>
       <StackPanel Orientation="Vertical">
-        <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                 BorderThickness="1" Padding="15">
           <DatePicker x:Name="Control2" DayFormat="d (ddd)"
                              YearVisible="False" />
         </Border>
-        <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
           <TextBlock Padding="15">
             <TextBlock.Text>
               <x:String>
@@ -58,15 +58,15 @@
         </Panel>
       </StackPanel>
 
-      <Border Background="{DynamicResource SystemControlHighlightBaseLowBrush}" BorderThickness="1" Margin="15" />
+      <Border Background="{DynamicResource CatalogBaseLowColor}" BorderThickness="1" Margin="15" />
 
       <TextBlock FontSize="18">A simple TimePicker.</TextBlock>
       <StackPanel Orientation="Vertical">
-        <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                 BorderThickness="1" Padding="15">
           <TimePicker />
         </Border>
-        <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
           <TextBlock Padding="15">
             <TextBlock.Text>
               <x:String>
@@ -77,7 +77,7 @@
         </Panel>
       </StackPanel>
       
-      <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+      <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
               BorderThickness="1" Padding="15">
         <TimePicker>
           <DataValidationErrors.Error>
@@ -88,11 +88,11 @@
 
       <TextBlock FontSize="18">A TimePicker with minute increments specified.</TextBlock>
       <StackPanel Orientation="Vertical">
-        <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                 BorderThickness="1" Padding="15">
           <TimePicker MinuteIncrement="15" />
         </Border>
-        <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
           <TextBlock Padding="15">
             <TextBlock.Text>
               <x:String>
@@ -105,11 +105,11 @@
 
       <TextBlock FontSize="18">A TimePicker using a 12-hour clock.</TextBlock>
       <StackPanel Orientation="Vertical">
-        <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                 BorderThickness="1" Padding="15">
           <TimePicker ClockIdentifier="12HourClock"/>
         </Border>
-        <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
           <TextBlock Padding="15">
             <TextBlock.Text>
               <x:String>
@@ -122,11 +122,11 @@
 
       <TextBlock FontSize="18">A TimePicker using a 24-hour clock.</TextBlock>
       <StackPanel Orientation="Vertical">
-        <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                 BorderThickness="1" Padding="15">
           <TimePicker ClockIdentifier="24HourClock" />
         </Border>
-        <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
           <TextBlock Padding="15">
             <TextBlock.Text>
               <x:String>

+ 11 - 11
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@@ -26,31 +26,31 @@
         <StackPanel Spacing="10">
             <TextBlock FontSize="18" Text="Button with a Flyout" />
             <StackPanel>
-                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                         BorderThickness="1" Padding="15">
                     <Button Content="Click Me!" Flyout="{StaticResource BasicFlyout}" />
                 </Border>
-                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                <Panel Background="{DynamicResource CatalogBaseLowColor}">
                     <TextBlock Name="ButtonFlyoutXamlText" Padding="15" />
                 </Panel>
             </StackPanel>
 
             <TextBlock FontSize="18" Text="MenuFlyout" />
             <StackPanel>
-                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                         BorderThickness="1" Padding="15">
                     <Button Content="Click Me!" Flyout="{StaticResource SharedMenuFlyout}" />
                 </Border>
-                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                <Panel Background="{DynamicResource CatalogBaseLowColor}">
                     <TextBlock Name="MenuFlyoutXamlText" Padding="15" />
                 </Panel>
             </StackPanel>
 
             <TextBlock FontSize="18" Text="Attached Flyouts" />
             <StackPanel>
-                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                         BorderThickness="1" Padding="15">
-                    <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
+                    <Panel Background="{DynamicResource CatalogBaseLowColor}"
                            HorizontalAlignment="Left"
                            Height="100"
                            Name="AttachedFlyoutPanel">
@@ -70,7 +70,7 @@
 
                     </Panel>
                 </Border>
-                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                <Panel Background="{DynamicResource CatalogBaseLowColor}">
                     <TextBlock Name="AttachedFlyoutXamlText" Padding="15" />
                 </Panel>
             </StackPanel>
@@ -78,21 +78,21 @@
 
             <TextBlock FontSize="18" Text="Sharing Flyouts" />
             <StackPanel>
-                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                         BorderThickness="1" Padding="15">
                     <StackPanel Orientation="Horizontal" Spacing="30">
                         <Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
                         <Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
                     </StackPanel>
                 </Border>
-                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                <Panel Background="{DynamicResource CatalogBaseLowColor}">
                     <TextBlock Name="SharedFlyoutXamlText" Padding="15" />
                 </Panel>
             </StackPanel>
 
             <TextBlock FontSize="18" Text="Flyout Placements" />
             <StackPanel>
-                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                         BorderThickness="1" Padding="15">
                     <UniformGrid Columns="3">
                         <UniformGrid.Styles>
@@ -215,7 +215,7 @@
 
             <TextBlock FontSize="18" Text="Flyout ShowMode" />
             <StackPanel>
-                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                         BorderThickness="1" Padding="15">
                     <WrapPanel Orientation="Horizontal">
                         <WrapPanel.Styles>

+ 0 - 1
samples/ControlCatalog/Pages/GesturePage.cs

@@ -70,7 +70,6 @@ namespace ControlCatalog.Pages
 
             _currentScale = 1;
             Vector3 currentOffset = default;
-            bool isZooming = false;
 
             CompositionVisual? compositionVisual = null;
 

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

@@ -62,7 +62,7 @@
       <Button x:Name="scrollToRandom">Scroll to Random</Button>
       <Button x:Name="scrollToSelected">Scroll to Selected</Button>
     </StackPanel>
-    <Border BorderThickness="1" BorderBrush="{DynamicResource SystemControlHighlightBaseMediumLowBrush}" Margin="0 0 0 16">
+    <Border BorderThickness="1" BorderBrush="{DynamicResource CatalogBaseMediumColor}" Margin="0 0 0 16">
       <ScrollViewer Name="scroller"
                     HorizontalScrollBarVisibility="Auto"
                     VerticalScrollBarVisibility="Auto">

+ 8 - 8
samples/ControlCatalog/Pages/SplitViewPage.xaml

@@ -32,7 +32,7 @@
 
         <TextBlock Text="PaneBackground" />
         <ComboBox Name="PaneBackgroundSelector" SelectedIndex="0" Width="170" Margin="10">
-          <ComboBoxItem Tag="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}">SystemControlBackgroundChromeMediumLowBrush</ComboBoxItem>
+          <ComboBoxItem Tag="{DynamicResource CatalogChromeMediumColor}">CatalogChromeMediumColor</ComboBoxItem>
           <ComboBoxItem Tag="Red">Red</ComboBoxItem>
           <ComboBoxItem Tag="Blue">Blue</ComboBoxItem>
           <ComboBoxItem Tag="Green">Green</ComboBoxItem>
@@ -48,7 +48,7 @@
 
       </StackPanel>
 
-      <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+      <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
             BorderThickness="1">
         <!--{Binding SelectedItem.Tag, ElementName=PaneBackgroundSelector}-->
         <SplitView Name="SplitView"
@@ -77,7 +77,7 @@
                   <Border Width="48">
                     <Viewbox Width="24" Height="24" HorizontalAlignment="Left">
                       <Canvas Width="24" Height="24">
-                        <Path Fill="{DynamicResource SystemControlForegroundBaseHighBrush}" Data="M16 17V19H2V17S2 13 9 13 16 17 16 17M12.5 7.5A3.5 3.5 0 1 0 9 11A3.5 3.5 0 0 0 12.5 7.5M15.94 13A5.32 5.32 0 0 1 18 17V19H22V17S22 13.37 15.94 13M15 4A3.39 3.39 0 0 0 13.07 4.59A5 5 0 0 1 13.07 10.41A3.39 3.39 0 0 0 15 11A3.5 3.5 0 0 0 15 4Z" />
+                        <Path Fill="{DynamicResource CatalogBaseHighColor}" Data="M16 17V19H2V17S2 13 9 13 16 17 16 17M12.5 7.5A3.5 3.5 0 1 0 9 11A3.5 3.5 0 0 0 12.5 7.5M15.94 13A5.32 5.32 0 0 1 18 17V19H22V17S22 13.37 15.94 13M15 4A3.39 3.39 0 0 0 13.07 4.59A5 5 0 0 1 13.07 10.41A3.39 3.39 0 0 0 15 11A3.5 3.5 0 0 0 15 4Z" />
                       </Canvas>
                     </Viewbox>
                   </Border>
@@ -89,11 +89,11 @@
           </SplitView.Pane>
 
           <Grid>
-            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Center" VerticalAlignment="Center"  Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
-            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" TextAlignment="Left"  Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
-            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Right" TextAlignment="Left"  Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
-            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" TextAlignment="Left"  Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
-            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" HorizontalAlignment="Right" TextAlignment="Left"  Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" />
+            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Center" VerticalAlignment="Center"  Foreground="{DynamicResource CatalogBaseHighColor}" />
+            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" TextAlignment="Left"  Foreground="{DynamicResource CatalogBaseHighColor}" />
+            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Right" TextAlignment="Left"  Foreground="{DynamicResource CatalogBaseHighColor}" />
+            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" TextAlignment="Left"  Foreground="{DynamicResource CatalogBaseHighColor}" />
+            <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" HorizontalAlignment="Right" TextAlignment="Left"  Foreground="{DynamicResource CatalogBaseHighColor}" />
           </Grid>
 
         </SplitView>

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

@@ -9,7 +9,7 @@
       <WrapPanel.Styles>
         <Style Selector="Border">
           <Setter Property="BorderThickness" Value="1" />
-          <Setter Property="BorderBrush" Value="{DynamicResource SystemControlHighlightBaseMediumLowBrush}" />
+          <Setter Property="BorderBrush" Value="{DynamicResource CatalogBaseMediumColor}" />
           <Setter Property="Padding" Value="2" />
           <Setter Property="Margin" Value="10" />
           <Setter Property="Width" Value="200" />

+ 79 - 0
samples/ControlCatalog/Pages/ThemePage.axaml

@@ -0,0 +1,79 @@
+<UserControl x:Class="ControlCatalog.Pages.ThemePage"
+             xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:pages="clr-namespace:ControlCatalog.Pages"
+             d:DesignWidth="300"
+             mc:Ignorable="d">
+
+  <UserControl.Resources>
+    <ResourceDictionary>
+      <ResourceDictionary.ThemeDictionaries>
+        <ResourceDictionary x:Key="Dark">
+          <SolidColorBrush x:Key="DemoBackground">Black</SolidColorBrush>
+        </ResourceDictionary>
+        <ResourceDictionary x:Key="Light">
+          <SolidColorBrush x:Key="DemoBackground">White</SolidColorBrush>
+        </ResourceDictionary>
+        <ResourceDictionary x:Key="{x:Static pages:ThemePage.Pink}">
+          <SolidColorBrush x:Key="DemoBackground">#ffe5ea</SolidColorBrush>
+          <SolidColorBrush x:Key="NormalBackgroundBrush" Color="#ffc0cb" />
+          <SolidColorBrush x:Key="PointerOverBackgroundBrush" Color="#ffb3c0" />
+          <SolidColorBrush x:Key="PressedBackgroundBrush" Color="#ff4d6c" />
+          <SolidColorBrush x:Key="NormalBorderBrush" Color="#ff8096" />
+          <SolidColorBrush x:Key="PointerOverBorderBrush" Color="#ff8096" />
+          <SolidColorBrush x:Key="PressedBorderBrush" Color="#ff4d6c" />
+
+          <!-- Override colors for fluent theme -->
+          <StaticResource x:Key="ButtonBackground" ResourceKey="NormalBackgroundBrush" />
+          <StaticResource x:Key="ButtonBackgroundPointerOver" ResourceKey="PointerOverBackgroundBrush" />
+          <StaticResource x:Key="ButtonBackgroundPressed" ResourceKey="PressedBackgroundBrush" />
+          <StaticResource x:Key="ButtonBorderBrush" ResourceKey="NormalBorderBrush" />
+          <StaticResource x:Key="ButtonBorderBrushPointerOver" ResourceKey="PointerOverBorderBrush" />
+          <StaticResource x:Key="ButtonBorderBrushPressed" ResourceKey="PressedBorderBrush" />
+          <StaticResource x:Key="TextControlBackground" ResourceKey="NormalBackgroundBrush" />
+          <StaticResource x:Key="TextControlBackgroundPointerOver" ResourceKey="PointerOverBackgroundBrush" />
+          <StaticResource x:Key="TextControlBackgroundFocused" ResourceKey="PointerOverBackgroundBrush" />
+          <StaticResource x:Key="TextControlBorderBrush" ResourceKey="NormalBorderBrush" />
+          <StaticResource x:Key="TextControlBorderBrushPointerOver" ResourceKey="PointerOverBorderBrush" />
+          <StaticResource x:Key="TextControlBorderBrushFocused" ResourceKey="PressedBorderBrush" />
+          <StaticResource x:Key="ComboBoxBackground" ResourceKey="NormalBackgroundBrush" />
+          <StaticResource x:Key="ComboBoxBackgroundPointerOver" ResourceKey="PointerOverBackgroundBrush" />
+          <StaticResource x:Key="ComboBoxBackgroundPressed" ResourceKey="PressedBackgroundBrush" />
+          <StaticResource x:Key="ComboBoxBorderBrush" ResourceKey="NormalBorderBrush" />
+          <StaticResource x:Key="ComboBoxBorderBrushPointerOver" ResourceKey="PointerOverBorderBrush" />
+          <StaticResource x:Key="ComboBoxBorderBrushPressed" ResourceKey="PressedBorderBrush" />
+          <!-- Override colors for default theme -->
+          <StaticResource x:Key="ThemeControlMidBrush" ResourceKey="NormalBackgroundBrush" />
+          <StaticResource x:Key="ThemeControlHighBrush" ResourceKey="PressedBackgroundBrush" />
+          <StaticResource x:Key="ThemeBorderLowBrush" ResourceKey="NormalBorderBrush" />
+          <StaticResource x:Key="ThemeBorderMidBrush" ResourceKey="PointerOverBorderBrush" />
+        </ResourceDictionary>
+      </ResourceDictionary.ThemeDictionaries>
+    </ResourceDictionary>
+  </UserControl.Resources>
+
+  <ThemeVariantScope x:Name="ThemeVariantScope">
+    <Border Background="{DynamicResource DemoBackground}"
+            VerticalAlignment="Top"
+            HorizontalAlignment="Left"
+            Padding="4"
+            CornerRadius="4">
+      <Grid RowDefinitions="Auto, 4, Auto, 4, Auto, 4, Auto" ColumnDefinitions="150, 150">
+        <ComboBox Grid.Column="0" Grid.Row="0" x:Name="Selector" HorizontalAlignment="Stretch">
+          <ComboBox.ItemTemplate>
+            <DataTemplate x:DataType="x:String">
+              <TextBlock Text="{Binding TargetNullValue=Unset}" />
+            </DataTemplate>
+          </ComboBox.ItemTemplate>
+        </ComboBox>
+        <TextBlock Grid.Column="0" Grid.Row="2" Text="Username:" VerticalAlignment="Center" />
+        <TextBlock Grid.Column="0" Grid.Row="4" Text="Password:" VerticalAlignment="Center" />
+        <TextBox Grid.Column="1" Grid.Row="2" Watermark="Input here" HorizontalAlignment="Stretch" />
+        <TextBox Grid.Column="1" Grid.Row="4" Watermark="Input here" HorizontalAlignment="Stretch" />
+        <Button Grid.Column="1" Grid.Row="6" Content="Login" HorizontalAlignment="Stretch" />
+      </Grid>
+    </Border>
+  </ThemeVariantScope>
+</UserControl>

+ 37 - 0
samples/ControlCatalog/Pages/ThemePage.axaml.cs

@@ -0,0 +1,37 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Styling;
+
+namespace ControlCatalog.Pages
+{
+    public class ThemePage : UserControl
+    {
+        public static ThemeVariant Pink { get; } = new("Pink", ThemeVariant.Light);
+        
+        public ThemePage()
+        {
+            AvaloniaXamlLoader.Load(this);
+
+            var selector = this.FindControl<ComboBox>("Selector")!;
+            var themeVariantScope = this.FindControl<ThemeVariantScope>("ThemeVariantScope")!;
+
+            selector.Items = new[]
+            {
+                ThemeVariant.Default,
+                ThemeVariant.Dark,
+                ThemeVariant.Light,
+                Pink
+            };
+            selector.SelectedIndex = 0;
+
+            selector.SelectionChanged += (_, _) =>
+            {
+                if (selector.SelectedItem is ThemeVariant theme)
+                {
+                    themeVariantScope.RequestedThemeVariant = theme;
+                }
+            };
+        }
+    }
+}

+ 3 - 2
samples/GpuInterop/MainWindow.axaml.cs

@@ -1,6 +1,7 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using Avalonia.Rendering;
 
 namespace GpuInterop
 {
@@ -8,9 +9,9 @@ namespace GpuInterop
     {
         public MainWindow()
         {
-            this.InitializeComponent();
+            InitializeComponent();
             this.AttachDevTools();
-            this.Renderer.DrawFps = true;
+            Renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.Fps;
         }
 
         private void InitializeComponent()

+ 3 - 3
samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs

@@ -38,8 +38,8 @@ static class VulkanBufferHelper
             MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api,
                 physicalDevice,
                 memoryRequirements.MemoryTypeBits,
-                MemoryPropertyFlags.MemoryPropertyHostCoherentBit |
-                MemoryPropertyFlags.MemoryPropertyHostVisibleBit)
+                MemoryPropertyFlags.HostCoherentBit |
+                MemoryPropertyFlags.HostVisibleBit)
         };
 
         api.AllocateMemory(device, memoryAllocateInfo, null, out memory).ThrowOnError();
@@ -77,4 +77,4 @@ static class VulkanBufferHelper
 
         return -1;
     }
-}
+}

+ 3 - 4
samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using Avalonia.Input;
 using Silk.NET.Vulkan;
 using SilkNetDemo;
 
@@ -25,7 +24,7 @@ namespace Avalonia.Vulkan
             var commandPoolCreateInfo = new CommandPoolCreateInfo
             {
                 SType = StructureType.CommandPoolCreateInfo,
-                Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit,
+                Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
                 QueueFamilyIndex = queueFamilyIndex
             };
 
@@ -109,7 +108,7 @@ namespace Avalonia.Vulkan
                 var fenceCreateInfo = new FenceCreateInfo()
                 {
                     SType = StructureType.FenceCreateInfo,
-                    Flags = FenceCreateFlags.FenceCreateSignaledBit
+                    Flags = FenceCreateFlags.SignaledBit
                 };
 
                 api.CreateFence(device, fenceCreateInfo, null, out _fence);
@@ -134,7 +133,7 @@ namespace Avalonia.Vulkan
                     var beginInfo = new CommandBufferBeginInfo
                     {
                         SType = StructureType.CommandBufferBeginInfo,
-                        Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit
+                        Flags = CommandBufferUsageFlags.OneTimeSubmitBit
                     };
 
                     _api.BeginCommandBuffer(InternalHandle, beginInfo);

+ 25 - 25
samples/GpuInterop/VulkanDemo/VulkanContent.cs

@@ -208,7 +208,7 @@ unsafe class VulkanContent : IDisposable
         api.CmdBindDescriptorSets(commandBufferHandle, PipelineBindPoint.Graphics,
             _pipelineLayout,0,1, &dset, null);
 
-        api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.ShaderStageVertexBit | ShaderStageFlags.FragmentBit, 0,
+        api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, 0,
             (uint)Marshal.SizeOf<VertextPushConstant>(), &vertexConstant);
         api.CmdBindVertexBuffers(commandBufferHandle, 0, 1, _vertexBuffer, 0);
         api.CmdBindIndexBuffer(commandBufferHandle, _indexBuffer, 0, IndexType.Uint16);
@@ -237,14 +237,14 @@ unsafe class VulkanContent : IDisposable
             SrcSubresource =
                 new ImageSubresourceLayers
                 {
-                    AspectMask = ImageAspectFlags.ImageAspectColorBit,
+                    AspectMask = ImageAspectFlags.ColorBit,
                     BaseArrayLayer = 0,
                     LayerCount = 1,
                     MipLevel = 0
                 },
             DstSubresource = new ImageSubresourceLayers
             {
-                AspectMask = ImageAspectFlags.ImageAspectColorBit,
+                AspectMask = ImageAspectFlags.ColorBit,
                 BaseArrayLayer = 0,
                 LayerCount = 1,
                 MipLevel = 0
@@ -326,19 +326,19 @@ unsafe class VulkanContent : IDisposable
         var imageCreateInfo = new ImageCreateInfo
         {
             SType = StructureType.ImageCreateInfo,
-            ImageType = ImageType.ImageType2D,
+            ImageType = ImageType.Type2D,
             Format = Format.D32Sfloat,
             Extent =
                 new Extent3D((uint?)size.Width,
                     (uint?)size.Height, 1),
             MipLevels = 1,
             ArrayLayers = 1,
-            Samples = SampleCountFlags.SampleCount1Bit,
+            Samples = SampleCountFlags.Count1Bit,
             Tiling = ImageTiling.Optimal,
-            Usage = ImageUsageFlags.ImageUsageDepthStencilAttachmentBit,
+            Usage = ImageUsageFlags.DepthStencilAttachmentBit,
             SharingMode = SharingMode.Exclusive,
             InitialLayout = ImageLayout.Undefined,
-            Flags = ImageCreateFlags.ImageCreateMutableFormatBit
+            Flags = ImageCreateFlags.CreateMutableFormatBit
         };
 
         var api = _context.Api;
@@ -355,7 +355,7 @@ unsafe class VulkanContent : IDisposable
             AllocationSize = memoryRequirements.Size,
             MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api,
                 _context.PhysicalDevice,
-                memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit)
+                memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit)
         };
 
         api.AllocateMemory(device, memoryAllocateInfo, null,
@@ -369,14 +369,14 @@ unsafe class VulkanContent : IDisposable
             ComponentSwizzle.B,
             ComponentSwizzle.A);
 
-        var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectDepthBit,
+        var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.DepthBit,
             0, 1, 0, 1);
 
         var imageViewCreateInfo = new ImageViewCreateInfo
         {
             SType = StructureType.ImageViewCreateInfo,
             Image = _depthImage,
-            ViewType = ImageViewType.ImageViewType2D,
+            ViewType = ImageViewType.Type2D,
             Format = Format.D32Sfloat,
             Components = componentMapping,
             SubresourceRange = subresourceRange
@@ -406,7 +406,7 @@ unsafe class VulkanContent : IDisposable
         var colorAttachment = new AttachmentDescription()
         {
             Format = Format.R8G8B8A8Unorm,
-            Samples = SampleCountFlags.SampleCount1Bit,
+            Samples = SampleCountFlags.Count1Bit,
             LoadOp = AttachmentLoadOp.Clear,
             StoreOp = AttachmentStoreOp.Store,
             InitialLayout = ImageLayout.Undefined,
@@ -418,7 +418,7 @@ unsafe class VulkanContent : IDisposable
         var depthAttachment = new AttachmentDescription()
         {
             Format = Format.D32Sfloat,
-            Samples = SampleCountFlags.SampleCount1Bit,
+            Samples = SampleCountFlags.Count1Bit,
             LoadOp = AttachmentLoadOp.Clear,
             StoreOp = AttachmentStoreOp.DontCare,
             InitialLayout = ImageLayout.Undefined,
@@ -431,10 +431,10 @@ unsafe class VulkanContent : IDisposable
         {
             SrcSubpass = Vk.SubpassExternal,
             DstSubpass = 0,
-            SrcStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit,
+            SrcStageMask = PipelineStageFlags.ColorAttachmentOutputBit,
             SrcAccessMask = 0,
-            DstStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit,
-            DstAccessMask = AccessFlags.AccessColorAttachmentWriteBit
+            DstStageMask = PipelineStageFlags.ColorAttachmentOutputBit,
+            DstAccessMask = AccessFlags.ColorAttachmentWriteBit
         };
 
         var colorAttachmentReference = new AttachmentReference()
@@ -498,14 +498,14 @@ unsafe class VulkanContent : IDisposable
         var vertShaderStageInfo = new PipelineShaderStageCreateInfo()
         {
             SType = StructureType.PipelineShaderStageCreateInfo,
-            Stage = ShaderStageFlags.ShaderStageVertexBit,
+            Stage = ShaderStageFlags.VertexBit,
             Module = _vertShader,
             PName = (byte*)pname,
         };
         var fragShaderStageInfo = new PipelineShaderStageCreateInfo()
         {
             SType = StructureType.PipelineShaderStageCreateInfo,
-            Stage = ShaderStageFlags.ShaderStageFragmentBit,
+            Stage = ShaderStageFlags.FragmentBit,
             Module = _fragShader,
             PName = (byte*)pname,
         };
@@ -564,7 +564,7 @@ unsafe class VulkanContent : IDisposable
                 RasterizerDiscardEnable = false,
                 PolygonMode = PolygonMode.Fill,
                 LineWidth = 1,
-                CullMode = CullModeFlags.CullModeNone,
+                CullMode = CullModeFlags.None,
                 DepthBiasEnable = false
             };
 
@@ -572,7 +572,7 @@ unsafe class VulkanContent : IDisposable
             {
                 SType = StructureType.PipelineMultisampleStateCreateInfo,
                 SampleShadingEnable = false,
-                RasterizationSamples = SampleCountFlags.SampleCount1Bit
+                RasterizationSamples = SampleCountFlags.Count1Bit
             };
 
             var depthStencilCreateInfo = new PipelineDepthStencilStateCreateInfo()
@@ -587,10 +587,10 @@ unsafe class VulkanContent : IDisposable
 
             var colorBlendAttachmentState = new PipelineColorBlendAttachmentState()
             {
-                ColorWriteMask = ColorComponentFlags.ColorComponentABit |
-                                 ColorComponentFlags.ColorComponentRBit |
-                                 ColorComponentFlags.ColorComponentGBit |
-                                 ColorComponentFlags.ColorComponentBBit,
+                ColorWriteMask = ColorComponentFlags.ABit |
+                                 ColorComponentFlags.RBit |
+                                 ColorComponentFlags.GBit |
+                                 ColorComponentFlags.BBit,
                 BlendEnable = false
             };
 
@@ -617,14 +617,14 @@ unsafe class VulkanContent : IDisposable
                 {
                     Offset = 0,
                     Size = (uint)Marshal.SizeOf<VertextPushConstant>(),
-                    StageFlags = ShaderStageFlags.ShaderStageVertexBit
+                    StageFlags = ShaderStageFlags.VertexBit
                 };
 
                 var fragPushConstantRange = new PushConstantRange()
                 {
                     //Offset = vertexPushConstantRange.Size,
                     Size = (uint)Marshal.SizeOf<VertextPushConstant>(),
-                    StageFlags = ShaderStageFlags.ShaderStageFragmentBit
+                    StageFlags = ShaderStageFlags.FragmentBit
                 };
 
                 var layoutBindingInfo = new DescriptorSetLayoutBinding

+ 6 - 6
samples/GpuInterop/VulkanDemo/VulkanContext.cs

@@ -86,12 +86,12 @@ public unsafe class VulkanContext : IDisposable
                 var debugCreateInfo = new DebugUtilsMessengerCreateInfoEXT
                 {
                     SType = StructureType.DebugUtilsMessengerCreateInfoExt,
-                    MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityVerboseBitExt |
-                                      DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityWarningBitExt |
-                                      DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityErrorBitExt,
-                    MessageType = DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeGeneralBitExt |
-                                  DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeValidationBitExt |
-                                  DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypePerformanceBitExt,
+                    MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.VerboseBitExt |
+                                      DebugUtilsMessageSeverityFlagsEXT.WarningBitExt |
+                                      DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt,
+                    MessageType = DebugUtilsMessageTypeFlagsEXT.GeneralBitExt |
+                                  DebugUtilsMessageTypeFlagsEXT.ValidationBitExt |
+                                  DebugUtilsMessageTypeFlagsEXT.PerformanceBitExt,
                     PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(LogCallback),
                 };
 

+ 0 - 12
samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs

@@ -1,24 +1,12 @@
 using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Avalonia;
-using Avalonia.Platform;
 using Avalonia.Rendering.Composition;
-using Silk.NET.Core;
-using Silk.NET.Vulkan;
-using Silk.NET.Vulkan.Extensions.KHR;
-using SilkNetDemo;
 
 namespace GpuInterop.VulkanDemo;
 
 public class VulkanDemoControl : DrawingSurfaceDemoBase
 {
-    private Instance _vkInstance;
-    private Vk _api;
-
     class VulkanResources : IAsyncDisposable
     {
         public VulkanContext Context { get; }

+ 9 - 9
samples/GpuInterop/VulkanDemo/VulkanImage.cs

@@ -54,8 +54,8 @@ public unsafe class VulkanImage : IDisposable
             Size = size;
             MipLevels = 1;//mipLevels;
             _imageUsageFlags =
-                ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit |
-                ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit;
+                ImageUsageFlags.ColorAttachmentBit | ImageUsageFlags.TransferDstBit |
+                ImageUsageFlags.TransferSrcBit | ImageUsageFlags.SampledBit;
             
             //MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2));
 
@@ -72,19 +72,19 @@ public unsafe class VulkanImage : IDisposable
             {
                 PNext = exportable ? &externalMemoryCreateInfo : null,
                 SType = StructureType.ImageCreateInfo,
-                ImageType = ImageType.ImageType2D,
+                ImageType = ImageType.Type2D,
                 Format = Format,
                 Extent =
                     new Extent3D((uint?)Size.Width,
                         (uint?)Size.Height, 1),
                 MipLevels = MipLevels,
                 ArrayLayers = 1,
-                Samples = SampleCountFlags.SampleCount1Bit,
+                Samples = SampleCountFlags.Count1Bit,
                 Tiling = Tiling,
                 Usage = _imageUsageFlags,
                 SharingMode = SharingMode.Exclusive,
                 InitialLayout = ImageLayout.Undefined,
-                Flags = ImageCreateFlags.ImageCreateMutableFormatBit
+                Flags = ImageCreateFlags.CreateMutableFormatBit
             };
 
             Api
@@ -128,7 +128,7 @@ public unsafe class VulkanImage : IDisposable
                 MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(
                     Api,
                     _physicalDevice,
-                    memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit)
+                    memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit)
             };
 
             Api.AllocateMemory(_device, memoryAllocateInfo, null,
@@ -146,7 +146,7 @@ public unsafe class VulkanImage : IDisposable
                 ComponentSwizzle.Identity,
                 ComponentSwizzle.Identity);
 
-            AspectFlags = ImageAspectFlags.ImageAspectColorBit;
+            AspectFlags = ImageAspectFlags.ColorBit;
 
             var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1);
 
@@ -154,7 +154,7 @@ public unsafe class VulkanImage : IDisposable
             {
                 SType = StructureType.ImageViewCreateInfo,
                 Image = InternalHandle.Value,
-                ViewType = ImageViewType.ImageViewType2D,
+                ViewType = ImageViewType.Type2D,
                 Format = Format,
                 Components = componentMapping,
                 SubresourceRange = subresourceRange
@@ -168,7 +168,7 @@ public unsafe class VulkanImage : IDisposable
 
             _currentLayout = ImageLayout.Undefined;
 
-            TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
+            TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.NoneKhr);
         }
 
         public int ExportFd()

+ 4 - 4
samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs

@@ -29,7 +29,7 @@ internal static class VulkanMemoryHelper
         AccessFlags destinationAccessMask,
         uint mipLevels)
     {
-        var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1);
+        var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, mipLevels, 0, 1);
 
         var barrier = new ImageMemoryBarrier
         {
@@ -46,8 +46,8 @@ internal static class VulkanMemoryHelper
 
         api.CmdPipelineBarrier(
             commandBuffer,
-            PipelineStageFlags.PipelineStageAllCommandsBit,
-            PipelineStageFlags.PipelineStageAllCommandsBit,
+            PipelineStageFlags.AllCommandsBit,
+            PipelineStageFlags.AllCommandsBit,
             0,
             0,
             null,
@@ -56,4 +56,4 @@ internal static class VulkanMemoryHelper
             1,
             barrier);
     }
-}
+}

+ 1 - 4
samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs

@@ -1,5 +1,4 @@
 using System;
-using System.IO;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Avalonia;
@@ -7,9 +6,7 @@ using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
 using Avalonia.Vulkan;
-using Metsys.Bson;
 using Silk.NET.Vulkan;
-using SkiaSharp;
 
 namespace GpuInterop.VulkanDemo;
 
@@ -84,7 +81,7 @@ class VulkanSwapchainImage : ISwapchainImage
 
         _image.TransitionLayout(buffer.InternalHandle, 
             ImageLayout.Undefined, AccessFlags.None,
-            ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessColorAttachmentReadBit);
+            ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentReadBit);
 
         if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
             buffer.Submit(null,null,null, null, new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo

+ 1 - 1
samples/IntegrationTestApp/App.axaml

@@ -2,6 +2,6 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="IntegrationTestApp.App">
     <Application.Styles>
-        <FluentTheme Mode="Light"/>
+        <FluentTheme />
     </Application.Styles>
 </Application>

+ 30 - 24
samples/IntegrationTestApp/MainWindow.axaml

@@ -120,30 +120,36 @@
       </TabItem>
       
       <TabItem Header="Window">
-        <StackPanel>
-          <TextBox Name="ShowWindowSize" Watermark="Window Size"/>
-          <ComboBox Name="ShowWindowMode" SelectedIndex="0">
-            <ComboBoxItem>NonOwned</ComboBoxItem>
-            <ComboBoxItem>Owned</ComboBoxItem>
-            <ComboBoxItem>Modal</ComboBoxItem>
-          </ComboBox>
-          <ComboBox Name="ShowWindowLocation" SelectedIndex="0">
-            <ComboBoxItem>Manual</ComboBoxItem>
-            <ComboBoxItem>CenterScreen</ComboBoxItem>
-            <ComboBoxItem>CenterOwner</ComboBoxItem>
-          </ComboBox>
-          <ComboBox Name="ShowWindowState" SelectedIndex="0">
-            <ComboBoxItem Name="ShowWindowStateNormal">Normal</ComboBoxItem>
-            <ComboBoxItem Name="ShowWindowStateMinimized">Minimized</ComboBoxItem>
-            <ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
-            <ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
-          </ComboBox>
-          <Button Name="ShowWindow">Show Window</Button>
-          <Button Name="SendToBack">Send to Back</Button>
-          <Button Name="EnterFullscreen">Enter Fullscreen</Button>
-          <Button Name="ExitFullscreen">Exit Fullscreen</Button>
-          <Button Name="RestoreAll">Restore All</Button>
-        </StackPanel>
+        <Grid ColumnDefinitions="*,8,*">
+          <StackPanel Grid.Column="0">
+            <TextBox Name="ShowWindowSize" Watermark="Window Size"/>
+            <ComboBox Name="ShowWindowMode" SelectedIndex="0">
+              <ComboBoxItem>NonOwned</ComboBoxItem>
+              <ComboBoxItem>Owned</ComboBoxItem>
+              <ComboBoxItem>Modal</ComboBoxItem>
+            </ComboBox>
+            <ComboBox Name="ShowWindowLocation" SelectedIndex="0">
+              <ComboBoxItem>Manual</ComboBoxItem>
+              <ComboBoxItem>CenterScreen</ComboBoxItem>
+              <ComboBoxItem>CenterOwner</ComboBoxItem>
+            </ComboBox>
+            <ComboBox Name="ShowWindowState" SelectedIndex="0">
+              <ComboBoxItem Name="ShowWindowStateNormal">Normal</ComboBoxItem>
+              <ComboBoxItem Name="ShowWindowStateMinimized">Minimized</ComboBoxItem>
+              <ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
+              <ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
+            </ComboBox>
+            <Button Name="ShowWindow">Show Window</Button>
+            <Button Name="SendToBack">Send to Back</Button>
+            <Button Name="EnterFullscreen">Enter Fullscreen</Button>
+            <Button Name="ExitFullscreen">Exit Fullscreen</Button>
+            <Button Name="RestoreAll">Restore All</Button>
+          </StackPanel>
+          <StackPanel Grid.Column="2">
+            <Button Name="ShowTransparentWindow">Transparent Window</Button>
+            <Button Name="ShowTransparentPopup">Transparent Popup</Button>
+          </StackPanel>
+        </Grid>
       </TabItem>
     </TabControl>
   </DockPanel>

+ 91 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -7,9 +7,13 @@ using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.Media;
 using Avalonia.Markup.Xaml;
 using Avalonia.VisualTree;
 using Microsoft.CodeAnalysis;
+using Avalonia.Controls.Primitives;
+using Avalonia.Threading;
+using Avalonia.Controls.Primitives.PopupPositioning;
 
 namespace IntegrationTestApp
 {
@@ -103,6 +107,89 @@ namespace IntegrationTestApp
             }
         }
 
+        private void ShowTransparentWindow()
+        {
+            // Show a background window to make sure the color behind the transparent window is
+            // a known color (green).
+            var backgroundWindow = new Window
+            {
+                Title = "Transparent Window Background",
+                Name = "TransparentWindowBackground",
+                Width = 300,
+                Height = 300,
+                Background = Brushes.Green,
+                WindowStartupLocation = WindowStartupLocation.CenterOwner,
+            };
+
+            // This is the transparent window with a red circle.
+            var window = new Window
+            {
+                Title = "Transparent Window",
+                Name = "TransparentWindow",
+                SystemDecorations = SystemDecorations.None,
+                Background = Brushes.Transparent,
+                TransparencyLevelHint = WindowTransparencyLevel.Transparent,
+                WindowStartupLocation = WindowStartupLocation.CenterOwner,
+                Width = 200,
+                Height = 200,
+                Content = new Border
+                {
+                    Background = Brushes.Red,
+                    CornerRadius = new CornerRadius(100),
+                }
+            };
+
+            window.PointerPressed += (_, _) =>
+            {
+                window.Close();
+                backgroundWindow.Close();
+            };
+
+            backgroundWindow.Show(this);
+            window.Show(backgroundWindow);
+        }
+
+        private void ShowTransparentPopup()
+        {
+            var popup = new Popup
+            {
+                WindowManagerAddShadowHint = false,
+                PlacementMode = PlacementMode.AnchorAndGravity,
+                PlacementAnchor = PopupAnchor.Top,
+                PlacementGravity = PopupGravity.Bottom,
+                Width= 200,
+                Height= 200,
+                Child = new Border
+                {
+                    Background = Brushes.Red,
+                    CornerRadius = new CornerRadius(100),
+                }
+            };
+
+            // Show a background window to make sure the color behind the transparent window is
+            // a known color (green).
+            var backgroundWindow = new Window
+            {
+                Title = "Transparent Popup Background",
+                Name = "TransparentPopupBackground",
+                Width = 200,
+                Height = 200,
+                Background = Brushes.Green,
+                WindowStartupLocation = WindowStartupLocation.CenterOwner,
+                Content = new Border
+                {
+                    Name = "PopupContainer",
+                    Child = popup,
+                    [AutomationProperties.AccessibilityViewProperty] = AccessibilityView.Content,
+                }
+            };
+
+            backgroundWindow.PointerPressed += (_, _) => backgroundWindow.Close();
+            backgroundWindow.Show(this);
+
+            popup.Open();
+        }
+
         private void SendToBack()
         {
             var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
@@ -175,6 +262,10 @@ namespace IntegrationTestApp
                 this.Get<ListBox>("BasicListBox").SelectedIndex = -1;
             if (source?.Name == "MenuClickedMenuItemReset")
                 this.Get<TextBlock>("ClickedMenuItem").Text = "None";
+            if (source?.Name == "ShowTransparentWindow")
+                ShowTransparentWindow();
+            if (source?.Name == "ShowTransparentPopup")
+                ShowTransparentPopup();
             if (source?.Name == "ShowWindow")
                 ShowWindow();
             if (source?.Name == "SendToBack")

+ 0 - 1
samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj

@@ -24,7 +24,6 @@
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\MobileSandbox\MobileSandbox.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
-    <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
     <!-- For native controls test -->
     <PackageReference Include="MonoMac.NetStandard" Version="0.0.4" />
   </ItemGroup>

+ 3 - 2
samples/MobileSandbox/App.xaml

@@ -1,8 +1,9 @@
 <Application xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              Name="Mobile Sandbox"
-             x:Class="MobileSandbox.App">
+             x:Class="MobileSandbox.App"
+             RequestedThemeVariant="Dark">
   <Application.Styles>
-    <FluentTheme Mode="Dark" />
+    <FluentTheme />
   </Application.Styles>
 </Application>

+ 1 - 1
samples/PlatformSanityChecks/App.xaml

@@ -1,5 +1,5 @@
 <Application xmlns="https://github.com/avaloniaui">
     <Application.Styles>
-        <SimpleTheme Mode="Light" />
+        <SimpleTheme />
     </Application.Styles>
 </Application>

+ 1 - 1
samples/Previewer/App.xaml

@@ -1,5 +1,5 @@
 <Application xmlns="https://github.com/avaloniaui">
     <Application.Styles>
-        <SimpleTheme Mode="Light" />
+        <SimpleTheme />
     </Application.Styles>
 </Application>

+ 14 - 0
samples/RenderDemo/MainWindow.xaml

@@ -26,6 +26,20 @@
                         IsHitTestVisible="False" />
             </MenuItem.Icon>
           </MenuItem>
+          <MenuItem Command="{Binding ToggleDrawLayoutTimeGraph}" Header="Draw layout time graph">
+            <MenuItem.Icon>
+              <CheckBox BorderThickness="0"
+                        IsChecked="{Binding DrawLayoutTimeGraph}"
+                        IsHitTestVisible="False" />
+            </MenuItem.Icon>
+          </MenuItem>
+          <MenuItem Command="{Binding ToggleDrawRenderTimeGraph}" Header="Draw render time graph">
+            <MenuItem.Icon>
+              <CheckBox BorderThickness="0"
+                        IsChecked="{Binding DrawRenderTimeGraph}"
+                        IsHitTestVisible="False" />
+            </MenuItem.Icon>
+          </MenuItem>
         </MenuItem>
         <MenuItem Header="Tests">
           <MenuItem Command="{Binding ResizeWindow}" Header="Resize window" />

+ 19 - 4
samples/RenderDemo/MainWindow.xaml.cs

@@ -1,7 +1,9 @@
 using System;
+using System.Linq.Expressions;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using Avalonia.Rendering;
 using RenderDemo.ViewModels;
 using MiniMvvm;
 
@@ -11,13 +13,26 @@ namespace RenderDemo
     {
         public MainWindow()
         {
-            this.InitializeComponent();
+            InitializeComponent();
             this.AttachDevTools();
 
             var vm = new MainWindowViewModel();
-            vm.WhenAnyValue(x => x.DrawDirtyRects).Subscribe(x => Renderer.DrawDirtyRects = x);
-            vm.WhenAnyValue(x => x.DrawFps).Subscribe(x => Renderer.DrawFps = x);
-            this.DataContext = vm;
+
+            void BindOverlay(Expression<Func<MainWindowViewModel, bool>> expr, RendererDebugOverlays overlay)
+                => vm.WhenAnyValue(expr).Subscribe(x =>
+                {
+                    var diagnostics = Renderer.Diagnostics;
+                    diagnostics.DebugOverlays = x ?
+                        diagnostics.DebugOverlays | overlay :
+                        diagnostics.DebugOverlays & ~overlay;
+                });
+
+            BindOverlay(x => x.DrawDirtyRects, RendererDebugOverlays.DirtyRects);
+            BindOverlay(x => x.DrawFps, RendererDebugOverlays.Fps);
+            BindOverlay(x => x.DrawLayoutTimeGraph, RendererDebugOverlays.LayoutTimeGraph);
+            BindOverlay(x => x.DrawRenderTimeGraph, RendererDebugOverlays.RenderTimeGraph);
+
+            DataContext = vm;
         }
 
         private void InitializeComponent()

+ 31 - 14
samples/RenderDemo/ViewModels/MainWindowViewModel.cs

@@ -1,49 +1,66 @@
-using System.Reactive;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
 using MiniMvvm;
 
 namespace RenderDemo.ViewModels
 {
     public class MainWindowViewModel : ViewModelBase
     {
-        private bool drawDirtyRects = false;
-        private bool drawFps = true;
-        private double width = 800;
-        private double height = 600;
+        private bool _drawDirtyRects;
+        private bool _drawFps = true;
+        private bool _drawLayoutTimeGraph;
+        private bool _drawRenderTimeGraph;
+        private double _width = 800;
+        private double _height = 600;
 
         public MainWindowViewModel()
         {
             ToggleDrawDirtyRects = MiniCommand.Create(() => DrawDirtyRects = !DrawDirtyRects);
             ToggleDrawFps = MiniCommand.Create(() => DrawFps = !DrawFps);
+            ToggleDrawLayoutTimeGraph = MiniCommand.Create(() => DrawLayoutTimeGraph = !DrawLayoutTimeGraph);
+            ToggleDrawRenderTimeGraph = MiniCommand.Create(() => DrawRenderTimeGraph = !DrawRenderTimeGraph);
             ResizeWindow = MiniCommand.CreateFromTask(ResizeWindowAsync);
         }
 
         public bool DrawDirtyRects
         {
-            get => drawDirtyRects;
-            set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value);
+            get => _drawDirtyRects;
+            set => RaiseAndSetIfChanged(ref _drawDirtyRects, value);
         }
 
         public bool DrawFps
         {
-            get => drawFps;
-            set => this.RaiseAndSetIfChanged(ref drawFps, value);
+            get => _drawFps;
+            set => RaiseAndSetIfChanged(ref _drawFps, value);
+        }
+
+        public bool DrawLayoutTimeGraph
+        {
+            get => _drawLayoutTimeGraph;
+            set => RaiseAndSetIfChanged(ref _drawLayoutTimeGraph, value);
+        }
+
+        public bool DrawRenderTimeGraph
+        {
+            get => _drawRenderTimeGraph;
+            set => RaiseAndSetIfChanged(ref _drawRenderTimeGraph, value);
         }
 
         public double Width
         {
-            get => width;
-            set => this.RaiseAndSetIfChanged(ref width, value);
+            get => _width;
+            set => RaiseAndSetIfChanged(ref _width, value);
         }
 
         public double Height
         {
-            get => height;
-            set => this.RaiseAndSetIfChanged(ref height, value);
+            get => _height;
+            set => RaiseAndSetIfChanged(ref _height, value);
         }
 
         public MiniCommand ToggleDrawDirtyRects { get; }
         public MiniCommand ToggleDrawFps { get; }
+        public MiniCommand ToggleDrawLayoutTimeGraph { get; }
+        public MiniCommand ToggleDrawRenderTimeGraph { get; }
         public MiniCommand ResizeWindow { get; }
 
         private async Task ResizeWindowAsync()

+ 10 - 2
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@@ -52,6 +52,14 @@ namespace ControlSamples
                 var (oldBounds, newBounds) = change.GetOldAndNewValue<Rect>();
                 EnsureSplitViewMode(oldBounds, newBounds);
             }
+
+            if (change.Property == SelectedItemProperty)
+            {
+                if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay)
+                {
+                    _splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation);
+                }
+            }
         }
 
         private void EnsureSplitViewMode(Rect oldBounds, Rect newBounds)
@@ -60,12 +68,12 @@ namespace ControlSamples
             {
                 var threshold = ExpandedModeThresholdWidth;
 
-                if (newBounds.Width >= threshold && oldBounds.Width < threshold)
+                if (newBounds.Width >= threshold)
                 {
                     _splitView.DisplayMode = SplitViewDisplayMode.Inline;
                     _splitView.IsPaneOpen = true;
                 }
-                else if (newBounds.Width < threshold && oldBounds.Width >= threshold)
+                else if (newBounds.Width < threshold)
                 {
                     _splitView.DisplayMode = SplitViewDisplayMode.Overlay;
                     _splitView.IsPaneOpen = false;

+ 24 - 10
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

@@ -20,6 +20,21 @@
     </Border>
   </Design.PreviewWith>
 
+  <ResourceDictionary.ThemeDictionaries>
+    <ResourceDictionary x:Key="Dark">
+      <Color x:Key="HamburgerBaseHighColor">#99FFFFFF</Color>
+      <Color x:Key="HamburgerChromeMediumColor">#FF1F1F1F</Color>
+      <Color x:Key="HamburgerAltHighColor">#FF000000</Color>
+      <Color x:Key="HamburgerChromeLowColor">#FF171717</Color>
+    </ResourceDictionary>
+    <ResourceDictionary x:Key="Default">
+      <Color x:Key="HamburgerBaseHighColor">#99000000</Color>
+      <Color x:Key="HamburgerChromeMediumColor">#FFE6E6E6</Color>
+      <Color x:Key="HamburgerAltHighColor">#FFFFFFFF</Color>
+      <Color x:Key="HamburgerChromeLowColor">#FFF2F2F2</Color>
+    </ResourceDictionary>
+  </ResourceDictionary.ThemeDictionaries>
+  
   <x:Double x:Key="PaneCompactWidth">40</x:Double>
   <x:Double x:Key="PaneExpandWidth">220</x:Double>
   <x:Double x:Key="HeaderHeight">36</x:Double>
@@ -36,7 +51,6 @@
     <Setter Property="VerticalContentAlignment" Value="Center" />
     <Setter Property="HorizontalAlignment" Value="Stretch" />
     <Setter Property="VerticalAlignment" Value="Stretch" />
-    <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
     <Setter Property="FontWeight" Value="Normal" />
     <Setter Property="MinHeight" Value="0" />
     <Setter Property="Height" Value="{StaticResource NavigationItemHeight}" />
@@ -64,7 +78,7 @@
     </Setter>
 
     <Style Selector="^:pointerover /template/ ContentPresenter">
-      <Setter Property="Border.Background" Value="{DynamicResource SystemChromeLowColor}" />
+      <Setter Property="Border.Background" Value="{DynamicResource HamburgerChromeLowColor}" />
       <Setter Property="Border.BoxShadow" Value="{StaticResource NavigationItemShadow}" />
       <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPointerOver}" />
     </Style>
@@ -101,7 +115,7 @@
                     VerticalAlignment="Center"
                     Background="{DynamicResource TabItemHeaderSelectedPipeFill}"
                     IsVisible="False"
-                    CornerRadius="{DynamicResource ControlCornerRadius}"/>
+                    CornerRadius="4"/>
             <ContentPresenter Name="PART_ContentPresenter"
                               Padding="{TemplateBinding Padding}"
                               Margin="0"
@@ -121,9 +135,9 @@
       <Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
       
       <Style Selector="^ /template/ Border#PART_LayoutRoot">
-        <Setter Property="Background" Value="{DynamicResource SystemChromeLowColor}" />
+        <Setter Property="Background" Value="{DynamicResource HamburgerChromeLowColor}" />
         <Setter Property="BoxShadow" Value="{StaticResource NavigationItemShadow}" />
-        <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPointerOver}" />
+        <Setter Property="TextElement.Foreground" Value="{DynamicResource HamburgerBaseHighColor}" />
       </Style>
     </Style>
     
@@ -136,18 +150,18 @@
     </Style>
 
     <Style Selector="^:pressed /template/ Border#PART_LayoutRoot">
-      <Setter Property="Border.Background" Value="{DynamicResource SystemChromeLowColor}" />
+      <Setter Property="Border.Background" Value="{DynamicResource HamburgerChromeLowColor}" />
       <Setter Property="Border.BoxShadow" Value="{StaticResource NavigationItemShadow}" />
-      <Setter Property="TextElement.Foreground" Value="{DynamicResource TabItemHeaderForegroundUnselectedPressed}" />
+      <Setter Property="TextElement.Foreground" Value="{DynamicResource HamburgerBaseHighColor}" />
     </Style>
   </ControlTheme>
 
   <!--  HamburgerMenu  -->
   <ControlTheme x:Key="{x:Type catalog:HamburgerMenu}" TargetType="catalog:HamburgerMenu">
     <Setter Property="Padding" Value="12 8 4 0" />
-    <Setter Property="PaneBackground" Value="{DynamicResource SystemChromeMediumColor}" />
-    <Setter Property="Background" Value="{DynamicResource SystemChromeMediumColor}" />
-    <Setter Property="ContentBackground" Value="{DynamicResource SystemAltHighColor}" />
+    <Setter Property="PaneBackground" Value="{DynamicResource HamburgerChromeMediumColor}" />
+    <Setter Property="Background" Value="{DynamicResource HamburgerChromeMediumColor}" />
+    <Setter Property="ContentBackground" Value="{DynamicResource HamburgerAltHighColor}" />
     <Setter Property="ItemContainerTheme" Value="{StaticResource HamburgerMenuTabItem}"/>
     <Setter Property="TabStripPlacement" Value="Left" />
     <Setter Property="Template">

+ 1 - 1
samples/Sandbox/App.axaml

@@ -3,6 +3,6 @@
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     x:Class="Sandbox.App">
     <Application.Styles>
-        <FluentTheme Mode="Dark"/>
+        <FluentTheme />
     </Application.Styles>
 </Application>

+ 27 - 12
src/Avalonia.Base/Animation/Animatable.cs

@@ -27,7 +27,11 @@ namespace Avalonia.Animation
             AvaloniaProperty.Register<Animatable, Transitions?>(nameof(Transitions));
 
         private bool _transitionsEnabled = true;
+        private bool _isSubscribedToTransitionsCollection = false;
         private Dictionary<ITransition, TransitionState>? _transitionState;
+        private NotifyCollectionChangedEventHandler? _collectionChanged;
+        private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler => 
+            _collectionChanged ??= TransitionsCollectionChanged;
 
         /// <summary>
         /// Gets or sets the clock which controls the animations on the control.
@@ -60,9 +64,14 @@ namespace Avalonia.Animation
             {
                 _transitionsEnabled = true;
 
-                if (Transitions is object)
+                if (Transitions is Transitions transitions)
                 {
-                    AddTransitions(Transitions);
+                    if (!_isSubscribedToTransitionsCollection)
+                    {
+                        _isSubscribedToTransitionsCollection = true;
+                        transitions.CollectionChanged += TransitionsCollectionChangedHandler;
+                    }
+                    AddTransitions(transitions);
                 }
             }
         }
@@ -72,7 +81,7 @@ namespace Avalonia.Animation
         /// </summary>
         /// <remarks>
         /// This method should not be called from user code, it will be called automatically by the framework
-        /// when a control is added to the visual tree.
+        /// when a control is removed from the visual tree.
         /// </remarks>
         protected void DisableTransitions()
         {
@@ -80,9 +89,14 @@ namespace Avalonia.Animation
             {
                 _transitionsEnabled = false;
 
-                if (Transitions is object)
+                if (Transitions is Transitions transitions)
                 {
-                    RemoveTransitions(Transitions);
+                    if (_isSubscribedToTransitionsCollection)
+                    {
+                        _isSubscribedToTransitionsCollection = false;
+                        transitions.CollectionChanged -= TransitionsCollectionChangedHandler;
+                    }
+                    RemoveTransitions(transitions);
                 }
             }
         }
@@ -109,7 +123,8 @@ namespace Avalonia.Animation
                         toAdd = newTransitions.Except(oldTransitions).ToList();
                     }
 
-                    newTransitions.CollectionChanged += TransitionsCollectionChanged;
+                    newTransitions.CollectionChanged += TransitionsCollectionChangedHandler;
+                    _isSubscribedToTransitionsCollection = true;
                     AddTransitions(toAdd);
                 }
 
@@ -122,19 +137,19 @@ namespace Avalonia.Animation
                         toRemove = oldTransitions.Except(newTransitions).ToList();
                     }
 
-                    oldTransitions.CollectionChanged -= TransitionsCollectionChanged;
+                    oldTransitions.CollectionChanged -= TransitionsCollectionChangedHandler;
                     RemoveTransitions(toRemove);
                 }
             }
             else if (_transitionsEnabled &&
-                     Transitions is object &&
+                     Transitions is Transitions transitions &&
                      _transitionState is object &&
                      !change.Property.IsDirect &&
                      change.Priority > BindingPriority.Animation)
             {
-                for (var i = Transitions.Count -1; i >= 0; --i)
+                for (var i = transitions.Count - 1; i >= 0; --i)
                 {
-                    var transition = Transitions[i];
+                    var transition = transitions[i];
 
                     if (transition.Property == change.Property &&
                         _transitionState.TryGetValue(transition, out var state))
@@ -154,11 +169,11 @@ namespace Avalonia.Animation
                             {
                                 oldValue = animatedValue;
                             }
-
+                            var clock = Clock ?? AvaloniaLocator.Current.GetRequiredService<IGlobalClock>();
                             state.Instance?.Dispose();
                             state.Instance = transition.Apply(
                                 this,
-                                Clock ?? AvaloniaLocator.Current.GetRequiredService<IGlobalClock>(),
+                                clock,
                                 oldValue,
                                 newValue);
                             return;

+ 4 - 7
src/Avalonia.Base/Animation/KeySpline.cs

@@ -79,15 +79,12 @@ namespace Avalonia.Animation
         /// <param name="culture">culture of the string</param>
         /// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
         /// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
-        public static KeySpline Parse(string value, CultureInfo culture)
+        public static KeySpline Parse(string value, CultureInfo? culture)
         {
-            if (culture is null)
-                culture = CultureInfo.InvariantCulture;
+            culture ??= CultureInfo.InvariantCulture;
 
-            using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\"."))
-            {
-                return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
-            }
+            using var tokenizer = new StringTokenizer(value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".");
+            return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
         }
 
         /// <summary>

+ 15 - 23
src/Avalonia.Base/AvaloniaObject.cs

@@ -152,7 +152,7 @@ namespace Avalonia
             property = property ?? throw new ArgumentNullException(nameof(property));
             VerifyAccess();
 
-            _values?.ClearLocalValue(property);
+            _values.ClearLocalValue(property);
         }
 
         /// <summary>
@@ -242,7 +242,14 @@ namespace Avalonia
             return registered.InvokeGetter(this);
         }
 
-        /// <inheritdoc/>
+        /// <summary>
+        /// Gets an <see cref="AvaloniaProperty"/> base value.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <remarks>
+        /// Gets the value of the property excluding animated values, otherwise <see cref="Optional{T}.Empty"/>.
+        /// Note that this method does not return property values that come from inherited or default values.
+        /// </remarks>
         public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
         {
             _ = property ?? throw new ArgumentNullException(nameof(property));
@@ -261,7 +268,7 @@ namespace Avalonia
 
             VerifyAccess();
 
-            return _values?.IsAnimating(property) ?? false;
+            return _values.IsAnimating(property);
         }
 
         /// <summary>
@@ -279,7 +286,7 @@ namespace Avalonia
 
             VerifyAccess();
 
-            return _values?.IsSet(property) ?? false;
+            return _values.IsSet(property);
         }
 
         /// <summary>
@@ -515,14 +522,12 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);
 
-        /// <inheritdoc/>
         internal void AddInheritanceChild(AvaloniaObject child)
         {
             _inheritanceChildren ??= new List<AvaloniaObject>();
             _inheritanceChildren.Add(child);
         }
-        
-        /// <inheritdoc/>
+
         internal void RemoveInheritanceChild(AvaloniaObject child)
         {
             _inheritanceChildren?.Remove(child);
@@ -541,24 +546,11 @@ namespace Avalonia
                 return new AvaloniaPropertyValue(
                     property,
                     GetValue(property),
-                    BindingPriority.Unset,
-                    "Local Value");
-            }
-            else if (_values != null)
-            {
-                var result = _values.GetDiagnostic(property);
-
-                if (result != null)
-                {
-                    return result;
-                }
+                    BindingPriority.LocalValue,
+                    null);
             }
 
-            return new AvaloniaPropertyValue(
-                property,
-                GetValue(property),
-                BindingPriority.Unset,
-                "Unset");
+            return _values.GetDiagnostic(property);
         }
 
         internal ValueStore GetValueStore() => _values;

+ 13 - 5
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@@ -14,11 +14,7 @@ namespace Avalonia.Collections
     /// </summary>
     /// <typeparam name="TKey">The type of the dictionary key.</typeparam>
     /// <typeparam name="TValue">The type of the dictionary value.</typeparam>
-    public class AvaloniaDictionary<TKey, TValue> : IDictionary<TKey, TValue>,
-        IDictionary,
-        INotifyCollectionChanged,
-        INotifyPropertyChanged
-            where TKey : notnull
+    public class AvaloniaDictionary<TKey, TValue> : IAvaloniaDictionary<TKey, TValue> where TKey : notnull
     {
         private Dictionary<TKey, TValue> _inner;
 
@@ -29,6 +25,14 @@ namespace Avalonia.Collections
         {
             _inner = new Dictionary<TKey, TValue>();
         }
+        
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvaloniaDictionary{TKey, TValue}"/> class.
+        /// </summary>
+        public AvaloniaDictionary(int capacity)
+        {
+            _inner = new Dictionary<TKey, TValue>(capacity);
+        }
 
         /// <summary>
         /// Occurs when the collection changes.
@@ -62,6 +66,10 @@ namespace Avalonia.Collections
 
         object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot;
 
+        IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => _inner.Keys;
+
+        IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => _inner.Values;
+
         /// <summary>
         /// Gets or sets the named resource.
         /// </summary>

+ 112 - 0
src/Avalonia.Base/Collections/AvaloniaDictionaryExtensions.cs

@@ -0,0 +1,112 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Reactive;
+
+namespace Avalonia.Collections
+{
+    /// <summary>
+    /// Defines extension methods for working with <see cref="AvaloniaList{T}"/>s.
+    /// </summary>
+    public static class AvaloniaDictionaryExtensions
+    {
+        /// <summary>
+        /// Invokes an action for each item in a collection and subsequently each item added or
+        /// removed from the collection.
+        /// </summary>
+        /// <typeparam name="TKey">The key type of the collection items.</typeparam>
+        /// <typeparam name="TValue">The value type of the collection items.</typeparam>
+        /// <param name="collection">The collection.</param>
+        /// <param name="added">
+        /// An action called initially for each item in the collection and subsequently for each
+        /// item added to the collection. The parameters passed are the index in the collection and
+        /// the item.
+        /// </param>
+        /// <param name="removed">
+        /// An action called for each item removed from the collection. The parameters passed are
+        /// the index in the collection and the item.
+        /// </param>
+        /// <param name="reset">
+        /// An action called when the collection is reset. This will be followed by calls to 
+        /// <paramref name="added"/> for each item present in the collection after the reset.
+        /// </param>
+        /// <param name="weakSubscription">
+        /// Indicates if a weak subscription should be used to track changes to the collection.
+        /// </param>
+        /// <returns>A disposable used to terminate the subscription.</returns>
+        internal static IDisposable ForEachItem<TKey, TValue>(
+            this IAvaloniaReadOnlyDictionary<TKey, TValue> collection,
+            Action<TKey, TValue> added,
+            Action<TKey, TValue> removed,
+            Action reset,
+            bool weakSubscription = false)
+            where TKey : notnull
+        {
+            void Add(IEnumerable items)
+            {
+                foreach (KeyValuePair<TKey, TValue> pair in items)
+                {
+                    added(pair.Key, pair.Value);
+                }
+            }
+
+            void Remove(IEnumerable items)
+            {
+                foreach (KeyValuePair<TKey, TValue> pair in items)
+                {
+                    removed(pair.Key, pair.Value);
+                }
+            }
+
+            NotifyCollectionChangedEventHandler handler = (_, e) =>
+            {
+                switch (e.Action)
+                {
+                    case NotifyCollectionChangedAction.Add:
+                        Add(e.NewItems!);
+                        break;
+
+                    case NotifyCollectionChangedAction.Move:
+                    case NotifyCollectionChangedAction.Replace:
+                        Remove(e.OldItems!);
+                        int newIndex = e.NewStartingIndex;
+                        if(newIndex > e.OldStartingIndex)
+                        {
+                            newIndex -= e.OldItems!.Count;
+                        }
+                        Add(e.NewItems!);
+                        break;
+
+                    case NotifyCollectionChangedAction.Remove:
+                        Remove(e.OldItems!);
+                        break;
+
+                    case NotifyCollectionChangedAction.Reset:
+                        if (reset == null)
+                        {
+                            throw new InvalidOperationException(
+                                "Reset called on collection without reset handler.");
+                        }
+
+                        reset();
+                        Add(collection);
+                        break;
+                }
+            };
+
+            Add(collection);
+
+            if (weakSubscription)
+            {
+                return collection.WeakSubscribe(handler);
+            }
+            else
+            {
+                collection.CollectionChanged += handler;
+
+                return Disposable.Create(() => collection.CollectionChanged -= handler);
+            }
+        }
+    }
+}

+ 13 - 0
src/Avalonia.Base/Collections/IAvaloniaDictionary.cs

@@ -0,0 +1,13 @@
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Avalonia.Collections
+{
+    public interface IAvaloniaDictionary<TKey, TValue>
+        : IDictionary<TKey, TValue>,
+        IAvaloniaReadOnlyDictionary<TKey, TValue>,
+        IDictionary
+        where TKey : notnull
+    {
+    }
+}

+ 14 - 0
src/Avalonia.Base/Collections/IAvaloniaReadOnlyDictionary.cs

@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+
+namespace Avalonia.Collections
+{
+    public interface IAvaloniaReadOnlyDictionary<TKey, TValue>
+        : IReadOnlyDictionary<TKey, TValue>,
+        INotifyCollectionChanged,
+        INotifyPropertyChanged
+        where TKey : notnull
+    {
+    }
+}

+ 6 - 0
src/Avalonia.Base/Controls/IResourceDictionary.cs

@@ -1,4 +1,5 @@
 using System.Collections.Generic;
+using Avalonia.Styling;
 
 #nullable enable
 
@@ -13,5 +14,10 @@ namespace Avalonia.Controls
         /// Gets a collection of child resource dictionaries.
         /// </summary>
         IList<IResourceProvider> MergedDictionaries { get; }
+
+        /// <summary>
+        /// Gets a collection of merged resource dictionaries that are specifically keyed and composed to address theme scenarios.
+        /// </summary>
+        IDictionary<ThemeVariant, IResourceProvider> ThemeDictionaries { get; }
     }
 }

+ 4 - 3
src/Avalonia.Base/Controls/IResourceNode.cs

@@ -1,5 +1,5 @@
-using System;
-using Avalonia.Metadata;
+using Avalonia.Metadata;
+using Avalonia.Styling;
 
 namespace Avalonia.Controls
 {
@@ -23,6 +23,7 @@ namespace Avalonia.Controls
         /// Tries to find a resource within the object.
         /// </summary>
         /// <param name="key">The resource key.</param>
+        /// <param name="theme">Theme used to select theme dictionary.</param>
         /// <param name="value">
         /// When this method returns, contains the value associated with the specified key,
         /// if the key is found; otherwise, null.
@@ -30,6 +31,6 @@ namespace Avalonia.Controls
         /// <returns>
         /// True if the resource if found, otherwise false.
         /// </returns>
-        bool TryGetResource(object key, out object? value);
+        bool TryGetResource(object key, ThemeVariant? theme, out object? value);
     }
 }

+ 85 - 6
src/Avalonia.Base/Controls/ResourceDictionary.cs

@@ -1,9 +1,12 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Templates;
+using Avalonia.Media;
+using Avalonia.Styling;
 
 namespace Avalonia.Controls
 {
@@ -15,6 +18,7 @@ namespace Avalonia.Controls
         private Dictionary<object, object?>? _inner;
         private IResourceHost? _owner;
         private AvaloniaList<IResourceProvider>? _mergedDictionaries;
+        private AvaloniaDictionary<ThemeVariant, IResourceProvider>? _themeDictionary;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
@@ -69,14 +73,14 @@ namespace Avalonia.Controls
                     _mergedDictionaries.ForEachItem(
                         x =>
                         {
-                            if (Owner is object)
+                            if (Owner is not null)
                             {
                                 x.AddOwner(Owner);
                             }
                         },
                         x =>
                         {
-                            if (Owner is object)
+                            if (Owner is not null)
                             {
                                 x.RemoveOwner(Owner);
                             }
@@ -88,6 +92,34 @@ namespace Avalonia.Controls
             }
         }
 
+        public IDictionary<ThemeVariant, IResourceProvider> ThemeDictionaries
+        {
+            get
+            {
+                if (_themeDictionary == null)
+                {
+                    _themeDictionary = new AvaloniaDictionary<ThemeVariant, IResourceProvider>(2);
+                    _themeDictionary.ForEachItem(
+                        (_, x) =>
+                        {
+                            if (Owner is not null)
+                            {
+                                x.AddOwner(Owner);
+                            }
+                        },
+                        (_, x) =>
+                        {
+                            if (Owner is not null)
+                            {
+                                x.RemoveOwner(Owner);
+                            }
+                        },
+                        () => throw new NotSupportedException("Dictionary reset not supported"));
+                }
+                return _themeDictionary;
+            }
+        }
+
         bool IResourceNode.HasResources
         {
             get
@@ -152,16 +184,47 @@ namespace Avalonia.Controls
             return false;
         }
 
-        public bool TryGetResource(object key, out object? value)
+        public bool TryGetResource(object key, ThemeVariant? theme, out object? value)
         {
             if (TryGetValue(key, out value))
                 return true;
 
+            if (_themeDictionary is not null)
+            {
+                IResourceProvider? themeResourceProvider;
+                if (theme is not null && theme != ThemeVariant.Default)
+                {
+                    if (_themeDictionary.TryGetValue(theme, out themeResourceProvider)
+                        && themeResourceProvider.TryGetResource(key, theme, out value))
+                    {
+                        return true;
+                    }
+
+                    var themeInherit = theme.InheritVariant;
+                    while (themeInherit is not null)
+                    {
+                        if (_themeDictionary.TryGetValue(themeInherit, out themeResourceProvider)
+                            && themeResourceProvider.TryGetResource(key, theme, out value))
+                        {
+                            return true;
+                        }
+        
+                        themeInherit = themeInherit.InheritVariant;
+                    }
+                }
+
+                if (_themeDictionary.TryGetValue(ThemeVariant.Default, out themeResourceProvider)
+                    && themeResourceProvider.TryGetResource(key, theme, out value))
+                {
+                    return true;
+                }
+            }
+
             if (_mergedDictionaries != null)
             {
                 for (var i = _mergedDictionaries.Count - 1; i >= 0; --i)
                 {
-                    if (_mergedDictionaries[i].TryGetResource(key, out value))
+                    if (_mergedDictionaries[i].TryGetResource(key, theme, out value))
                     {
                         return true;
                     }
@@ -248,7 +311,7 @@ namespace Avalonia.Controls
 
             var hasResources = _inner?.Count > 0;
             
-            if (_mergedDictionaries is object)
+            if (_mergedDictionaries is not null)
             {
                 foreach (var i in _mergedDictionaries)
                 {
@@ -256,6 +319,14 @@ namespace Avalonia.Controls
                     hasResources |= i.HasResources;
                 }
             }
+            if (_themeDictionary is not null)
+            {
+                foreach (var i in _themeDictionary.Values)
+                {
+                    i.AddOwner(owner);
+                    hasResources |= i.HasResources;
+                }
+            }
 
             if (hasResources)
             {
@@ -273,7 +344,7 @@ namespace Avalonia.Controls
 
                 var hasResources = _inner?.Count > 0;
 
-                if (_mergedDictionaries is object)
+                if (_mergedDictionaries is not null)
                 {
                     foreach (var i in _mergedDictionaries)
                     {
@@ -281,6 +352,14 @@ namespace Avalonia.Controls
                         hasResources |= i.HasResources;
                     }
                 }
+                if (_themeDictionary is not null)
+                {
+                    foreach (var i in _themeDictionary.Values)
+                    {
+                        i.RemoveOwner(owner);
+                        hasResources |= i.HasResources;
+                    }
+                }
 
                 if (hasResources)
                 {

+ 109 - 16
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@@ -1,6 +1,4 @@
 using System;
-using Avalonia.Data.Converters;
-using Avalonia.LogicalTree;
 using Avalonia.Reactive;
 using Avalonia.Styling;
 
@@ -41,21 +39,66 @@ namespace Avalonia.Controls
             control = control ?? throw new ArgumentNullException(nameof(control));
             key = key ?? throw new ArgumentNullException(nameof(key));
 
-            IResourceNode? current = control;
+            return control.TryFindResource(key, null, out value);
+        }
+
+        /// <summary>
+        /// Finds the specified resource by searching up the logical tree and then global styles.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <param name="theme">Theme used to select theme dictionary.</param>
+        /// <param name="key">The resource key.</param>
+        /// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
+        public static object? FindResource(this IResourceHost control, ThemeVariant? theme, object key)
+        {
+            control = control ?? throw new ArgumentNullException(nameof(control));
+            key = key ?? throw new ArgumentNullException(nameof(key));
+
+            if (control.TryFindResource(key, theme, out var value))
+            {
+                return value;
+            }
+
+            return AvaloniaProperty.UnsetValue;
+        }
+        
+        /// <summary>
+        /// Tries to the specified resource by searching up the logical tree and then global styles.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <param name="key">The resource key.</param>
+        /// <param name="theme">Theme used to select theme dictionary.</param>
+        /// <param name="value">On return, contains the resource if found, otherwise null.</param>
+        /// <returns>True if the resource was found; otherwise false.</returns>
+        public static bool TryFindResource(this IResourceHost control, object key, ThemeVariant? theme, out object? value)
+        {
+            control = control ?? throw new ArgumentNullException(nameof(control));
+            key = key ?? throw new ArgumentNullException(nameof(key));
+
+            IResourceHost? current = control;
 
             while (current != null)
             {
-                if (current.TryGetResource(key, out value))
+                if (current.TryGetResource(key, theme, out value))
                 {
                     return true;
                 }
 
-                current = (current as IStyleHost)?.StylingParent as IResourceNode;
+                current = (current as IStyleHost)?.StylingParent as IResourceHost;
             }
 
             value = null;
             return false;
         }
+        
+        /// <inheritdoc cref="IResourceNode.TryGetResource" />
+        public static bool TryGetResource(this IResourceHost control, object key, out object? value)
+        {
+            control = control ?? throw new ArgumentNullException(nameof(control));
+            key = key ?? throw new ArgumentNullException(nameof(key));
+
+            return control.TryGetResource(key, null, out value);
+        }
 
         public static IObservable<object?> GetResourceObservable(
             this IResourceHost control,
@@ -95,24 +138,49 @@ namespace Avalonia.Controls
             protected override void Initialize()
             {
                 _target.ResourcesChanged += ResourcesChanged;
+                if (_target is StyledElement themeStyleable)
+                {
+                    themeStyleable.PropertyChanged += PropertyChanged;
+                }
             }
 
             protected override void Deinitialize()
             {
                 _target.ResourcesChanged -= ResourcesChanged;
+                if (_target is StyledElement themeStyleable)
+                {
+                    themeStyleable.PropertyChanged -= PropertyChanged;
+                }
             }
 
             protected override void Subscribed(IObserver<object?> observer, bool first)
             {
-                observer.OnNext(Convert(_target.FindResource(_key)));
+                observer.OnNext(GetValue());
             }
 
             private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e)
             {
-                PublishNext(Convert(_target.FindResource(_key)));
+                PublishNext(GetValue());
+            }
+
+            private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+            {
+                if (e.Property == StyledElement.ActualThemeVariantProperty)
+                {
+                    PublishNext(GetValue());
+                }
             }
 
-            private object? Convert(object? value) => _converter?.Invoke(value) ?? value;
+            private object? GetValue()
+            {
+                if (_target is not StyledElement themeStyleable
+                    || !_target.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value))
+                {
+                    value = _target.FindResource(_key) ?? AvaloniaProperty.UnsetValue;
+                }
+
+                return _converter?.Invoke(value) ?? value;
+            }
         }
 
         private class FloatingResourceObservable : LightweightObservableBase<object?>
@@ -134,7 +202,7 @@ namespace Avalonia.Controls
                 _target.OwnerChanged += OwnerChanged;
                 _owner = _target.Owner;
 
-                if (_owner is object)
+                if (_owner is not null)
                 {
                     _owner.ResourcesChanged += ResourcesChanged;
                 }
@@ -148,43 +216,68 @@ namespace Avalonia.Controls
 
             protected override void Subscribed(IObserver<object?> observer, bool first)
             {
-                if (_target.Owner is object)
+                if (_target.Owner is not null)
                 {
-                    observer.OnNext(Convert(_target.Owner.FindResource(_key)));
+                    observer.OnNext(GetValue());
                 }
             }
 
             private void PublishNext()
             {
-                if (_target.Owner is object)
+                if (_target.Owner is not null)
                 {
-                    PublishNext(Convert(_target.Owner.FindResource(_key)));
+                    PublishNext(GetValue());
                 }
             }
 
             private void OwnerChanged(object? sender, EventArgs e)
             {
-                if (_owner is object)
+                if (_owner is not null)
                 {
                     _owner.ResourcesChanged -= ResourcesChanged;
                 }
+                if (_owner is StyledElement styleable)
+                {
+                    styleable.PropertyChanged += PropertyChanged;
+                }
 
                 _owner = _target.Owner;
 
-                if (_owner is object)
+                if (_owner is not null)
                 {
                     _owner.ResourcesChanged += ResourcesChanged;
                 }
+                if (_owner is StyledElement styleable2)
+                {
+                    styleable2.PropertyChanged += PropertyChanged;
+                }
 
                 PublishNext();
             }
 
+            private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+            {
+                if (e.Property == StyledElement.ActualThemeVariantProperty)
+                {
+                    PublishNext();
+                }
+            }
+
             private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e)
             {
                 PublishNext();
             }
 
-            private object? Convert(object? value) => _converter?.Invoke(value) ?? value;
+            private object? GetValue()
+            {
+                if (!(_target.Owner is StyledElement themeStyleable)
+                    || !_target.Owner.TryFindResource(_key, themeStyleable.ActualThemeVariant, out var value))
+                {
+                    value = _target.Owner?.FindResource(_key) ?? AvaloniaProperty.UnsetValue;
+                }
+
+                return _converter?.Invoke(value) ?? value;
+            }
         }
     }
 }

+ 1 - 1
src/Avalonia.Base/Data/InstancedBinding.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Data
         /// <param name="priority">The priority of the binding.</param>
         /// <remarks>
         /// This constructor can be used to create any type of binding and as such requires an
-        /// <see cref="ISubject{Object}"/> as the binding source because this is the only binding
+        /// <see cref="IObservable{Object}"/> as the binding source because this is the only binding
         /// source which can be used for all binding modes. If you wish to create an instance with
         /// something other than a subject, use one of the static creation methods on this class.
         /// </remarks>

+ 0 - 3
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@@ -1,6 +1,3 @@
-using System;
-using Avalonia.Data;
-
 namespace Avalonia.Diagnostics
 {
     /// <summary>

+ 5 - 13
src/Avalonia.Base/Input/DragEventArgs.cs

@@ -1,36 +1,28 @@
 using System;
 using Avalonia.Interactivity;
 using Avalonia.Metadata;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Input
 {
     public class DragEventArgs : RoutedEventArgs
     {
-        private Interactive _target;
-        private Point _targetLocation;
+        private readonly Interactive _target;
+        private readonly Point _targetLocation;
 
         public DragDropEffects DragEffects { get; set; }
 
-        public IDataObject Data { get; private set; }
+        public IDataObject Data { get; }
 
-        public KeyModifiers KeyModifiers { get; private set; }
+        public KeyModifiers KeyModifiers { get; }
 
         public Point GetPosition(Visual relativeTo)
         {
-            var point = new Point(0, 0);
-
             if (relativeTo == null)
             {
                 throw new ArgumentNullException(nameof(relativeTo));
             }
 
-            if (_target != null)
-            {
-                point = _target.TranslatePoint(_targetLocation, relativeTo) ?? point;
-            }
-
-            return point;
+            return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0);
         }
 
         [Unstable]

+ 1 - 1
src/Avalonia.Base/Input/KeyGesture.cs

@@ -136,7 +136,7 @@ namespace Avalonia.Input
             return StringBuilderCache.GetStringAndRelease(s);
         }
 
-        public bool Matches(KeyEventArgs keyEvent) =>
+        public bool Matches(KeyEventArgs? keyEvent) =>
             keyEvent != null &&
             keyEvent.KeyModifiers == KeyModifiers &&
             ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key);

+ 19 - 25
src/Avalonia.Base/Input/KeyboardNavigationHandler.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
-using System.Linq;
 using Avalonia.Input.Navigation;
 using Avalonia.VisualTree;
 
@@ -51,7 +50,7 @@ namespace Avalonia.Input
 
             // If there's a custom keyboard navigation handler as an ancestor, use that.
             var custom = (element as Visual)?.FindAncestorOfType<ICustomKeyboardNavigation>(true);
-            if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce))
+            if (custom is not null && HandlePreCustomNavigation(custom, element, direction, out var ce))
                 return ce;
 
             var result = direction switch
@@ -117,32 +116,27 @@ namespace Avalonia.Input
             NavigationDirection direction,
             [NotNullWhen(true)] out IInputElement? result)
         {
-            if (customHandler != null)
+            var (handled, next) = customHandler.GetNext(element, direction);
+
+            if (handled)
             {
-                var (handled, next) = customHandler.GetNext(element, direction);
+                if (next is not null)
+                {
+                    result = next;
+                    return true;
+                }
 
-                if (handled)
+                var r = direction switch
                 {
-                    if (next != null)
-                    {
-                        result = next;
-                        return true;
-                    }
-                    else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
-                    {
-                        var r = direction switch
-                        {
-                            NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler),
-                            NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler),
-                            _ => throw new NotSupportedException(),
-                        };
-
-                        if (r is object)
-                        {
-                            result = r;
-                            return true;
-                        }
-                    }
+                    NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler),
+                    NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler),
+                    _ => null
+                };
+
+                if (r is not null)
+                {
+                    result = r;
+                    return true;
                 }
             }
 

+ 15 - 33
src/Avalonia.Base/Input/Navigation/TabNavigation.cs

@@ -1,6 +1,4 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Input.Navigation
@@ -54,8 +52,7 @@ namespace Avalonia.Input.Navigation
                 // Avoid the endless loop here for Cycle groups
                 if (loopStartElement == nextTabElement)
                     break;
-                if (loopStartElement == null)
-                    loopStartElement = nextTabElement;
+                loopStartElement ??= nextTabElement;
 
                 var firstTabElementInside = GetNextTab(null, nextTabElement, true);
                 if (firstTabElementInside != null)
@@ -80,12 +77,9 @@ namespace Avalonia.Input.Navigation
 
         public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e)
         {
-            if (e is IInputElement container)
+            if (e is IInputElement container && GetLastInTree(container) is { } last)
             {
-                var last = GetLastInTree(container);
-
-                if (last is object)
-                    return GetNextTab(last, false);
+                return GetNextTab(last, false);
             }
 
             return null;
@@ -93,11 +87,8 @@ namespace Avalonia.Input.Navigation
 
         public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly)
         {
-            if (e is null && container is null)
-                throw new InvalidOperationException("Either 'e' or 'container' must be non-null.");
-
-            if (container is null)
-                container = GetGroupParent(e!);
+            container ??=
+                GetGroupParent(e ?? throw new InvalidOperationException("Either 'e' or 'container' must be non-null."));
 
             KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container);
 
@@ -163,8 +154,7 @@ namespace Avalonia.Input.Navigation
                 // Avoid the endless loop here
                 if (loopStartElement == nextTabElement)
                     break;
-                if (loopStartElement == null)
-                    loopStartElement = nextTabElement;
+                loopStartElement ??= nextTabElement;
 
                 // At this point nextTabElement is TabGroup
                 var lastTabElementInside = GetPrevTab(null, nextTabElement, true);
@@ -189,22 +179,18 @@ namespace Avalonia.Input.Navigation
 
         public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e)
         {
-            if (e is IInputElement container)
+            if (e is IInputElement container && GetFirstChild(container) is { } first)
             {
-                var first = GetFirstChild(container);
-
-                if (first is object)
-                    return GetPrevTab(first, null, false);
+                return GetPrevTab(first, null, false);
             }
 
             return null;
         }
 
-        private static IInputElement? FocusedElement(IInputElement e)
+        private static IInputElement? FocusedElement(IInputElement? e)
         {
-            var iie = e;
             // Focus delegation is enabled only if keyboard focus is outside the container
-            if (iie != null && !iie.IsKeyboardFocusWithin)
+            if (e != null && !e.IsKeyboardFocusWithin)
             {
                 var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e);
                 if (focusedElement != null)
@@ -229,13 +215,11 @@ namespace Avalonia.Input.Navigation
         private static IInputElement? GetFirstChild(IInputElement e)
         {
             // If the element has a FocusedElement it should be its first child
-            if (FocusedElement(e) is IInputElement focusedElement)
+            if (FocusedElement(e) is { } focusedElement)
                 return focusedElement;
 
             // Return the first visible element.
-            var uiElement = e as InputElement;
-
-            if (uiElement is null || IsVisibleAndEnabled(uiElement))
+            if (e is not InputElement uiElement || IsVisibleAndEnabled(uiElement))
             {
                 if (e is Visual elementAsVisual)
                 {
@@ -265,7 +249,7 @@ namespace Avalonia.Input.Navigation
         private static IInputElement? GetLastChild(IInputElement e)
         {
             // If the element has a FocusedElement it should be its last child
-            if (FocusedElement(e) is IInputElement focusedElement)
+            if (FocusedElement(e) is { } focusedElement)
                 return focusedElement;
 
             // Return the last visible element.
@@ -273,9 +257,7 @@ namespace Avalonia.Input.Navigation
 
             if (uiElement == null || IsVisibleAndEnabled(uiElement))
             {
-                var elementAsVisual = e as Visual;
-
-                if (elementAsVisual != null)
+                if (e is Visual elementAsVisual)
                 {
                     var children = elementAsVisual.VisualChildren;
                     var count = children.Count;
@@ -322,7 +304,7 @@ namespace Avalonia.Input.Navigation
             return firstTabElement;
         }
 
-        private static IInputElement? GetLastInTree(IInputElement container)
+        private static IInputElement GetLastInTree(IInputElement container)
         {
             IInputElement? result;
             IInputElement? c = container;

+ 12 - 8
src/Avalonia.Base/Layout/LayoutManager.cs

@@ -3,8 +3,9 @@ using System.Buffers;
 using System.Collections.Generic;
 using System.Diagnostics;
 using Avalonia.Logging;
+using Avalonia.Rendering;
 using Avalonia.Threading;
-using Avalonia.VisualTree;
+using Avalonia.Utilities;
 
 #nullable enable
 
@@ -24,6 +25,7 @@ namespace Avalonia.Layout
         private bool _disposed;
         private bool _queued;
         private bool _running;
+        private int _totalPassCount;
 
         public LayoutManager(ILayoutRoot owner)
         {
@@ -33,6 +35,8 @@ namespace Avalonia.Layout
 
         public virtual event EventHandler? LayoutUpdated;
 
+        internal Action<LayoutPassTiming>? LayoutPassTimed { get; set; }
+
         /// <inheritdoc/>
         public virtual void InvalidateMeasure(Layoutable control)
         {
@@ -116,10 +120,9 @@ namespace Avalonia.Layout
 
             if (!_running)
             {
-                Stopwatch? stopwatch = null;
-
                 const LogEventLevel timingLogLevel = LogEventLevel.Information;
-                bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout);
+                var captureTiming = LayoutPassTimed is not null || Logger.IsEnabled(timingLogLevel, LogArea.Layout);
+                var startingTimestamp = 0L;
 
                 if (captureTiming)
                 {
@@ -129,8 +132,7 @@ namespace Avalonia.Layout
                         _toMeasure.Count,
                         _toArrange.Count);
 
-                    stopwatch = new Stopwatch();
-                    stopwatch.Start();
+                    startingTimestamp = Stopwatch.GetTimestamp();
                 }
 
                 _toMeasure.BeginLoop(MaxPasses);
@@ -139,6 +141,7 @@ namespace Avalonia.Layout
                 try
                 {
                     _running = true;
+                    ++_totalPassCount;
 
                     for (var pass = 0; pass < MaxPasses; ++pass)
                     {
@@ -160,9 +163,10 @@ namespace Avalonia.Layout
 
                 if (captureTiming)
                 {
-                    stopwatch!.Stop();
+                    var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp);
+                    LayoutPassTimed?.Invoke(new LayoutPassTiming(_totalPassCount, elapsed));
 
-                    Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed);
+                    Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", elapsed);
                 }
             }
 

+ 3 - 3
src/Avalonia.Base/LogicalTree/LogicalExtensions.cs

@@ -48,7 +48,7 @@ namespace Avalonia.LogicalTree
         /// <param name="logical">The logical.</param>
         /// <param name="includeSelf">If given logical should be included in search.</param>
         /// <returns>First ancestor of given type.</returns>
-        public static T? FindLogicalAncestorOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
+        public static T? FindLogicalAncestorOfType<T>(this ILogical? logical, bool includeSelf = false) where T : class
         {
             if (logical is null)
             {
@@ -120,7 +120,7 @@ namespace Avalonia.LogicalTree
         /// <param name="logical">The logical.</param>
         /// <param name="includeSelf">If given logical should be included in search.</param>
         /// <returns>First descendant of given type.</returns>
-        public static T? FindLogicalDescendantOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
+        public static T? FindLogicalDescendantOfType<T>(this ILogical? logical, bool includeSelf = false) where T : class
         {
             if (logical is null)
             {
@@ -185,7 +185,7 @@ namespace Avalonia.LogicalTree
         /// True if <paramref name="logical"/> is an ancestor of <paramref name="target"/>;
         /// otherwise false.
         /// </returns>
-        public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target)
+        public static bool IsLogicalAncestorOf(this ILogical? logical, ILogical? target)
         {
             var current = target?.LogicalParent;
 

+ 2 - 7
src/Avalonia.Base/Media/Color.cs

@@ -147,16 +147,11 @@ namespace Avalonia.Media
         /// <param name="s">The color string.</param>
         /// <param name="color">The parsed color</param>
         /// <returns>The status of the operation.</returns>
-        public static bool TryParse(string s, out Color color)
+        public static bool TryParse(string? s, out Color color)
         {
             color = default;
 
-            if (s is null)
-            {
-                return false;
-            }
-
-            if (s.Length == 0)
+            if (string.IsNullOrEmpty(s))
             {
                 return false;
             }

+ 1 - 1
src/Avalonia.Base/Media/DrawingContext.cs

@@ -240,7 +240,7 @@ namespace Avalonia.Media
         /// </summary>
         /// <param name="foreground">The foreground brush.</param>
         /// <param name="glyphRun">The glyph run.</param>
-        public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
+        public void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun)
         {
             _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));
 

+ 13 - 13
src/Avalonia.Base/Media/DrawingGroup.cs

@@ -13,14 +13,14 @@ namespace Avalonia.Media
         public static readonly StyledProperty<double> OpacityProperty =
             AvaloniaProperty.Register<DrawingGroup, double>(nameof(Opacity), 1);
 
-        public static readonly StyledProperty<Transform> TransformProperty =
-            AvaloniaProperty.Register<DrawingGroup, Transform>(nameof(Transform));
+        public static readonly StyledProperty<Transform?> TransformProperty =
+            AvaloniaProperty.Register<DrawingGroup, Transform?>(nameof(Transform));
 
-        public static readonly StyledProperty<Geometry> ClipGeometryProperty =
-            AvaloniaProperty.Register<DrawingGroup, Geometry>(nameof(ClipGeometry));
+        public static readonly StyledProperty<Geometry?> ClipGeometryProperty =
+            AvaloniaProperty.Register<DrawingGroup, Geometry?>(nameof(ClipGeometry));
 
-        public static readonly StyledProperty<IBrush> OpacityMaskProperty =
-            AvaloniaProperty.Register<DrawingGroup, IBrush>(nameof(OpacityMask));
+        public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
+            AvaloniaProperty.Register<DrawingGroup, IBrush?>(nameof(OpacityMask));
 
         public static readonly DirectProperty<DrawingGroup, DrawingCollection> ChildrenProperty =
             AvaloniaProperty.RegisterDirect<DrawingGroup, DrawingCollection>(
@@ -36,19 +36,19 @@ namespace Avalonia.Media
             set => SetValue(OpacityProperty, value);
         }
 
-        public Transform Transform
+        public Transform? Transform
         {
             get => GetValue(TransformProperty);
             set => SetValue(TransformProperty, value);
         }
 
-        public Geometry ClipGeometry
+        public Geometry? ClipGeometry
         {
             get => GetValue(ClipGeometryProperty);
             set => SetValue(ClipGeometryProperty, value);
         }
 
-        public IBrush OpacityMask
+        public IBrush? OpacityMask
         {
             get => GetValue(OpacityMaskProperty);
             set => SetValue(OpacityMaskProperty, value);
@@ -159,7 +159,7 @@ namespace Avalonia.Media
 
             public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
             {
-                if (((brush == null) && (pen == null)) || (geometry == null))
+                if ((brush == null) && (pen == null))
                 {
                     return;
                 }
@@ -167,9 +167,9 @@ namespace Avalonia.Media
                 AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
             }
 
-            public void DrawGlyphRun(IBrush foreground, IRef<IGlyphRunImpl> glyphRun)
+            public void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun)
             {
-                if (foreground == null || glyphRun == null)
+                if (foreground == null)
                 {
                     return;
                 }
@@ -184,7 +184,7 @@ namespace Avalonia.Media
                 AddDrawing(glyphRunDrawing);
             }
 
-            public void DrawLine(IPen pen, Point p1, Point p2)
+            public void DrawLine(IPen? pen, Point p1, Point p2)
             {
                 if (pen == null)
                 {

+ 3 - 3
src/Avalonia.Base/Media/DrawingImage.cs

@@ -20,8 +20,8 @@ namespace Avalonia.Media
         /// <summary>
         /// Defines the <see cref="Drawing"/> property.
         /// </summary>
-        public static readonly StyledProperty<Drawing> DrawingProperty =
-            AvaloniaProperty.Register<DrawingImage, Drawing>(nameof(Drawing));
+        public static readonly StyledProperty<Drawing?> DrawingProperty =
+            AvaloniaProperty.Register<DrawingImage, Drawing?>(nameof(Drawing));
 
         /// <inheritdoc/>
         public event EventHandler? Invalidated;
@@ -30,7 +30,7 @@ namespace Avalonia.Media
         /// Gets or sets the drawing content.
         /// </summary>
         [Content]
-        public Drawing Drawing
+        public Drawing? Drawing
         {
             get => GetValue(DrawingProperty);
             set => SetValue(DrawingProperty, value);

+ 2 - 2
src/Avalonia.Base/Media/FontFamily.cs

@@ -119,7 +119,7 @@ namespace Avalonia.Media
 
                 case 2:
                     {
-                        var source = segments[0].StartsWith("/")
+                        var source = segments[0].StartsWith("/", StringComparison.Ordinal)
                             ? new Uri(segments[0], UriKind.Relative)
                             : new Uri(segments[0], UriKind.RelativeOrAbsolute);
 
@@ -188,7 +188,7 @@ namespace Avalonia.Media
         {
             unchecked
             {
-                return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0);
+                return (FamilyNames.GetHashCode() * 397) ^ (Key is not null ? Key.GetHashCode() : 0);
             }
         }
 

+ 1 - 4
src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs

@@ -41,10 +41,7 @@ namespace Avalonia.Media.Fonts
             {
                 var hash = (int)2166136261;
 
-                if (Source != null)
-                {
-                    hash = (hash * 16777619) ^ Source.GetHashCode();
-                }
+                hash = (hash * 16777619) ^ Source.GetHashCode();
 
                 if (BaseUri != null)
                 {

+ 26 - 12
src/Avalonia.Base/Media/FormattedText.cs

@@ -741,6 +741,11 @@ namespace Avalonia.Media
                         null // no previous line break
                         );
 
+                    if(Current is null)
+                    {
+                        return false;
+                    }
+
                     // check if this line fits the text height
                     if (_totalHeight + Current.Height > _that._maxTextHeight)
                     {
@@ -779,7 +784,7 @@ namespace Avalonia.Media
                 // maybe there is no next line at all
                 if (Position + Current.Length < _that._text.Length)
                 {
-                    bool nextLineFits;
+                    bool nextLineFits = false;
 
                     if (_lineCount + 1 >= _that._maxLineCount)
                     {
@@ -795,7 +800,10 @@ namespace Avalonia.Media
                             currentLineBreak
                             );
 
-                        nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
+                        if(_nextLine != null)
+                        {
+                            nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight);
+                        }
                     }
 
                     if (!nextLineFits)
@@ -819,16 +827,22 @@ namespace Avalonia.Media
                                 _previousLineBreak
                                 );
 
-                            currentLineBreak = Current.TextLineBreak;
+                            if(Current != null)
+                            {
+                                currentLineBreak = Current.TextLineBreak;
+                            }
 
                             _that._defaultParaProps.SetTextWrapping(currentWrap);
                         }
                     }
                 }
 
-                _previousHeight = Current.Height;
+                if(Current != null)
+                {
+                    _previousHeight = Current.Height;
 
-                Length = Current.Length;
+                    Length = Current.Length;
+                }
 
                 _previousLineBreak = currentLineBreak;
 
@@ -838,7 +852,7 @@ namespace Avalonia.Media
             /// <summary>
             /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed.
             /// </summary>
-            private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
+            private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak)
             {
                 var line = _formatter.FormatLine(
                     textSource,
@@ -848,7 +862,7 @@ namespace Avalonia.Media
                     lineBreak
                     );
 
-                if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
+                if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
                 {
                     // what I really need here is the last displayed text run of the line
                     // textSourcePosition + line.Length - 1 works except the end of paragraph case,
@@ -1340,7 +1354,7 @@ namespace Avalonia.Media
                     {
                         var highlightBounds = currentLine.GetTextBounds(x0,x1 - x0);
 
-                        if (highlightBounds != null)
+                        if (highlightBounds.Count > 0)
                         {
                             foreach (var bound in highlightBounds)
                             {
@@ -1351,7 +1365,7 @@ namespace Avalonia.Media
                                     // Convert logical units (which extend leftward from the right edge
                                     // of the paragraph) to physical units.
                                     //
-                                    // Note that since rect is in logical units, rect.Right corresponds to 
+                                    // Note that since rect is in logical units, rect.Right corresponds to
                                     // the visual *left* edge of the rectangle in the RTL case. Specifically,
                                     // is the distance leftward from the right edge of the formatting rectangle
                                     // whose width is the paragraph width passed to FormatLine.
@@ -1370,7 +1384,7 @@ namespace Avalonia.Media
                                 else
                                 {
                                     accumulatedBounds = Geometry.Combine(accumulatedBounds, rectangleGeometry, GeometryCombineMode.Union);
-                                }                                  
+                                }
                             }
                         }
                     }
@@ -1601,11 +1615,11 @@ namespace Avalonia.Media
             }
 
             /// <inheritdoc/>
-            public TextRun? GetTextRun(int textSourceCharacterIndex)
+            public TextRun GetTextRun(int textSourceCharacterIndex)
             {
                 if (textSourceCharacterIndex >= _that._text.Length)
                 {
-                    return null;
+                    return new TextEndOfParagraph();
                 }
 
                 var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex);

+ 3 - 3
src/Avalonia.Base/Media/GeometryDrawing.cs

@@ -15,8 +15,8 @@ namespace Avalonia.Media
         /// <summary>
         /// Defines the <see cref="Geometry"/> property.
         /// </summary>
-        public static readonly StyledProperty<Geometry> GeometryProperty =
-            AvaloniaProperty.Register<GeometryDrawing, Geometry>(nameof(Geometry));
+        public static readonly StyledProperty<Geometry?> GeometryProperty =
+            AvaloniaProperty.Register<GeometryDrawing, Geometry?>(nameof(Geometry));
 
         /// <summary>
         /// Defines the <see cref="Brush"/> property.
@@ -34,7 +34,7 @@ namespace Avalonia.Media
         /// Gets or sets the <see cref="Avalonia.Media.Geometry"/> that describes the shape of this <see cref="GeometryDrawing"/>.
         /// </summary>
         [Content]
-        public Geometry Geometry
+        public Geometry? Geometry
         {
             get => GetValue(GeometryProperty);
             set => SetValue(GeometryProperty, value);

+ 1 - 1
src/Avalonia.Base/Media/GlyphRun.cs

@@ -166,7 +166,7 @@ namespace Avalonia.Media
         /// </summary>
         public Point BaselineOrigin
         {
-            get => _baselineOrigin ?? default;
+            get => PlatformImpl.Item.BaselineOrigin;
             set => Set(ref _baselineOrigin, value);
         }
 

+ 6 - 6
src/Avalonia.Base/Media/GlyphRunDrawing.cs

@@ -2,19 +2,19 @@
 {
     public class GlyphRunDrawing : Drawing
     {
-        public static readonly StyledProperty<IBrush> ForegroundProperty =
-            AvaloniaProperty.Register<GlyphRunDrawing, IBrush>(nameof(Foreground));
+        public static readonly StyledProperty<IBrush?> ForegroundProperty =
+            AvaloniaProperty.Register<GlyphRunDrawing, IBrush?>(nameof(Foreground));
 
-        public static readonly StyledProperty<GlyphRun> GlyphRunProperty =
-            AvaloniaProperty.Register<GlyphRunDrawing, GlyphRun>(nameof(GlyphRun));
+        public static readonly StyledProperty<GlyphRun?> GlyphRunProperty =
+            AvaloniaProperty.Register<GlyphRunDrawing, GlyphRun?>(nameof(GlyphRun));
 
-        public IBrush Foreground
+        public IBrush? Foreground
         {
             get => GetValue(ForegroundProperty);
             set => SetValue(ForegroundProperty, value);
         }
 
-        public GlyphRun GlyphRun
+        public GlyphRun? GlyphRun
         {
             get => GetValue(GlyphRunProperty);
             set => SetValue(GlyphRunProperty, value);

+ 1 - 1
src/Avalonia.Base/Media/HslColor.cs

@@ -254,7 +254,7 @@ namespace Avalonia.Media
         /// <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)
+        public static bool TryParse(string? s, out HslColor hslColor)
         {
             bool prefixMatched = false;
 

+ 1 - 1
src/Avalonia.Base/Media/HsvColor.cs

@@ -254,7 +254,7 @@ namespace Avalonia.Media
         /// <param name="s">The HSV color string to parse.</param>
         /// <param name="hsvColor">The parsed <see cref="HsvColor"/>.</param>
         /// <returns>True if parsing was successful; otherwise, false.</returns>
-        public static bool TryParse(string s, out HsvColor hsvColor)
+        public static bool TryParse(string? s, out HsvColor hsvColor)
         {
             bool prefixMatched = false;
 

+ 1 - 2
src/Avalonia.Base/Media/IVisualBrush.cs

@@ -1,5 +1,4 @@
 using Avalonia.Metadata;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Media
 {
@@ -12,6 +11,6 @@ namespace Avalonia.Media
         /// <summary>
         /// Gets the visual to draw.
         /// </summary>
-        Visual Visual { get; }
+        Visual? Visual { get; }
     }
 }

+ 6 - 18
src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs

@@ -39,17 +39,8 @@ namespace Avalonia.Media.Immutable
             {
                 return true;
             }
-            else if (other is null)
-            {
-                return false;
-            }
 
-            if (Offset != other.Offset)
-            {
-                return false;
-            }
-
-            return SequenceEqual(Dashes, other.Dashes);
+            return other is not null && Offset == other.Offset && SequenceEqual(_dashes, other.Dashes);
         }
 
         /// <inheritdoc/>
@@ -58,30 +49,27 @@ namespace Avalonia.Media.Immutable
             var hashCode = 717868523;
             hashCode = hashCode * -1521134295 + Offset.GetHashCode();
 
-            if (_dashes != null)
+            foreach (var i in _dashes)
             {
-                foreach (var i in _dashes)
-                {
-                    hashCode = hashCode * -1521134295 + i.GetHashCode();
-                }
+                hashCode = hashCode * -1521134295 + i.GetHashCode();
             }
 
             return hashCode;
         }
 
-        private static bool SequenceEqual(IReadOnlyList<double> left, IReadOnlyList<double>? right)
+        private static bool SequenceEqual(double[] left, IReadOnlyList<double>? right)
         {
             if (ReferenceEquals(left, right))
             {
                 return true;
             }
 
-            if (left == null || right == null || left.Count != right.Count)
+            if (right is null || left.Length != right.Count)
             {
                 return false;
             }
 
-            for (var c = 0; c < left.Count; c++)
+            for (var c = 0; c < left.Length; c++)
             {
                 if (left[c] != right[c])
                 {

+ 3 - 4
src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs

@@ -1,5 +1,4 @@
 using Avalonia.Media.Imaging;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Media.Immutable
 {
@@ -31,11 +30,11 @@ namespace Avalonia.Media.Immutable
             RelativeRect? destinationRect = null,
             double opacity = 1,
             ImmutableTransform? transform = null,
-            RelativePoint transformOrigin = new RelativePoint(),
+            RelativePoint transformOrigin = default,
             RelativeRect? sourceRect = null,
             Stretch stretch = Stretch.Uniform,
             TileMode tileMode = TileMode.None,
-            Imaging.BitmapInterpolationMode bitmapInterpolationMode = Imaging.BitmapInterpolationMode.Default)
+            BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
             : base(
                   alignmentX,
                   alignmentY,
@@ -62,6 +61,6 @@ namespace Avalonia.Media.Immutable
         }
 
         /// <inheritdoc/>
-        public Visual Visual { get; }
+        public Visual? Visual { get; }
     }
 }

+ 7 - 7
src/Avalonia.Base/Media/TextDecoration.cs

@@ -22,8 +22,8 @@ namespace Avalonia.Media
         /// <summary>
         /// Defines the <see cref="Stroke"/> property.
         /// </summary>
-        public static readonly StyledProperty<IBrush> StrokeProperty =
-            AvaloniaProperty.Register<TextDecoration, IBrush>(nameof(Stroke));
+        public static readonly StyledProperty<IBrush?> StrokeProperty =
+            AvaloniaProperty.Register<TextDecoration, IBrush?>(nameof(Stroke));
 
         /// <summary>
         /// Defines the <see cref="StrokeThicknessUnit"/> property.
@@ -34,8 +34,8 @@ namespace Avalonia.Media
         /// <summary>
         /// Defines the <see cref="StrokeDashArray"/> property.
         /// </summary>
-        public static readonly StyledProperty<AvaloniaList<double>> StrokeDashArrayProperty =
-            AvaloniaProperty.Register<TextDecoration, AvaloniaList<double>>(nameof(StrokeDashArray));
+        public static readonly StyledProperty<AvaloniaList<double>?> StrokeDashArrayProperty =
+            AvaloniaProperty.Register<TextDecoration, AvaloniaList<double>?>(nameof(StrokeDashArray));
 
         /// <summary>
         /// Defines the <see cref="StrokeDashOffset"/> property.
@@ -82,7 +82,7 @@ namespace Avalonia.Media
         /// <summary>
         /// Gets or sets the <see cref="IBrush"/> that specifies how the <see cref="TextDecoration"/> is painted.
         /// </summary>
-        public IBrush Stroke
+        public IBrush? Stroke
         {
             get { return GetValue(StrokeProperty); }
             set { SetValue(StrokeProperty, value); }
@@ -101,7 +101,7 @@ namespace Avalonia.Media
         /// Gets or sets a collection of <see cref="double"/> values that indicate the pattern of dashes and gaps
         /// that is used to draw the <see cref="TextDecoration"/>.
         /// </summary>
-        public AvaloniaList<double> StrokeDashArray
+        public AvaloniaList<double>? StrokeDashArray
         {
             get { return GetValue(StrokeDashArrayProperty); }
             set { SetValue(StrokeDashArrayProperty, value); }
@@ -220,7 +220,7 @@ namespace Avalonia.Media
 
                 var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY));
 
-                if (intersections != null && intersections.Count > 0)
+                if (intersections.Count > 0)
                 {
                     var last = baselineOrigin.X;
                     var finalPos = last + glyphRun.Size.Width;

+ 1 - 3
src/Avalonia.Base/Media/TextFormatting/ITextSource.cs

@@ -1,6 +1,4 @@
-using Avalonia.Metadata;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
 {
     /// <summary>
     /// Produces <see cref="TextRun"/> objects that are used by the <see cref="TextFormatter"/>.

+ 5 - 11
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@@ -15,9 +15,7 @@ namespace Avalonia.Media.TextFormatting
 
         public override void Justify(TextLine textLine)
         {
-            var lineImpl = textLine as TextLineImpl;
-
-            if(lineImpl is null)
+            if (textLine is not TextLineImpl lineImpl)
             {
                 return;
             }
@@ -34,14 +32,9 @@ namespace Avalonia.Media.TextFormatting
                 return;
             }
 
-            var textLineBreak = lineImpl.TextLineBreak;
-
-            if (textLineBreak is not null && textLineBreak.TextEndOfLine is not null)
+            if (lineImpl.TextLineBreak is { TextEndOfLine: not null, IsSplit: false })
             {
-                if (textLineBreak.RemainingRuns is null || textLineBreak.RemainingRuns.Count == 0)
-                {
-                    return;
-                }
+                return;
             }
 
             var breakOportunities = new Queue<int>();
@@ -107,7 +100,8 @@ namespace Avalonia.Media.TextFormatting
                         var glyphIndex = glyphRun.FindGlyphIndex(characterIndex);
                         var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex];
 
-                        shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
+                        shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex,
+                            glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
                     }
 
                     glyphRun.GlyphInfos = shapedBuffer.GlyphInfos;

+ 14 - 22
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -82,24 +82,15 @@ namespace Avalonia.Media.TextFormatting
             var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
             var textSpan = text.Span;
 
-            if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script))
+            if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count))
             {
-                if (script == Script.Common && previousGlyphTypeface is not null)
-                {
-                    if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _))
-                    {
-                        return new UnshapedTextRun(text.Slice(0, fallbackCount),
-                            defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
-                    }
-                }
-
                 return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface),
                     biDiLevel);
             }
 
             if (previousGlyphTypeface is not null)
             {
-                if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _))
+                if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count))
                 {
                     return new UnshapedTextRun(text.Slice(0, count),
                         defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
@@ -127,14 +118,17 @@ namespace Avalonia.Media.TextFormatting
                 fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
                     defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
                     out var fallbackTypeface);
-
-            var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
-
-            if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _))
+                        
+            if (matchFound)
             {
-                //Fallback found
-                return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
-                    biDiLevel);
+                // Fallback found
+                var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
+                                
+                if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
+                {                    
+                    return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
+                        biDiLevel);
+                }                
             }
 
             // no fallback found
@@ -160,17 +154,15 @@ namespace Avalonia.Media.TextFormatting
         /// <param name="glyphTypeface">The typeface that is used to find matching characters.</param>
         /// <param name="defaultGlyphTypeface">The default typeface.</param>
         /// <param name="length">The shapeable length.</param>
-        /// <param name="script"></param>
         /// <returns></returns>
         internal static bool TryGetShapeableLength(
             ReadOnlySpan<char> text,
             IGlyphTypeface glyphTypeface,
             IGlyphTypeface? defaultGlyphTypeface,
-            out int length,
-            out Script script)
+            out int length)
         {
             length = 0;
-            script = Script.Unknown;
+            var script = Script.Unknown;
 
             if (text.IsEmpty)
             {

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs

@@ -38,7 +38,7 @@
         /// <param name="previousLineBreak">A <see cref="TextLineBreak"/> value that specifies the text formatter state,
         /// in terms of where the previous line in the paragraph was broken by the text formatting process.</param>
         /// <returns>The formatted line.</returns>
-        public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+        public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
             TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null);
     }
 }

+ 111 - 98
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -2,7 +2,6 @@
 using System;
 using System.Buffers;
 using System.Collections.Generic;
-using System.Linq;
 using System.Runtime.InteropServices;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Utilities;
@@ -19,71 +18,63 @@ namespace Avalonia.Media.TextFormatting
         [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
 
         /// <inheritdoc cref="TextFormatter.FormatLine"/>
-        public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
+        public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
             TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
         {
-            var textWrapping = paragraphProperties.TextWrapping;
-            FlowDirection resolvedFlowDirection;
             TextLineBreak? nextLineBreak = null;
-            IReadOnlyList<TextRun>? textRuns;
             var objectPool = FormattingObjectPool.Instance;
             var fontManager = FontManager.Current;
 
-            var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
-                out var textEndOfLine, out var textSourceLength);
+            // we've wrapped the previous line and need to continue wrapping: ignore the textSource and do that instead
+            if (previousLineBreak is WrappingTextLineBreak wrappingTextLineBreak
+                && wrappingTextLineBreak.AcquireRemainingRuns() is { } remainingRuns
+                && paragraphProperties.TextWrapping != TextWrapping.NoWrap)
+            {
+                return PerformTextWrapping(remainingRuns, true, firstTextSourceIndex, paragraphWidth,
+                    paragraphProperties, previousLineBreak.FlowDirection, previousLineBreak, objectPool);
+            }
 
+            RentedList<TextRun>? fetchedRuns = null;
             RentedList<TextRun>? shapedTextRuns = null;
-
             try
             {
-                if (previousLineBreak?.RemainingRuns is { } remainingRuns)
+                fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine,
+                    out var textSourceLength);
+
+                if (fetchedRuns.Count == 0)
                 {
-                    resolvedFlowDirection = previousLineBreak.FlowDirection;
-                    textRuns = remainingRuns;
-                    nextLineBreak = previousLineBreak;
-                    shapedTextRuns = null;
+                    return null;
                 }
-                else
-                {
-                    shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
-                        out resolvedFlowDirection);
-                    textRuns = shapedTextRuns;
 
-                    if (nextLineBreak == null && textEndOfLine != null)
-                    {
-                        nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
-                    }
-                }
+                shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
+                    out var resolvedFlowDirection);
 
-                TextLineImpl textLine;
+                if (nextLineBreak == null && textEndOfLine != null)
+                {
+                    nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+                }
 
-                switch (textWrapping)
+                switch (paragraphProperties.TextWrapping)
                 {
                     case TextWrapping.NoWrap:
                     {
-                        // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
-                        // which already uses an array: ToArray() won't ever be called in this case
-                        var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
-
-                        textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
+                        var textLine = new TextLineImpl(shapedTextRuns.ToArray(), firstTextSourceIndex,
+                            textSourceLength,
                             paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
 
-                        textLine.FinalizeLine();
+                            textLine.FinalizeLine();
 
-                        break;
+                        return textLine;
                     }
                     case TextWrapping.WrapWithOverflow:
                     case TextWrapping.Wrap:
                     {
-                        textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
-                            paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
-                        break;
+                        return PerformTextWrapping(shapedTextRuns, false, firstTextSourceIndex, paragraphWidth,
+                            paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool);
                     }
                     default:
-                        throw new ArgumentOutOfRangeException(nameof(textWrapping));
+                        throw new ArgumentOutOfRangeException(nameof(paragraphProperties.TextWrapping));
                 }
-
-                return textLine;
             }
             finally
             {
@@ -108,15 +99,16 @@ namespace Avalonia.Media.TextFormatting
             for (var i = 0; i < textRuns.Count; i++)
             {
                 var currentRun = textRuns[i];
+                var currentRunLength = currentRun.Length;
 
-                if (currentLength + currentRun.Length < length)
+                if (currentLength + currentRunLength < length)
                 {
-                    currentLength += currentRun.Length;
+                    currentLength += currentRunLength;
 
                     continue;
                 }
 
-                var firstCount = currentRun.Length >= 1 ? i + 1 : i;
+                var firstCount = currentRunLength >= 1 ? i + 1 : i;
 
                 if (firstCount > 1)
                 {
@@ -128,13 +120,13 @@ namespace Avalonia.Media.TextFormatting
 
                 var secondCount = textRuns.Count - firstCount;
 
-                if (currentLength + currentRun.Length == length)
+                if (currentLength + currentRunLength == length)
                 {
                     var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null;
 
                     if (second != null)
                     {
-                        var offset = currentRun.Length >= 1 ? 1 : 0;
+                        var offset = currentRunLength >= 1 ? 1 : 0;
 
                         for (var j = 0; j < secondCount; j++)
                         {
@@ -249,49 +241,49 @@ namespace Avalonia.Media.TextFormatting
                     switch (currentRun)
                     {
                         case UnshapedTextRun shapeableRun:
-                        {
-                            groupedRuns.Clear();
-                            groupedRuns.Add(shapeableRun);
+                            {
+                                groupedRuns.Clear();
+                                groupedRuns.Add(shapeableRun);
 
-                            var text = shapeableRun.Text;
-                            var properties = shapeableRun.Properties;
+                                var text = shapeableRun.Text;
+                                var properties = shapeableRun.Properties;
 
-                            while (index + 1 < processedRuns.Count)
-                            {
-                                if (processedRuns[index + 1] is not UnshapedTextRun nextRun)
+                                while (index + 1 < processedRuns.Count)
                                 {
+                                    if (processedRuns[index + 1] is not UnshapedTextRun nextRun)
+                                    {
+                                        break;
+                                    }
+
+                                    if (shapeableRun.BidiLevel == nextRun.BidiLevel
+                                        && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
+                                        && CanShapeTogether(properties, nextRun.Properties))
+                                    {
+                                        groupedRuns.Add(nextRun);
+                                        index++;
+                                        shapeableRun = nextRun;
+                                        text = joinedText;
+                                        continue;
+                                    }
+
                                     break;
                                 }
 
-                                if (shapeableRun.BidiLevel == nextRun.BidiLevel
-                                    && TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
-                                    && CanShapeTogether(properties, nextRun.Properties))
-                                {
-                                    groupedRuns.Add(nextRun);
-                                    index++;
-                                    shapeableRun = nextRun;
-                                    text = joinedText;
-                                    continue;
-                                }
+                                var shaperOptions = new TextShaperOptions(
+                                    properties.CachedGlyphTypeface,
+                                    properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
+                                    paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
+
+                                ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns);
 
                                 break;
                             }
-
-                            var shaperOptions = new TextShaperOptions(
-                                properties.CachedGlyphTypeface,
-                                properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
-                                paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
-
-                            ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns);
-
-                            break;
-                        }
                         default:
-                        {
-                            shapedRuns.Add(currentRun);
+                            {
+                                shapedRuns.Add(currentRun);
 
-                            break;
-                        }
+                                break;
+                            }
                     }
                 }
             }
@@ -504,16 +496,7 @@ namespace Avalonia.Media.TextFormatting
 
             while (textRunEnumerator.MoveNext())
             {
-                var textRun = textRunEnumerator.Current;
-
-                if (textRun == null)
-                {
-                    textRuns.Add(new TextEndOfParagraph());
-
-                    textSourceLength += TextRun.DefaultTextSourceLength;
-
-                    break;
-                }
+                TextRun textRun = textRunEnumerator.Current!;
 
                 if (textRun is TextEndOfLine textEndOfLine)
                 {
@@ -653,7 +636,7 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         /// <returns>The empty text line.</returns>
         public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
-            TextParagraphProperties paragraphProperties, FontManager fontManager)
+            TextParagraphProperties paragraphProperties)
         {
             var flowDirection = paragraphProperties.FlowDirection;
             var properties = paragraphProperties.DefaultTextRunProperties;
@@ -675,21 +658,21 @@ namespace Avalonia.Media.TextFormatting
         /// Performs text wrapping returns a list of text lines.
         /// </summary>
         /// <param name="textRuns"></param>
+        /// <param name="canReuseTextRunList">Whether <see cref="textRuns"/> can be reused to store the split runs.</param>
         /// <param name="firstTextSourceIndex">The first text source index.</param>
         /// <param name="paragraphWidth">The paragraph width.</param>
         /// <param name="paragraphProperties">The text paragraph properties.</param>
         /// <param name="resolvedFlowDirection"></param>
         /// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
         /// <param name="objectPool">A pool used to get reusable formatting objects.</param>
-        /// <param name="fontManager">The font manager to use.</param>
         /// <returns>The wrapped text line.</returns>
-        private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
-            double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
-            TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager)
+        private static TextLineImpl PerformTextWrapping(List<TextRun> textRuns, bool canReuseTextRunList,
+            int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties,
+            FlowDirection resolvedFlowDirection, TextLineBreak? currentLineBreak, FormattingObjectPool objectPool)
         {
             if (textRuns.Count == 0)
             {
-                return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager);
+                return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
             }
 
             if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@@ -712,7 +695,7 @@ namespace Avalonia.Media.TextFormatting
                 switch (currentRun)
                 {
                     case ShapedTextRun:
-                    {
+                        {
                             var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
 
                             while (lineBreaker.MoveNext(out var lineBreak))
@@ -754,7 +737,7 @@ namespace Avalonia.Media.TextFormatting
                                                 break;
                                             }
 
-                                            while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count)
+                                            while (lineBreaker.MoveNext(out lineBreak))
                                             {
                                                 currentPosition += lineBreak.PositionWrap;
 
@@ -780,6 +763,11 @@ namespace Avalonia.Media.TextFormatting
                                             currentPosition = currentLength + lineBreak.PositionWrap;
                                         }
 
+                                        if (currentPosition == 0 && measuredLength > 0)
+                                        {
+                                            currentPosition = measuredLength;
+                                        }
+
                                         breakFound = true;
 
                                         break;
@@ -819,13 +807,37 @@ namespace Avalonia.Media.TextFormatting
 
             try
             {
-                var textLineBreak = postSplitRuns?.Count > 0 ?
-                    new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
-                    null;
+                TextLineBreak? textLineBreak;
+                if (postSplitRuns?.Count > 0)
+                {
+                    List<TextRun> remainingRuns;
+
+                    // reuse the list as much as possible:
+                    // if canReuseTextRunList == true it's coming from previous remaining runs
+                    if (canReuseTextRunList)
+                    {
+                        remainingRuns = textRuns;
+                        remainingRuns.Clear();
+                    }
+                    else
+                    {
+                        remainingRuns = new List<TextRun>();
+                    }
 
-                if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
+                    for (var i = 0; i < postSplitRuns.Count; ++i)
+                    {
+                        remainingRuns.Add(postSplitRuns[i]);
+                    }
+
+                    textLineBreak = new WrappingTextLineBreak(null, resolvedFlowDirection, remainingRuns);
+                }
+                else if (currentLineBreak?.TextEndOfLine is { } textEndOfLine)
                 {
-                    textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
+                    textLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
+                }
+                else
+                {
+                    textLineBreak = null;
                 }
 
                 var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
@@ -833,6 +845,7 @@ namespace Avalonia.Media.TextFormatting
                     textLineBreak);
 
                 textLine.FinalizeLine();
+
                 return textLine;
             }
             finally

+ 37 - 17
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting
             foreach (var textLine in _textLines)
             {
                 //Current line isn't covered.
-                if (textLine.FirstTextSourceIndex + textLine.Length < start)
+                if (textLine.FirstTextSourceIndex + textLine.Length <= start)
                 {
                     currentY += textLine.Height;
 
@@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting
         {
             var (x, y) = point;
 
-            var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
-
             var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
 
-            if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+            var lastTrailingIndex = 0;
+
+            if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight)
             {
-                lastTrailingIndex -= textLine.NewLineLength;
+                lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
+
+                if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+                {
+                    lastTrailingIndex -= textLine.NewLineLength;
+                }
+
+                if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
+                {
+                    lastTrailingIndex -= textEndOfLine.Length;
+                }
             }
+            else
+            {
+                if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
+                {
+                    lastTrailingIndex += textLine.NewLineLength;
+                }
+
+                if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine)
+                {
+                    lastTrailingIndex += textEndOfLine.Length;
+                }
+            }       
 
             var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
 
@@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting
         /// <returns></returns>
         private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
             IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping,
-            TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, 
+            TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight,
             double letterSpacing)
         {
             var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
@@ -416,9 +438,11 @@ namespace Avalonia.Media.TextFormatting
                 width = lineWidth;
             }
 
-            if (left > textLine.Start)
+            var start = textLine.Start;
+
+            if (left > start)
             {
-                left = textLine.Start;
+                left = start;
             }
 
             height += textLine.Height;
@@ -427,12 +451,10 @@ namespace Avalonia.Media.TextFormatting
         private TextLine[] CreateTextLines()
         {
             var objectPool = FormattingObjectPool.Instance;
-            var fontManager = FontManager.Current;
 
             if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
             {
-                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties,
-                    fontManager);
+                var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
 
                 Bounds = new Rect(0, 0, 0, textLine.Height);
 
@@ -456,12 +478,12 @@ namespace Avalonia.Media.TextFormatting
                     var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
                         _paragraphProperties, previousLine?.TextLineBreak);
 
-                    if (textLine.Length == 0)
+                    if (textLine is null)
                     {
                         if (previousLine != null && previousLine.NewLineLength > 0)
                         {
                             var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
-                                _paragraphProperties, fontManager);
+                                _paragraphProperties);
 
                             textLines.Add(emptyTextLine);
 
@@ -504,7 +526,7 @@ namespace Avalonia.Media.TextFormatting
                     //Fulfill max lines constraint
                     if (MaxLines > 0 && textLines.Count >= MaxLines)
                     {
-                        if (textLine.TextLineBreak?.RemainingRuns is not null)
+                        if (textLine.TextLineBreak is { IsSplit: true })
                         {
                             textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
                         }
@@ -518,11 +540,9 @@ namespace Avalonia.Media.TextFormatting
                     }
                 }
 
-                //Make sure the TextLayout always contains at least on empty line
                 if (textLines.Count == 0)
                 {
-                    var textLine =
-                        TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
+                    var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
 
                     textLines.Add(textLine);
 

+ 7 - 8
src/Avalonia.Base/Media/TextFormatting/TextLineBreak.cs

@@ -1,15 +1,13 @@
-using System.Collections.Generic;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
 {
     public class TextLineBreak
     {
-        public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, 
-            IReadOnlyList<TextRun>? remainingRuns = null)
+        public TextLineBreak(TextEndOfLine? textEndOfLine = null,
+            FlowDirection flowDirection = FlowDirection.LeftToRight, bool isSplit = false)
         {
             TextEndOfLine = textEndOfLine;
             FlowDirection = flowDirection;
-            RemainingRuns = remainingRuns;
+            IsSplit = isSplit;
         }
 
         /// <summary>
@@ -23,8 +21,9 @@ namespace Avalonia.Media.TextFormatting
         public FlowDirection FlowDirection { get; }
         
         /// <summary>
-        /// Get the remaining runs that were split up by the <see cref="TextFormatter"/> during the formatting process.
+        /// Gets whether there were remaining runs after this line break,
+        /// that were split up by the <see cref="TextFormatter"/> during the formatting process.
         /// </summary>
-        public IReadOnlyList<TextRun>? RemainingRuns { get; }
+        public bool IsSplit { get; }
     }
 }

+ 398 - 294
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting
         private readonly double _paragraphWidth;
         private readonly TextParagraphProperties _paragraphProperties;
         private TextLineMetrics _textLineMetrics;
+        private TextLineBreak? _textLineBreak;
         private readonly FlowDirection _resolvedFlowDirection;
 
         public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
@@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
         {
             FirstTextSourceIndex = firstTextSourceIndex;
             Length = length;
-            TextLineBreak = lineBreak;
+            _textLineBreak = lineBreak;
             HasCollapsed = hasCollapsed;
 
             _textRuns = textRuns;
@@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting
         public override int Length { get; }
 
         /// <inheritdoc/>
-        public override TextLineBreak? TextLineBreak { get; }
+        public override TextLineBreak? TextLineBreak => _textLineBreak;
 
         /// <inheritdoc/>
         public override bool HasCollapsed { get; }
@@ -167,38 +168,54 @@ namespace Avalonia.Media.TextFormatting
         {
             if (_textRuns.Length == 0)
             {
-                return new CharacterHit();
+                return new CharacterHit(FirstTextSourceIndex);
             }
 
             distance -= Start;
 
+            var lastIndex = _textRuns.Length - 1;
+
+            if (_textRuns[lastIndex] is TextEndOfLine)
+            {
+                lastIndex--;
+            }
+
+            var currentPosition = FirstTextSourceIndex;
+
+            if (lastIndex < 0)
+            {
+                return new CharacterHit(currentPosition);
+            }
+
             if (distance <= 0)
             {
                 var firstRun = _textRuns[0];
 
-                return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0);
+                if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft)
+                {
+                    currentPosition = Length - firstRun.Length;
+                }
+
+                return GetRunCharacterHit(firstRun, currentPosition, 0);
             }
 
             if (distance >= WidthIncludingTrailingWhitespace)
             {
-                var lastRun = _textRuns[_textRuns.Length - 1];
+                var lastRun = _textRuns[lastIndex];
 
-                var size = 0.0;
-
-                if (lastRun is DrawableTextRun drawableTextRun)
+                if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
                 {
-                    size = drawableTextRun.Size.Width;
+                    currentPosition = Length - lastRun.Length;
                 }
 
-                return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size);
+                return GetRunCharacterHit(lastRun, currentPosition, distance);
             }
 
             // process hit that happens within the line
             var characterHit = new CharacterHit();
-            var currentPosition = FirstTextSourceIndex;
             var currentDistance = 0.0;
 
-            for (var i = 0; i < _textRuns.Length; i++)
+            for (var i = 0; i <= lastIndex; i++)
             {
                 var currentRun = _textRuns[i];
 
@@ -230,7 +247,7 @@ namespace Avalonia.Media.TextFormatting
 
                         currentRun = _textRuns[j];
 
-                        if(currentRun is not ShapedTextRun)
+                        if (currentRun is not ShapedTextRun)
                         {
                             continue;
                         }
@@ -262,10 +279,6 @@ namespace Avalonia.Media.TextFormatting
                         continue;
                     }
                 }
-                else
-                {
-                    continue;
-                }
 
                 break;
             }
@@ -410,10 +423,10 @@ namespace Avalonia.Media.TextFormatting
                     {
                         if (currentGlyphRun != null)
                         {
-                            distance = currentGlyphRun.Size.Width - distance;
+                            currentDistance -= currentGlyphRun.Size.Width;
                         }
 
-                        return Math.Max(0, currentDistance - distance);
+                        return currentDistance + distance;
                     }
 
                     if (currentRun is DrawableTextRun drawableTextRun)
@@ -563,386 +576,505 @@ namespace Avalonia.Media.TextFormatting
             return GetPreviousCaretCharacterHit(characterHit);
         }
 
-        private IReadOnlyList<TextBounds> GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength)
+        public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
         {
-            var characterIndex = firstTextSourceIndex + textLength;
+            if (_textRuns.Length == 0)
+            {
+                return Array.Empty<TextBounds>();
+            }
 
-            var result = new List<TextBounds>(_textRuns.Length);
-            var lastDirection = FlowDirection.LeftToRight;
-            var currentDirection = lastDirection;
+            var result = new List<TextBounds>();
 
             var currentPosition = FirstTextSourceIndex;
             var remainingLength = textLength;
 
-            var startX = Start;
-            double currentWidth = 0;
-            var currentRect = default(Rect);
-
-            TextRunBounds lastRunBounds = default;
-
-            for (var index = 0; index < _textRuns.Length; index++)
+            static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection)
             {
-                if (_textRuns[index] is not DrawableTextRun currentRun)
+                if (textRun is ShapedTextRun shapedTextRun)
                 {
-                    continue;
+                    return shapedTextRun.ShapedBuffer.IsLeftToRight ?
+                        FlowDirection.LeftToRight :
+                        FlowDirection.RightToLeft;
                 }
 
-                var characterLength = 0;
-                var endX = startX;
-
-                TextRunBounds currentRunBounds;
+                return currentDirection;
+            }
 
-                double combinedWidth;
+            if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
+            {
+                var currentX = Start;
 
-                if (currentRun is ShapedTextRun currentShapedRun)
+                for (int i = 0; i < _textRuns.Length; i++)
                 {
-                    var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
+                    var currentRun = _textRuns[i];
 
-                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+                    var firstRunIndex = i;
+                    var lastRunIndex = firstRunIndex;
+                    var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight);
+                    var directionalWidth = 0.0;
+
+                    if (currentRun is DrawableTextRun currentDrawable)
                     {
-                        startX += currentRun.Size.Width;
+                        directionalWidth = currentDrawable.Size.Width;
+                    }
 
-                        currentPosition += currentRun.Length;
+                    // Find consecutive runs of same direction
+                    for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++)
+                    {
+                        var nextRun = _textRuns[lastRunIndex + 1];
 
-                        continue;
+                        var nextDirection = GetDirection(nextRun, currentDirection);
+
+                        if (currentDirection != nextDirection)
+                        {
+                            break;
+                        }
+
+                        if (nextRun is DrawableTextRun nextDrawable)
+                        {
+                            directionalWidth += nextDrawable.Size.Width;
+                        }
                     }
 
-                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                    //Skip runs that are not part of the hit test range
+                    switch (currentDirection)
                     {
-                        var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
+                        case FlowDirection.RightToLeft:
+                            {
+                                for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+                                {
+                                    currentRun = _textRuns[lastRunIndex];
 
-                        double startOffset;
+                                    if (currentPosition + currentRun.Length > firstTextSourceIndex)
+                                    {
+                                        break;
+                                    }
 
-                        double endOffset;
+                                    currentPosition += currentRun.Length;
 
-                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+                                    if (currentRun is DrawableTextRun drawableTextRun)
+                                    {
+                                        directionalWidth -= drawableTextRun.Size.Width;
+                                        currentX += drawableTextRun.Size.Width;
+                                    }
 
-                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                                    if(lastRunIndex - 1 < 0)
+                                    {
+                                        break;
+                                    }
+                                }
 
-                        startX += startOffset;
+                                break;
+                            }
+                        default:
+                            {
+                                for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+                                {
+                                    currentRun = _textRuns[firstRunIndex];
 
-                        endX += endOffset;
+                                    if (currentPosition + currentRun.Length > firstTextSourceIndex)
+                                    {
+                                        break;
+                                    }
 
-                        var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+                                    currentPosition += currentRun.Length;
 
-                        var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+                                    if (currentRun is DrawableTextRun drawableTextRun)
+                                    {
+                                        currentX += drawableTextRun.Size.Width;
+                                        directionalWidth -= drawableTextRun.Size.Width;
+                                    }
 
-                        characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
+                                    if(firstRunIndex + 1 == _textRuns.Length)
+                                    {
+                                        break;
+                                    }
+                                }
 
-                        currentDirection = FlowDirection.LeftToRight;
+                                break;
+                            }
                     }
-                    else
+
+                    i = lastRunIndex;
+
+                    if (directionalWidth == 0)
                     {
-                        var rightToLeftIndex = index;
-                        var rightToLeftWidth = currentShapedRun.Size.Width;
+                        continue;
+                    }
 
-                        while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
-                        {
-                            if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
+                    var coveredLength = 0;
+                    TextBounds? textBounds = null;
+
+                    switch (currentDirection)
+                    {
+
+                        case FlowDirection.RightToLeft:
                             {
+                                textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex,
+                                        currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+                                currentX += directionalWidth;
+
                                 break;
                             }
+                        default:
+                            {
+                                textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
+                                        currentPosition, remainingLength, out coveredLength, out currentPosition);
 
-                            rightToLeftIndex++;
-
-                            rightToLeftWidth += nextShapedRun.Size.Width;
+                                currentX = textBounds.Rectangle.Right;
 
-                            if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
-                            {
                                 break;
                             }
+                    }
 
-                            currentShapedRun = nextShapedRun;
-                        }
+                    if (coveredLength > 0)
+                    {
+                        result.Add(textBounds);
 
-                        startX += rightToLeftWidth;
+                        remainingLength -= coveredLength;
+                    }
 
-                        currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+                    if (remainingLength <= 0)
+                    {
+                        break;
+                    }
+                }
+            }
+            else
+            {
+                var currentX = Start + WidthIncludingTrailingWhitespace;
 
-                        remainingLength -= currentRunBounds.Length;
-                        currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
-                        endX = currentRunBounds.Rectangle.Right;
-                        startX = currentRunBounds.Rectangle.Left;
+                for (int i = _textRuns.Length - 1; i >= 0; i--)
+                {
+                    var currentRun = _textRuns[i];
+                    var firstRunIndex = i;
+                    var lastRunIndex = firstRunIndex;
+                    var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft);
+                    var directionalWidth = 0.0;
 
-                        var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
+                    if (currentRun is DrawableTextRun currentDrawable)
+                    {
+                        directionalWidth = currentDrawable.Size.Width;
+                    }
+
+                    // Find consecutive runs of same direction
+                    for (; firstRunIndex - 1 > 0; firstRunIndex--)
+                    {
+                        var previousRun = _textRuns[firstRunIndex - 1];
+
+                        var previousDirection = GetDirection(previousRun, currentDirection);
+
+                        if (currentDirection != previousDirection)
+                        {
+                            break;
+                        }
 
-                        for (int i = rightToLeftIndex - 1; i >= index; i--)
+                        if (currentRun is DrawableTextRun previousDrawable)
                         {
-                            if (_textRuns[i] is not ShapedTextRun shapedRun)
+                            directionalWidth += previousDrawable.Size.Width;
+                        }
+                    }
+
+                    //Skip runs that are not part of the hit test range
+                    switch (currentDirection)
+                    {
+                        case FlowDirection.RightToLeft:
                             {
-                                continue;
-                            }
+                                for (; lastRunIndex >= firstRunIndex; lastRunIndex--)
+                                {
+                                    currentRun = _textRuns[lastRunIndex];
 
-                            currentShapedRun = shapedRun;
+                                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+                                    {
+                                        currentPosition += currentRun.Length;
 
-                            currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
+                                        if (currentRun is DrawableTextRun drawableTextRun)
+                                        {
+                                            currentX -= drawableTextRun.Size.Width;
+                                            directionalWidth -= drawableTextRun.Size.Width;
+                                        }
 
-                            rightToLeftRunBounds.Insert(0, currentRunBounds);
+                                        continue;
+                                    }
 
-                            remainingLength -= currentRunBounds.Length;
-                            startX = currentRunBounds.Rectangle.Left;
+                                    break;
+                                }
 
-                            currentPosition += currentRunBounds.Length;
-                        }
+                                break;
+                            }
+                        default:
+                            {
+                                for (; firstRunIndex <= lastRunIndex; firstRunIndex++)
+                                {
+                                    currentRun = _textRuns[firstRunIndex];
 
-                        combinedWidth = endX - startX;
+                                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
+                                    {
+                                        currentPosition += currentRun.Length;
 
-                        currentRect = new Rect(startX, 0, combinedWidth, Height);
+                                        if (currentRun is DrawableTextRun drawableTextRun)
+                                        {
+                                            currentX += drawableTextRun.Size.Width;
+                                            directionalWidth -= drawableTextRun.Size.Width;
+                                        }
 
-                        currentDirection = FlowDirection.RightToLeft;
+                                        continue;
+                                    }
 
-                        if (!MathUtilities.IsZero(combinedWidth))
-                        {
-                            result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
-                        }
+                                    break;
+                                }
 
-                        startX = endX;
+                                break;
+                            }
                     }
-                }
-                else
-                {
-                    if (currentPosition + currentRun.Length <= firstTextSourceIndex)
-                    {
-                        startX += currentRun.Size.Width;
 
-                        currentPosition += currentRun.Length;
+                    i = firstRunIndex;
 
+                    if (directionalWidth == 0)
+                    {
                         continue;
                     }
 
-                    if (currentPosition < firstTextSourceIndex)
-                    {
-                        startX += currentRun.Size.Width;
-                    }
+                    var coveredLength = 0;
+
+                    TextBounds? textBounds = null;
 
-                    if (currentPosition + currentRun.Length <= characterIndex)
+                    switch (currentDirection)
                     {
-                        endX += currentRun.Size.Width;
+                        case FlowDirection.LeftToRight:
+                            {
+                                textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex,
+                                        currentPosition, remainingLength, out coveredLength, out currentPosition);
+
+                                currentX -= directionalWidth;
+
+                                break;
+                            }
+                        default:
+                            {
+                                textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex,
+                                        currentPosition, remainingLength, out coveredLength, out currentPosition);
 
-                        characterLength = currentRun.Length;
+                                currentX = textBounds.Rectangle.Left;
+
+                                break;
+                            }
                     }
-                }
 
-                if (endX < startX)
-                {
-                    (endX, startX) = (startX, endX);
-                }
+                    //Visual order is always left to right so we need to insert
+                    result.Insert(0, textBounds);
 
-                //Lines that only contain a linebreak need to be covered here
-                if (characterLength == 0)
-                {
-                    characterLength = NewLineLength;
+                    remainingLength -= coveredLength;
+
+                    if (remainingLength <= 0)
+                    {
+                        break;
+                    }
                 }
+            }
 
-                combinedWidth = endX - startX;
+            return result;
+        }
 
-                currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
+        private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX,
+            int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
+        {
+            coveredLength = 0;
+            var textRunBounds = new List<TextRunBounds>();
+            var startX = endX;
 
-                currentPosition += characterLength;
+            for (int i = lastRunIndex; i >= firstRunIndex; i--)
+            {
+                var currentRun = _textRuns[i];
 
-                remainingLength -= characterLength;
+                if (currentRun is ShapedTextRun shapedTextRun)
+                {
+                    var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
 
-                startX = endX;
+                    textRunBounds.Insert(0, runBounds);
 
-                if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
-                {
-                    if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
+                    if (offset > 0)
                     {
-                        currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
+                        endX = runBounds.Rectangle.Right;
 
-                        var textBounds = result[result.Count - 1];
+                        startX = endX;
+                    }
 
-                        textBounds.Rectangle = currentRect;
+                    startX -= runBounds.Rectangle.Width;
 
-                        textBounds.TextRunBounds.Add(currentRunBounds);
-                    }
-                    else
+                    currentPosition += runBounds.Length + offset;
+
+                    coveredLength += runBounds.Length;
+
+                    remainingLength -= runBounds.Length;
+                }
+                else
+                {
+                    if (currentRun is DrawableTextRun drawableTextRun)
                     {
-                        currentRect = currentRunBounds.Rectangle;
+                        startX -= drawableTextRun.Size.Width;
 
-                        result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
+                        textRunBounds.Insert(0,
+                          new TextRunBounds(
+                              new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
                     }
-                }
 
-                lastRunBounds = currentRunBounds;
+                    currentPosition += currentRun.Length;
 
-                currentWidth += combinedWidth;
+                    coveredLength += currentRun.Length;
 
-                if (remainingLength <= 0 || currentPosition >= characterIndex)
+                    remainingLength -= currentRun.Length;
+                }
+
+                if (remainingLength <= 0)
                 {
                     break;
                 }
-
-                lastDirection = currentDirection;
             }
 
-            return result;
-        }
+            newPosition = currentPosition;
 
-        private IReadOnlyList<TextBounds> GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength)
-        {
-            var characterIndex = firstTextSourceIndex + textLength;
+            var runWidth = endX - startX;
 
-            var result = new List<TextBounds>(_textRuns.Length);
-            var lastDirection = FlowDirection.LeftToRight;
-            var currentDirection = lastDirection;
+            var bounds = new Rect(startX, 0, runWidth, Height);
 
-            var currentPosition = FirstTextSourceIndex;
-            var remainingLength = textLength;
+            return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds);
+        }
 
-            var startX = WidthIncludingTrailingWhitespace;
-            double currentWidth = 0;
-            var currentRect = default(Rect);
+        private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX,
+           int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition)
+        {
+            coveredLength = 0;
+            var textRunBounds = new List<TextRunBounds>();
+            var endX = startX;
 
-            for (var index = _textRuns.Length - 1; index >= 0; index--)
+            for (int i = firstRunIndex; i <= lastRunIndex; i++)
             {
-                if (_textRuns[index] is not DrawableTextRun currentRun)
-                {
-                    continue;
-                }
-
-                if (currentPosition + currentRun.Length < firstTextSourceIndex)
-                {
-                    startX -= currentRun.Size.Width;
-
-                    currentPosition += currentRun.Length;
-
-                    continue;
-                }
-
-                var characterLength = 0;
-                var endX = startX;
+                var currentRun = _textRuns[i];
 
-                if (currentRun is ShapedTextRun currentShapedRun)
+                if (currentRun is ShapedTextRun shapedTextRun)
                 {
-                    var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
-
-                    currentPosition += offset;
+                    var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset);
 
-                    var startIndex = currentPosition;
-                    double startOffset;
-                    double endOffset;
+                    textRunBounds.Add(runBounds);
 
-                    if (currentShapedRun.ShapedBuffer.IsLeftToRight)
+                    if (offset > 0)
                     {
-                        if (currentPosition < startIndex)
-                        {
-                            startOffset = endOffset = 0;
-                        }
-                        else
-                        {
-                            endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                        startX = runBounds.Rectangle.Left;
 
-                            startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
-                        }
-                    }
-                    else
-                    {
-                        endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
-
-                        startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+                        endX = startX;
                     }
 
-                    startX -= currentRun.Size.Width - startOffset;
-                    endX -= currentRun.Size.Width - endOffset;
+                    currentPosition += runBounds.Length + offset;
 
-                    var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-                    var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+                    endX += runBounds.Rectangle.Width;
 
-                    characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+                    coveredLength += runBounds.Length;
 
-                    currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ?
-                        FlowDirection.LeftToRight :
-                        FlowDirection.RightToLeft;
+                    remainingLength -= runBounds.Length;
                 }
                 else
                 {
-                    if (currentPosition + currentRun.Length <= characterIndex)
+                    if (currentRun is DrawableTextRun drawableTextRun)
                     {
-                        endX -= currentRun.Size.Width;
+                        textRunBounds.Add(
+                            new TextRunBounds(
+                                new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun));
+
+                        endX += drawableTextRun.Size.Width;
                     }
 
-                    if (currentPosition < firstTextSourceIndex)
-                    {
-                        startX -= currentRun.Size.Width;
+                    currentPosition += currentRun.Length;
 
-                        characterLength = currentRun.Length;
-                    }
-                }
+                    coveredLength += currentRun.Length;
 
-                if (endX < startX)
-                {
-                    (endX, startX) = (startX, endX);
+                    remainingLength -= currentRun.Length;
                 }
 
-                //Lines that only contain a linebreak need to be covered here
-                if (characterLength == 0)
+                if (remainingLength <= 0)
                 {
-                    characterLength = NewLineLength;
+                    break;
                 }
+            }
 
-                var runWidth = endX - startX;
+            newPosition = currentPosition;
 
-                var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
+            var runWidth = endX - startX;
 
-                if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0)
-                {
-                    if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX))
-                    {
-                        currentRect = currentRect.WithWidth(currentWidth + runWidth);
+            var bounds = new Rect(startX, 0, runWidth, Height);
 
-                        var textBounds = result[result.Count - 1];
+            return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds);
+        }
 
-                        textBounds.Rectangle = currentRect;
+        private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX,
+            int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
+        {
+            var startIndex = currentPosition;
 
-                        textBounds.TextRunBounds.Add(currentRunBounds);
-                    }
-                    else
-                    {
-                        currentRect = currentRunBounds.Rectangle;
+            offset = Math.Max(0, firstTextSourceIndex - currentPosition);
 
-                        result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
-                    }
-                }
+            var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
 
-                currentWidth += runWidth;
-                currentPosition += characterLength;
+            if (currentPosition != firstCluster)
+            {
+                startIndex = firstCluster + offset;
+            }
+            else
+            {
+                startIndex += offset;
+            }
 
-                if (currentPosition > characterIndex)
-                {
-                    break;
-                }
+            var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+            var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
 
-                lastDirection = currentDirection;
-                remainingLength -= characterLength;
+            var endX = startX + endOffset;
+            startX += startOffset;
 
-                if (remainingLength <= 0)
-                {
-                    break;
-                }
+            var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+            var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+
+            var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength);
+
+            if (endX < startX)
+            {
+                (endX, startX) = (startX, endX);
             }
 
-            result.Reverse();
+            //Lines that only contain a linebreak need to be covered here
+            if (characterLength == 0)
+            {
+                characterLength = NewLineLength;
+            }
 
-            return result;
+            var runWidth = endX - startX;
+
+            return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
         }
 
-        private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength)
+        private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX,
+            int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset)
         {
             var startX = endX;
 
-            var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
+            var startIndex = currentPosition;
 
-            currentPosition += offset;
+            offset = Math.Max(0, firstTextSourceIndex - currentPosition);
 
-            var startIndex = currentPosition;
+            var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster;
 
-            double startOffset;
-            double endOffset;
+            if (currentPosition != firstCluster)
+            {
+                startIndex = firstCluster + offset;
+            }
+            else
+            {
+                startIndex += offset;
+            }
 
-            endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
+            var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
 
-            startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
+            var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
 
             startX -= currentRun.Size.Width - startOffset;
             endX -= currentRun.Size.Width - endOffset;
@@ -968,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting
             return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun);
         }
 
-        public override IReadOnlyList<TextBounds> GetTextBounds(int firstTextSourceIndex, int textLength)
-        {
-            if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
-            {
-                return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength);
-            }
-
-            return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength);
-        }
-
         public override void Dispose()
         {
             for (int i = 0; i < _textRuns.Length; i++)
@@ -993,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting
         {
             _textLineMetrics = CreateLineMetrics();
 
+            if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
+            {
+                _textLineBreak = new TextLineBreak(textEndOfLine);
+            }
+
             BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
         }
 
@@ -1285,13 +1412,11 @@ namespace Avalonia.Media.TextFormatting
                 {
                     case ShapedTextRun textRun:
                         {
-                            var properties = textRun.Properties;
-                            var textMetrics =
-                                new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
+                            var textMetrics = textRun.TextMetrics;
 
-                            if (fontRenderingEmSize < properties.FontRenderingEmSize)
+                            if (fontRenderingEmSize < textMetrics.FontRenderingEmSize)
                             {
-                                fontRenderingEmSize = properties.FontRenderingEmSize;
+                                fontRenderingEmSize = textMetrics.FontRenderingEmSize;
 
                                 if (ascent > textMetrics.Ascent)
                                 {
@@ -1318,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
                                 trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
-                                newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
+                                newLineLength += textRun.GlyphRun.Metrics.NewLineLength;
                             }
 
                             widthIncludingWhitespace += textRun.Size.Width;
@@ -1330,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting
                         {
                             widthIncludingWhitespace += drawableTextRun.Size.Width;
 
-                            switch (_paragraphProperties.FlowDirection)
+                            if (index == lastRunIndex)
                             {
-                                case FlowDirection.LeftToRight:
-                                    {
-                                        if (index == lastRunIndex)
-                                        {
-                                            width = widthIncludingWhitespace;
-                                            trailingWhitespaceLength = 0;
-                                            newLineLength = 0;
-                                        }
-
-                                        break;
-                                    }
-
-                                case FlowDirection.RightToLeft:
-                                    {
-                                        if (index == lastRunIndex)
-                                        {
-                                            width = widthIncludingWhitespace;
-                                            trailingWhitespaceLength = 0;
-                                            newLineLength = 0;
-                                        }
-
-                                        break;
-                                    }
+                                width = widthIncludingWhitespace;
+                                trailingWhitespaceLength = 0;
                             }
 
                             if (drawableTextRun.Size.Height > height)

+ 30 - 0
src/Avalonia.Base/Media/TextFormatting/WrappingTextLineBreak.cs

@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Avalonia.Media.TextFormatting
+{
+    /// <summary>Represents a line break that occurred due to wrapping.</summary>
+    internal sealed class WrappingTextLineBreak : TextLineBreak
+    {
+        private List<TextRun>? _remainingRuns;
+
+        public WrappingTextLineBreak(TextEndOfLine? textEndOfLine, FlowDirection flowDirection,
+            List<TextRun> remainingRuns)
+            : base(textEndOfLine, flowDirection, isSplit: true)
+        {
+            Debug.Assert(remainingRuns.Count > 0);
+            _remainingRuns = remainingRuns;
+        }
+
+        /// <summary>
+        /// Gets the remaining runs from this line break, and clears them from this line break.
+        /// </summary>
+        /// <returns>A list of text runs.</returns>
+        public List<TextRun>? AcquireRemainingRuns()
+        {
+            var remainingRuns = _remainingRuns;
+            _remainingRuns = null;
+            return remainingRuns;
+        }
+    }
+}

+ 3 - 4
src/Avalonia.Base/Media/VisualBrush.cs

@@ -1,5 +1,4 @@
 using Avalonia.Media.Immutable;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Media
 {
@@ -11,8 +10,8 @@ namespace Avalonia.Media
         /// <summary>
         /// Defines the <see cref="Visual"/> property.
         /// </summary>
-        public static readonly StyledProperty<Visual> VisualProperty =
-            AvaloniaProperty.Register<VisualBrush, Visual>(nameof(Visual));
+        public static readonly StyledProperty<Visual?> VisualProperty =
+            AvaloniaProperty.Register<VisualBrush, Visual?>(nameof(Visual));
 
         static VisualBrush()
         {
@@ -38,7 +37,7 @@ namespace Avalonia.Media
         /// <summary>
         /// Gets or sets the visual to draw.
         /// </summary>
-        public Visual Visual
+        public Visual? Visual
         {
             get { return GetValue(VisualProperty); }
             set { SetValue(VisualProperty, value); }

+ 2 - 2
src/Avalonia.Base/Metadata/AmbientAttribute.cs

@@ -3,10 +3,10 @@ using System;
 namespace Avalonia.Metadata
 {
     /// <summary>
-    /// Defines the ambient class/property 
+    /// Defines the ambient class/property
     /// </summary>
     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)]
-    public class AmbientAttribute : Attribute
+    public sealed class AmbientAttribute : Attribute
     {
     }
 }

+ 1 - 1
src/Avalonia.Base/Metadata/ContentAttribute.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Metadata
     /// Defines the property that contains the object's content in markup.
     /// </summary>
     [AttributeUsage(AttributeTargets.Property)]
-    public class ContentAttribute : Attribute
+    public sealed class ContentAttribute : Attribute
     {
     }
 }

+ 2 - 2
src/Avalonia.Base/Metadata/DataTypeAttribute.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Metadata;
 /// Used on DataTemplate.DataType property so it can be inherited in compiled bindings inside of the template.
 /// </remarks>
 [AttributeUsage(AttributeTargets.Property)]
-public class DataTypeAttribute : Attribute
+public sealed class DataTypeAttribute : Attribute
 {
-    
+
 }

+ 1 - 1
src/Avalonia.Base/Metadata/DependsOnAttribute.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Metadata
     /// Indicates that the property depends on the value of another property in markup.
     /// </summary>
     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
-    public class DependsOnAttribute : Attribute
+    public sealed class DependsOnAttribute : Attribute
     {
         /// <summary>
         /// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.

+ 2 - 2
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@@ -25,9 +25,9 @@ public sealed class InheritDataTypeFromItemsAttribute : Attribute
     /// The name of the property whose item type should be used on the target property.
     /// </summary>
     public string AncestorItemsProperty { get; }
-    
+
     /// <summary>
-    /// The ancestor type to be used in a lookup for the <see cref="AncestorProperty"/>.
+    /// The ancestor type to be used in a lookup for the <see cref="AncestorItemsProperty"/>.
     /// If null, the declaring type of the target property is used.
     /// </summary>
     public Type? AncestorType { get; set; }

+ 1 - 1
src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Metadata
     /// may be added to its API.
     /// </remarks>
     [AttributeUsage(AttributeTargets.Interface)]
-    public class NotClientImplementableAttribute : Attribute
+    public sealed class NotClientImplementableAttribute : Attribute
     {
     }
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików