Browse Source

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

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

25
.editorconfig

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

1
Avalonia.Desktop.slnf

@ -15,6 +15,7 @@
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
"src\\Avalonia.Controls.DataGrid\\Avalonia.Controls.DataGrid.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.Controls\\Avalonia.Controls.csproj",
"src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj", "src\\Avalonia.DesignerSupport\\Avalonia.DesignerSupport.csproj",
"src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj",

13
Avalonia.sln

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

6
build/HarfBuzzSharp.props

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

2
build/ImageSharp.props

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

2
build/Moq.props

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

1
build/SharedVersion.props

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

6
build/SkiaSharp.props

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

15
build/XUnit.props

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

2
native/Avalonia.Native/src/OSX/WindowImpl.mm

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

7
samples/BindingDemo/App.xaml

@ -2,13 +2,6 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="BindingDemo.App"> 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> <Application.Styles>
<FluentTheme /> <FluentTheme />
</Application.Styles> </Application.Styles>

4
samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj

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

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

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

24
samples/ControlCatalog/App.xaml

@ -6,18 +6,34 @@
x:Class="ControlCatalog.App"> x:Class="ControlCatalog.App">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<!-- Custom controls defined in other assemblies -->
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" /> <ResourceInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
</ResourceDictionary.MergedDictionaries> </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="DataGridFluent" Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
<StyleInclude x:Key="DataGridSimple" Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.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="ColorPickerFluent" Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
<StyleInclude x:Key="ColorPickerSimple" Source="avares://Avalonia.Controls.ColorPicker/Themes/Simple/Simple.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> </ResourceDictionary>
</Application.Resources> </Application.Resources>
<Application.Styles> <Application.Styles>

48
samples/ControlCatalog/App.xaml.cs

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

1
samples/ControlCatalog/ControlCatalog.csproj

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

23
samples/ControlCatalog/MainView.xaml

@ -14,8 +14,8 @@
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
</Style> </Style>
</Grid.Styles> </Grid.Styles>
<controls:HamburgerMenu Name="Sidebar"> <controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition"> <TabItem Header="Composition">
<pages:CompositionPage/> <pages:CompositionPage/>
</TabItem> </TabItem>
<TabItem Header="Acrylic"> <TabItem Header="Acrylic">
@ -168,6 +168,9 @@
<TabItem Header="TextBlock"> <TabItem Header="TextBlock">
<pages:TextBlockPage /> <pages:TextBlockPage />
</TabItem> </TabItem>
<TabItem Header="Theme Variants">
<pages:ThemePage />
</TabItem>
<TabItem Header="ToggleSwitch"> <TabItem Header="ToggleSwitch">
<pages:ToggleSwitchPage /> <pages:ToggleSwitchPage />
</TabItem> </TabItem>
@ -201,14 +204,22 @@
<SystemDecorations>Full</SystemDecorations> <SystemDecorations>Full</SystemDecorations>
</ComboBox.Items> </ComboBox.Items>
</ComboBox> </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" <ComboBox x:Name="Themes"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
SelectedIndex="0"> SelectedIndex="0">
<ComboBox.Items> <ComboBox.Items>
<models:CatalogTheme>FluentLight</models:CatalogTheme> <models:CatalogTheme>Fluent</models:CatalogTheme>
<models:CatalogTheme>FluentDark</models:CatalogTheme> <models:CatalogTheme>Simple</models:CatalogTheme>
<models:CatalogTheme>SimpleLight</models:CatalogTheme>
<models:CatalogTheme>SimpleDark</models:CatalogTheme>
</ComboBox.Items> </ComboBox.Items>
</ComboBox> </ComboBox>
<ComboBox x:Name="TransparencyLevels" <ComboBox x:Name="TransparencyLevels"

39
samples/ControlCatalog/MainView.xaml.cs

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

6
samples/ControlCatalog/Models/CatalogTheme.cs

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

30
samples/ControlCatalog/Pages/DateTimePickerPage.xaml

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

22
samples/ControlCatalog/Pages/FlyoutsPage.axaml

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

1
samples/ControlCatalog/Pages/GesturePage.cs

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

2
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -62,7 +62,7 @@
<Button x:Name="scrollToRandom">Scroll to Random</Button> <Button x:Name="scrollToRandom">Scroll to Random</Button>
<Button x:Name="scrollToSelected">Scroll to Selected</Button> <Button x:Name="scrollToSelected">Scroll to Selected</Button>
</StackPanel> </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" <ScrollViewer Name="scroller"
HorizontalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"> VerticalScrollBarVisibility="Auto">

