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

1
Avalonia.Desktop.slnf

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

13
Avalonia.sln

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

6
build/HarfBuzzSharp.props

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

2
build/ImageSharp.props

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

2
build/Moq.props

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

1
build/SharedVersion.props

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

6
build/SkiaSharp.props

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

15
build/XUnit.props

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

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

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

7
samples/BindingDemo/App.xaml

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

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

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

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

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

24
samples/ControlCatalog/App.xaml

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

48
samples/ControlCatalog/App.xaml.cs

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

1
samples/ControlCatalog/ControlCatalog.csproj

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

23
samples/ControlCatalog/MainView.xaml

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

39
samples/ControlCatalog/MainView.xaml.cs

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

6
samples/ControlCatalog/Models/CatalogTheme.cs

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

30
samples/ControlCatalog/Pages/DateTimePickerPage.xaml

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

22
samples/ControlCatalog/Pages/FlyoutsPage.axaml

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

1
samples/ControlCatalog/Pages/GesturePage.cs

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

2
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

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

16
samples/ControlCatalog/Pages/SplitViewPage.xaml

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

2
samples/ControlCatalog/Pages/TextBlockPage.xaml

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

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

6
samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs

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

7
samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs

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

50
samples/GpuInterop/VulkanDemo/VulkanContent.cs

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

12
samples/GpuInterop/VulkanDemo/VulkanContext.cs

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

12
samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs

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

18
samples/GpuInterop/VulkanDemo/VulkanImage.cs

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

8
samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs

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

5
samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs

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

2
samples/IntegrationTestApp/App.axaml

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

54
samples/IntegrationTestApp/MainWindow.axaml

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

91
samples/IntegrationTestApp/MainWindow.axaml.cs

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

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

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

5
samples/MobileSandbox/App.xaml

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

2
samples/PlatformSanityChecks/App.xaml

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

2
samples/Previewer/App.xaml

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

14
samples/RenderDemo/MainWindow.xaml

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

23
samples/RenderDemo/MainWindow.xaml.cs

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

45
samples/RenderDemo/ViewModels/MainWindowViewModel.cs

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

12
samples/SampleControls/HamburgerMenu/HamburgerMenu.cs

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

34
samples/SampleControls/HamburgerMenu/HamburgerMenu.xaml

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

2
samples/Sandbox/App.axaml

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

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

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

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

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

38
src/Avalonia.Base/AvaloniaObject.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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="hslColor">The parsed <see cref="HslColor"/>.</param>
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor)
public static bool TryParse(string? s, out HslColor hslColor)
{
bool prefixMatched = false;

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="hsvColor">The parsed <see cref="HsvColor"/>.</param>
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HsvColor hsvColor)
public static bool TryParse(string? s, out HsvColor hsvColor)
{
bool prefixMatched = false;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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,
/// in terms of where the previous line in the paragraph was broken by the text formatting process.</param>
/// <returns>The formatted line.</returns>
public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null);
}
}

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

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

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

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

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

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

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

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

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

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

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.
/// </summary>
[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.
/// </remarks>
[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.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class DependsOnAttribute : Attribute
public sealed class DependsOnAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DependsOnAttribute"/> class.

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.
/// </summary>
public string AncestorItemsProperty { get; }
/// <summary>
/// The ancestor type to be used in a lookup for the <see cref="AncestorProperty"/>.
/// The ancestor type to be used in a lookup for the <see cref="AncestorItemsProperty"/>.
/// If null, the declaring type of the target property is used.
/// </summary>
public Type? AncestorType { get; set; }

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

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

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

Loading…
Cancel
Save