16
samples/ControlCatalog/Pages/SplitViewPage.xaml

@ -32,7 +32,7 @@
<TextBlock Text="PaneBackground" /> <TextBlock Text="PaneBackground" />
<ComboBox Name="PaneBackgroundSelector" SelectedIndex="0" Width="170" Margin="10"> <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="Red">Red</ComboBoxItem>
<ComboBoxItem Tag="Blue">Blue</ComboBoxItem> <ComboBoxItem Tag="Blue">Blue</ComboBoxItem>
<ComboBoxItem Tag="Green">Green</ComboBoxItem> <ComboBoxItem Tag="Green">Green</ComboBoxItem>
@ -48,7 +48,7 @@
</StackPanel> </StackPanel>
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}" <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
BorderThickness="1"> BorderThickness="1">
<!--{Binding SelectedItem.Tag, ElementName=PaneBackgroundSelector}--> <!--{Binding SelectedItem.Tag, ElementName=PaneBackgroundSelector}-->
<SplitView Name="SplitView" <SplitView Name="SplitView"
@ -77,7 +77,7 @@
<Border Width="48"> <Border Width="48">
<Viewbox Width="24" Height="24" HorizontalAlignment="Left"> <Viewbox Width="24" Height="24" HorizontalAlignment="Left">
<Canvas Width="24" Height="24"> <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> </Canvas>
</Viewbox> </Viewbox>
</Border> </Border>
@ -89,11 +89,11 @@
</SplitView.Pane> </SplitView.Pane>
<Grid> <Grid>
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Center" VerticalAlignment="Center" 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 SystemControlForegroundBaseHighBrush}" /> <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 SystemControlForegroundBaseHighBrush}" /> <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 SystemControlForegroundBaseHighBrush}" /> <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 SystemControlForegroundBaseHighBrush}" /> <TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" HorizontalAlignment="Right" TextAlignment="Left" Foreground="{DynamicResource CatalogBaseHighColor}" />
</Grid> </Grid>
</SplitView> </SplitView>

2
samples/ControlCatalog/Pages/TextBlockPage.xaml

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

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

5
samples/GpuInterop/MainWindow.axaml.cs

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

6
samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs

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

7
samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs

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

50
samples/GpuInterop/VulkanDemo/VulkanContent.cs

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

12
samples/GpuInterop/VulkanDemo/VulkanContext.cs

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

12
samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs

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

18
samples/GpuInterop/VulkanDemo/VulkanImage.cs

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

8
samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs

@ -29,7 +29,7 @@ internal static class VulkanMemoryHelper
AccessFlags destinationAccessMask, AccessFlags destinationAccessMask,
uint mipLevels) 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 var barrier = new ImageMemoryBarrier
{ {
@ -46,8 +46,8 @@ internal static class VulkanMemoryHelper
api.CmdPipelineBarrier( api.CmdPipelineBarrier(
commandBuffer, commandBuffer,
PipelineStageFlags.PipelineStageAllCommandsBit, PipelineStageFlags.AllCommandsBit,
PipelineStageFlags.PipelineStageAllCommandsBit, PipelineStageFlags.AllCommandsBit,
0, 0,
0, 0,
null, null,
@ -56,4 +56,4 @@ internal static class VulkanMemoryHelper
1, 1,
barrier); barrier);
} }
} }

5
samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs

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

2
samples/IntegrationTestApp/App.axaml

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

54
samples/IntegrationTestApp/MainWindow.axaml

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

91
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -7,9 +7,13 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using Avalonia.Controls.Primitives.PopupPositioning;
namespace IntegrationTestApp 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() private void SendToBack()
{ {
var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!;
@ -175,6 +262,10 @@ namespace IntegrationTestApp
this.Get<ListBox>("BasicListBox").SelectedIndex = -1; this.Get<ListBox>("BasicListBox").SelectedIndex = -1;
if (source?.Name == "MenuClickedMenuItemReset") if (source?.Name == "MenuClickedMenuItemReset")
this.Get<TextBlock>("ClickedMenuItem").Text = "None"; this.Get<TextBlock>("ClickedMenuItem").Text = "None";
if (source?.Name == "ShowTransparentWindow")
ShowTransparentWindow();
if (source?.Name == "ShowTransparentPopup")
ShowTransparentPopup();
if (source?.Name == "ShowWindow") if (source?.Name == "ShowWindow")
ShowWindow(); ShowWindow();
if (source?.Name == "SendToBack") if (source?.Name == "SendToBack")

1
samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj

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

5
samples/MobileSandbox/App.xaml

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

2
samples/PlatformSanityChecks/App.xaml

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

2
samples/Previewer/App.xaml

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

14
samples/RenderDemo/MainWindow.xaml

@ -26,6 +26,20 @@
IsHitTestVisible="False" /> IsHitTestVisible="False" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </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>
<MenuItem Header="Tests"> <MenuItem Header="Tests">
<MenuItem Command="{Binding ResizeWindow}" Header="Resize window" /> <MenuItem Command="{Binding ResizeWindow}" Header="Resize window" />

23
samples/RenderDemo/MainWindow.xaml.cs

@ -1,7 +1,9 @@
using System; using System;
using System.Linq.Expressions;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using RenderDemo.ViewModels; using RenderDemo.ViewModels;
using MiniMvvm; using MiniMvvm;
@ -11,13 +13,26 @@ namespace RenderDemo
{ {
public MainWindow() public MainWindow()
{ {
this.InitializeComponent(); InitializeComponent();
this.AttachDevTools(); this.AttachDevTools();
var vm = new MainWindowViewModel(); var vm = new MainWindowViewModel();
vm.WhenAnyValue(x => x.DrawDirtyRects).Subscribe(x => Renderer.DrawDirtyRects = x);
vm.WhenAnyValue(x => x.DrawFps).Subscribe(x => Renderer.DrawFps = x); void BindOverlay(Expression<Func<MainWindowViewModel, bool>> expr, RendererDebugOverlays overlay)
this.DataContext = vm; => 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() private void InitializeComponent()

45
samples/RenderDemo/ViewModels/MainWindowViewModel.cs

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

12
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

@ -52,6 +52,14 @@ namespace ControlSamples
var (oldBounds, newBounds) = change.GetOldAndNewValue<Rect>(); var (oldBounds, newBounds) = change.GetOldAndNewValue<Rect>();
EnsureSplitViewMode(oldBounds, newBounds); 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) private void EnsureSplitViewMode(Rect oldBounds, Rect newBounds)
@ -60,12 +68,12 @@ namespace ControlSamples
{ {
var threshold = ExpandedModeThresholdWidth; var threshold = ExpandedModeThresholdWidth;
if (newBounds.Width >= threshold && oldBounds.Width < threshold) if (newBounds.Width >= threshold)
{ {
_splitView.DisplayMode = SplitViewDisplayMode.Inline; _splitView.DisplayMode = SplitViewDisplayMode.Inline;
_splitView.IsPaneOpen = true; _splitView.IsPaneOpen = true;
} }
else if (newBounds.Width < threshold && oldBounds.Width >= threshold) else if (newBounds.Width < threshold)
{ {
_splitView.DisplayMode = SplitViewDisplayMode.Overlay; _splitView.DisplayMode = SplitViewDisplayMode.Overlay;
_splitView.IsPaneOpen = false; _splitView.IsPaneOpen = false;

34
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

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

2
samples/Sandbox/App.axaml

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

39
src/Avalonia.Base/Animation/Animatable.cs

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

11
src/Avalonia.Base/Animation/KeySpline.cs

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

38
src/Avalonia.Base/AvaloniaObject.cs

@ -152,7 +152,7 @@ namespace Avalonia
property = property ?? throw new ArgumentNullException(nameof(property)); property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess(); VerifyAccess();
_values?.ClearLocalValue(property); _values.ClearLocalValue(property);
} }
/// <summary> /// <summary>
@ -242,7 +242,14 @@ namespace Avalonia
return registered.InvokeGetter(this); 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) public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
{ {
_ = property ?? throw new ArgumentNullException(nameof(property)); _ = property ?? throw new ArgumentNullException(nameof(property));
@ -261,7 +268,7 @@ namespace Avalonia
VerifyAccess(); VerifyAccess();
return _values?.IsAnimating(property) ?? false; return _values.IsAnimating(property);
} }
/// <summary> /// <summary>
@ -279,7 +286,7 @@ namespace Avalonia
VerifyAccess(); VerifyAccess();
return _values?.IsSet(property) ?? false; return _values.IsSet(property);
} }
/// <summary> /// <summary>
@ -515,14 +522,12 @@ namespace Avalonia
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property); public void CoerceValue(AvaloniaProperty property) => _values.CoerceValue(property);
/// <inheritdoc/>
internal void AddInheritanceChild(AvaloniaObject child) internal void AddInheritanceChild(AvaloniaObject child)
{ {
_inheritanceChildren ??= new List<AvaloniaObject>(); _inheritanceChildren ??= new List<AvaloniaObject>();
_inheritanceChildren.Add(child); _inheritanceChildren.Add(child);
} }
/// <inheritdoc/>
internal void RemoveInheritanceChild(AvaloniaObject child) internal void RemoveInheritanceChild(AvaloniaObject child)
{ {
_inheritanceChildren?.Remove(child); _inheritanceChildren?.Remove(child);
@ -541,24 +546,11 @@ namespace Avalonia
return new AvaloniaPropertyValue( return new AvaloniaPropertyValue(
property, property,
GetValue(property), GetValue(property),
BindingPriority.Unset, BindingPriority.LocalValue,
"Local Value"); null);
}
else if (_values != null)
{
var result = _values.GetDiagnostic(property);
if (result != null)
{
return result;
}
} }
return new AvaloniaPropertyValue( return _values.GetDiagnostic(property);
property,
GetValue(property),
BindingPriority.Unset,
"Unset");
} }
internal ValueStore GetValueStore() => _values; internal ValueStore GetValueStore() => _values;

18
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

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

112
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
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
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
src/Avalonia.Base/Controls/IResourceDictionary.cs

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Styling;
#nullable enable #nullable enable
@ -13,5 +14,10 @@ namespace Avalonia.Controls
/// Gets a collection of child resource dictionaries. /// Gets a collection of child resource dictionaries.
/// </summary> /// </summary>
IList<IResourceProvider> MergedDictionaries { get; } 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; }
} }
} }

7
src/Avalonia.Base/Controls/IResourceNode.cs

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

91
src/Avalonia.Base/Controls/ResourceDictionary.cs

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

125
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@ -1,6 +1,4 @@
using System; using System;
using Avalonia.Data.Converters;
using Avalonia.LogicalTree;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Styling; using Avalonia.Styling;
@ -41,21 +39,66 @@ namespace Avalonia.Controls
control = control ?? throw new ArgumentNullException(nameof(control)); control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key)); 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) while (current != null)
{ {
if (current.TryGetResource(key, out value)) if (current.TryGetResource(key, theme, out value))
{ {
return true; return true;
} }
current = (current as IStyleHost)?.StylingParent as IResourceNode; current = (current as IStyleHost)?.StylingParent as IResourceHost;
} }
value = null; value = null;
return false; 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( public static IObservable<object?> GetResourceObservable(
this IResourceHost control, this IResourceHost control,
@ -95,24 +138,49 @@ namespace Avalonia.Controls
protected override void Initialize() protected override void Initialize()
{ {
_target.ResourcesChanged += ResourcesChanged; _target.ResourcesChanged += ResourcesChanged;
if (_target is StyledElement themeStyleable)
{
themeStyleable.PropertyChanged += PropertyChanged;
}
} }
protected override void Deinitialize() protected override void Deinitialize()
{ {
_target.ResourcesChanged -= ResourcesChanged; _target.ResourcesChanged -= ResourcesChanged;
if (_target is StyledElement themeStyleable)
{
themeStyleable.PropertyChanged -= PropertyChanged;
}
} }
protected override void Subscribed(IObserver<object?> observer, bool first) 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) 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?> private class FloatingResourceObservable : LightweightObservableBase<object?>
@ -134,7 +202,7 @@ namespace Avalonia.Controls
_target.OwnerChanged += OwnerChanged; _target.OwnerChanged += OwnerChanged;
_owner = _target.Owner; _owner = _target.Owner;
if (_owner is object) if (_owner is not null)
{ {
_owner.ResourcesChanged += ResourcesChanged; _owner.ResourcesChanged += ResourcesChanged;
} }
@ -148,43 +216,68 @@ namespace Avalonia.Controls
protected override void Subscribed(IObserver<object?> observer, bool first) 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() 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) private void OwnerChanged(object? sender, EventArgs e)
{ {
if (_owner is object) if (_owner is not null)
{ {
_owner.ResourcesChanged -= ResourcesChanged; _owner.ResourcesChanged -= ResourcesChanged;
} }
if (_owner is StyledElement styleable)
{
styleable.PropertyChanged += PropertyChanged;
}
_owner = _target.Owner; _owner = _target.Owner;
if (_owner is object) if (_owner is not null)
{ {
_owner.ResourcesChanged += ResourcesChanged; _owner.ResourcesChanged += ResourcesChanged;
} }
if (_owner is StyledElement styleable2)
{
styleable2.PropertyChanged += PropertyChanged;
}
PublishNext(); PublishNext();
} }
private void PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == StyledElement.ActualThemeVariantProperty)
{
PublishNext();
}
}
private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e)
{ {
PublishNext(); 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;
}
} }
} }
} }

2
src/Avalonia.Base/Data/InstancedBinding.cs

@ -23,7 +23,7 @@ namespace Avalonia.Data
/// <param name="priority">The priority of the binding.</param> /// <param name="priority">The priority of the binding.</param>
/// <remarks> /// <remarks>
/// This constructor can be used to create any type of binding and as such requires an /// 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 /// 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. /// something other than a subject, use one of the static creation methods on this class.
/// </remarks> /// </remarks>

3
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

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

18
src/Avalonia.Base/Input/DragEventArgs.cs

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

2
src/Avalonia.Base/Input/KeyGesture.cs

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

44
src/Avalonia.Base/Input/KeyboardNavigationHandler.cs

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

48
src/Avalonia.Base/Input/Navigation/TabNavigation.cs

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

20
src/Avalonia.Base/Layout/LayoutManager.cs

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

6
src/Avalonia.Base/LogicalTree/LogicalExtensions.cs

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

9
src/Avalonia.Base/Media/Color.cs

@ -147,16 +147,11 @@ namespace Avalonia.Media
/// <param name="s">The color string.</param> /// <param name="s">The color string.</param>
/// <param name="color">The parsed color</param> /// <param name="color">The parsed color</param>
/// <returns>The status of the operation.</returns> /// <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; color = default;
if (s is null) if (string.IsNullOrEmpty(s))
{
return false;
}
if (s.Length == 0)
{ {
return false; return false;
} }

2
src/Avalonia.Base/Media/DrawingContext.cs

@ -240,7 +240,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <param name="foreground">The foreground brush.</param> /// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</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)); _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));

26
src/Avalonia.Base/Media/DrawingGroup.cs

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

6
src/Avalonia.Base/Media/DrawingImage.cs

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

4
src/Avalonia.Base/Media/FontFamily.cs

@ -119,7 +119,7 @@ namespace Avalonia.Media
case 2: 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.Relative)
: new Uri(segments[0], UriKind.RelativeOrAbsolute); : new Uri(segments[0], UriKind.RelativeOrAbsolute);
@ -188,7 +188,7 @@ namespace Avalonia.Media
{ {
unchecked unchecked
{ {
return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0); return (FamilyNames.GetHashCode() * 397) ^ (Key is not null ? Key.GetHashCode() : 0);
} }
} }

5
src/Avalonia.Base/Media/Fonts/FontFamilyKey.cs

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

38
src/Avalonia.Base/Media/FormattedText.cs

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

6
src/Avalonia.Base/Media/GeometryDrawing.cs

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

2
src/Avalonia.Base/Media/GlyphRun.cs

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

12
src/Avalonia.Base/Media/GlyphRunDrawing.cs

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

2
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="s">The HSL color string to parse.</param>
/// <param name="hslColor">The parsed <see cref="HslColor"/>.</param> /// <param name="hslColor">The parsed <see cref="HslColor"/>.</param>
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <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; bool prefixMatched = false;

2
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="s">The HSV color string to parse.</param>
/// <param name="hsvColor">The parsed <see cref="HsvColor"/>.</param> /// <param name="hsvColor">The parsed <see cref="HsvColor"/>.</param>
/// <returns>True if parsing was successful; otherwise, false.</returns> /// <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; bool prefixMatched = false;

3
src/Avalonia.Base/Media/IVisualBrush.cs

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

24
src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs

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

7
src/Avalonia.Base/Media/Immutable/ImmutableVisualBrush.cs

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

14
src/Avalonia.Base/Media/TextDecoration.cs

@ -22,8 +22,8 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Defines the <see cref="Stroke"/> property. /// Defines the <see cref="Stroke"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBrush> StrokeProperty = public static readonly StyledProperty<IBrush?> StrokeProperty =
AvaloniaProperty.Register<TextDecoration, IBrush>(nameof(Stroke)); AvaloniaProperty.Register<TextDecoration, IBrush?>(nameof(Stroke));
/// <summary> /// <summary>
/// Defines the <see cref="StrokeThicknessUnit"/> property. /// Defines the <see cref="StrokeThicknessUnit"/> property.
@ -34,8 +34,8 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Defines the <see cref="StrokeDashArray"/> property. /// Defines the <see cref="StrokeDashArray"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<AvaloniaList<double>> StrokeDashArrayProperty = public static readonly StyledProperty<AvaloniaList<double>?> StrokeDashArrayProperty =
AvaloniaProperty.Register<TextDecoration, AvaloniaList<double>>(nameof(StrokeDashArray)); AvaloniaProperty.Register<TextDecoration, AvaloniaList<double>?>(nameof(StrokeDashArray));
/// <summary> /// <summary>
/// Defines the <see cref="StrokeDashOffset"/> property. /// Defines the <see cref="StrokeDashOffset"/> property.
@ -82,7 +82,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the <see cref="IBrush"/> that specifies how the <see cref="TextDecoration"/> is painted. /// Gets or sets the <see cref="IBrush"/> that specifies how the <see cref="TextDecoration"/> is painted.
/// </summary> /// </summary>
public IBrush Stroke public IBrush? Stroke
{ {
get { return GetValue(StrokeProperty); } get { return GetValue(StrokeProperty); }
set { SetValue(StrokeProperty, value); } 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 /// 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"/>. /// that is used to draw the <see cref="TextDecoration"/>.
/// </summary> /// </summary>
public AvaloniaList<double> StrokeDashArray public AvaloniaList<double>? StrokeDashArray
{ {
get { return GetValue(StrokeDashArrayProperty); } get { return GetValue(StrokeDashArrayProperty); }
set { SetValue(StrokeDashArrayProperty, value); } 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)); 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 last = baselineOrigin.X;
var finalPos = last + glyphRun.Size.Width; var finalPos = last + glyphRun.Size.Width;

4
src/Avalonia.Base/Media/TextFormatting/ITextSource.cs

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

16
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

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

36
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

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

2
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, /// <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> /// in terms of where the previous line in the paragraph was broken by the text formatting process.</param>
/// <returns>The formatted line.</returns> /// <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); TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null);
} }
} }

209
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

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

54
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

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

15
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 class TextLineBreak
{ {
public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight, public TextLineBreak(TextEndOfLine? textEndOfLine = null,
IReadOnlyList<TextRun>? remainingRuns = null) FlowDirection flowDirection = FlowDirection.LeftToRight, bool isSplit = false)
{ {
TextEndOfLine = textEndOfLine; TextEndOfLine = textEndOfLine;
FlowDirection = flowDirection; FlowDirection = flowDirection;
RemainingRuns = remainingRuns; IsSplit = isSplit;
} }
/// <summary> /// <summary>
@ -23,8 +21,9 @@ namespace Avalonia.Media.TextFormatting
public FlowDirection FlowDirection { get; } public FlowDirection FlowDirection { get; }
/// <summary> /// <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> /// </summary>
public IReadOnlyList<TextRun>? RemainingRuns { get; } public bool IsSplit { get; }
} }
} }

692
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

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

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

7
src/Avalonia.Base/Media/VisualBrush.cs

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

4
src/Avalonia.Base/Metadata/AmbientAttribute.cs

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

2
src/Avalonia.Base/Metadata/ContentAttribute.cs

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

4
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. /// Used on DataTemplate.DataType property so it can be inherited in compiled bindings inside of the template.
/// </remarks> /// </remarks>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public class DataTypeAttribute : Attribute public sealed class DataTypeAttribute : Attribute
{ {
} }

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

4
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. /// The name of the property whose item type should be used on the target property.
/// </summary> /// </summary>
public string AncestorItemsProperty { get; } public string AncestorItemsProperty { get; }
/// <summary> /// <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. /// If null, the declaring type of the target property is used.
/// </summary> /// </summary>
public Type? AncestorType { get; set; } public Type? AncestorType { get; set; }

2
src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs

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

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

Loading…
Cancel
Save