Browse Source

Merge branch 'master' into colorspectrum

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

30
Avalonia.sln

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

9
build/DevAnalyzers.props

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

10
native/Avalonia.Native/src/OSX/window.mm

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

42
samples/ControlCatalog.Web/ControlCatalog.Web.csproj

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

28
samples/ControlCatalog.Web/LinkerConfig.xml

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

8
samples/ControlCatalog/MainView.xaml

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

9
samples/ControlCatalog/MainView.xaml.cs

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

32
samples/ControlCatalog/Pages/ButtonsPage.xaml

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

2
samples/ControlCatalog/Pages/CanvasPage.xaml

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

2
samples/ControlCatalog/Pages/ScreenPage.cs

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

1
src/Android/Avalonia.Android/Avalonia.Android.csproj

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

1
src/Avalonia.Animation/Avalonia.Animation.csproj

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

1
src/Avalonia.Base/Avalonia.Base.csproj

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

3
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

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

1
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

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

19
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -2060,6 +2060,25 @@ namespace Avalonia.Controls
forceHorizontalScroll: true);
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (DataConnection.DataSource != null && !DataConnection.EventsWired)
{
DataConnection.WireEvents(DataConnection.DataSource);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
// When wired to INotifyCollectionChanged, the DataGrid will be cleaned up by GC
if (DataConnection.DataSource != null && DataConnection.EventsWired)
{
DataConnection.UnWireEvents(DataConnection.DataSource);
}
}
/// <summary>
/// Arranges the content of the <see cref="T:Avalonia.Controls.DataGridRow" />.

9
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -1,4 +1,6 @@
Compat issues with assembly Avalonia.Controls:
TypesMustExist : Type 'Avalonia.Controls.DropDown' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Controls.DropDownItem' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Controls.IMenuItem.StaysOpenOnClick.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.IMenuItem.StaysOpenOnClick.set(System.Boolean)' is present in the implementation but not in the contract.
@ -34,6 +36,11 @@ MembersMustExist : Member 'public Avalonia.AttachedProperty<Avalonia.Media.FontS
MembersMustExist : Member 'public Avalonia.AttachedProperty<Avalonia.Media.FontWeight> Avalonia.AttachedProperty<Avalonia.Media.FontWeight> Avalonia.Controls.TextBlock.FontWeightProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AttachedProperty<Avalonia.Media.IBrush> Avalonia.AttachedProperty<Avalonia.Media.IBrush> Avalonia.Controls.TextBlock.ForegroundProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AttachedProperty<System.Double> Avalonia.AttachedProperty<System.Double> Avalonia.Controls.TextBlock.FontSizeProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Media.TextAlignment> Avalonia.StyledProperty<Avalonia.Media.TextAlignment> Avalonia.Controls.TextBlock.TextAlignmentProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Media.TextTrimming> Avalonia.StyledProperty<Avalonia.Media.TextTrimming> Avalonia.Controls.TextBlock.TextTrimmingProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Media.TextWrapping> Avalonia.StyledProperty<Avalonia.Media.TextWrapping> Avalonia.Controls.TextBlock.TextWrappingProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Double> Avalonia.StyledProperty<System.Double> Avalonia.Controls.TextBlock.LineHeightProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.StyledProperty<System.Int32> Avalonia.StyledProperty<System.Int32> Avalonia.Controls.TextBlock.MaxLinesProperty' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.FontFamily Avalonia.Controls.TextBlock.GetFontFamily(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Double Avalonia.Controls.TextBlock.GetFontSize(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.FontStyle Avalonia.Controls.TextBlock.GetFontStyle(Avalonia.Controls.Control)' does not exist in the implementation but it does exist in the contract.
@ -87,4 +94,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 88
Total Issues: 95

1
src/Avalonia.Controls/Avalonia.Controls.csproj

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

123
src/Avalonia.Controls/Button.cs

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

19
src/Avalonia.Controls/ComboBox.cs

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

68
src/Avalonia.Controls/Control.cs

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

28
src/Avalonia.Controls/DropDown.cs

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

15
src/Avalonia.Controls/DropDownButton.cs

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

67
src/Avalonia.Controls/HotkeyManager.cs

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

2
src/Avalonia.Controls/Image.cs

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

10
src/Avalonia.Controls/MenuItem.cs

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

168
src/Avalonia.Controls/Presenters/ContentPresenter.cs

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

2
src/Avalonia.Controls/Presenters/TextPresenter.cs

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

2
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

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

30
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

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

2
src/Avalonia.Controls/Primitives/PopupRoot.cs

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

5
src/Avalonia.Controls/Primitives/TemplatedControl.cs

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

32
src/Avalonia.Controls/SplitButton/SplitButton.cs

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

178
src/Avalonia.Controls/TextBlock.cs

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

2
src/Avalonia.Controls/TextBox.cs

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

6
src/Avalonia.Controls/TopLevel.cs

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

1
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

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

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

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

1
src/Avalonia.Input/Avalonia.Input.csproj

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

18
src/Avalonia.Input/IClickableControl.cs

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

4
src/Avalonia.Input/Properties/AssemblyInfo.cs

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

1
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

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

1
src/Avalonia.Layout/Avalonia.Layout.csproj

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

2
src/Avalonia.Native/Avalonia.Native.csproj

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

4
src/Avalonia.OpenGL/Avalonia.OpenGL.csproj

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

1
src/Avalonia.Styling/Avalonia.Styling.csproj

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

1
src/Avalonia.Themes.Default/Controls/CheckBox.xaml

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

67
src/Avalonia.Themes.Default/Controls/DropDownButton.xaml

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

4
src/Avalonia.Themes.Default/Controls/SplitButton.xaml

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

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

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

28
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@ -4,7 +4,7 @@
<StackPanel Spacing="20">
<Button Content="Click Me!" />
<Button Classes="accent" Content="Click Me!" />
</StackPanel>
</StackPanel>
</Border>
</Design.PreviewWith>
<Styles.Resources>
@ -19,9 +19,7 @@
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="Padding" Value="{DynamicResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="VerticalAlignment" Value="Center" />
<!--<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-3" />-->
<Setter Property="Template">
@ -45,40 +43,40 @@
<Style Selector="Button:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPointerOver}" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
<Style Selector="Button.accent /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForeground}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
</Style>
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPointerOver}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPointerOver}" />
</Style>
<Style Selector="Button.accent:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushPressed}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
</Style>
<Style Selector="Button, RepeatButton, ToggleButton">
<Style Selector="Button, RepeatButton, ToggleButton, DropDownButton">
<Setter Property="RenderTransform" Value="none" />
<Setter Property="Transitions">
<Transitions>
@ -87,13 +85,13 @@
</Setter>
</Style>
<Style Selector="Button:pressed, RepeatButton:pressed, ToggleButton:pressed">
<Style Selector="Button:pressed, RepeatButton:pressed, ToggleButton:pressed, DropDownButton:pressed">
<Setter Property="RenderTransform" Value="scale(0.98)" />
</Style>
<Style Selector="Button.accent:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />
</Style>
</Styles>

6
src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml

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

19
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@ -76,7 +76,7 @@
<!-- Unchecked PointerOver State -->
<Style Selector="CheckBox:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:pointerover /template/ Border#PART_Border">
@ -95,7 +95,7 @@
<!-- Unchecked Pressed State -->
<Style Selector="CheckBox:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedPressed}" />
</Style>
<Style Selector="CheckBox:pressed /template/ Border#PART_Border">
@ -114,7 +114,7 @@
<!-- Unchecked Disabled state -->
<Style Selector="CheckBox:disabled /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundUncheckedDisabled}" />
</Style>
<Style Selector="CheckBox:disabled /template/ Border#PART_Border">
@ -152,11 +152,12 @@
<Setter Property="Data" Value="M1507 31L438 1101L-119 543L-29 453L438 919L1417 -59L1507 31Z" />
<Setter Property="Width" Value="9" />
<Setter Property="Opacity" Value="1" />
<Setter Property="FlowDirection" Value="LeftToRight" />
</Style>
<!-- Checked PointerOver State -->
<Style Selector="CheckBox:checked:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPointerOver}" />
</Style>
<Style Selector="CheckBox:checked:pointerover /template/ Border#PART_Border">
@ -175,7 +176,7 @@
<!-- Checked Pressed State -->
<Style Selector="CheckBox:checked:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedPressed}" />
</Style>
<Style Selector="CheckBox:checked:pressed /template/ Border#PART_Border">
@ -194,7 +195,7 @@
<!-- Checked Disabled State -->
<Style Selector="CheckBox:checked:disabled /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundCheckedDisabled}" />
</Style>
<Style Selector="CheckBox:checked:disabled /template/ Border#PART_Border">
@ -236,7 +237,7 @@
<!-- Indeterminate PointerOver State -->
<Style Selector="CheckBox:indeterminate:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePointerOver}" />
</Style>
<Style Selector="CheckBox:indeterminate:pointerover /template/ Border#PART_Border">
@ -255,7 +256,7 @@
<!-- Indeterminate Pressed State -->
<Style Selector="CheckBox:indeterminate:pressed /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminatePressed}" />
</Style>
<Style Selector="CheckBox:indeterminate:pressed /template/ Border#PART_Border">
@ -274,7 +275,7 @@
<!-- Indeterminate Disabled State -->
<Style Selector="CheckBox:indeterminate:disabled /template/ ContentPresenter#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource CheckBoxForegroundIndeterminateDisabled}" />
</Style>
<Style Selector="CheckBox:indeterminate:disabled /template/ Border#PART_Border">

11
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -37,6 +37,7 @@
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="MinHeight" Value="{DynamicResource ComboBoxMinHeight}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Left" />
@ -53,7 +54,7 @@
Grid.Column="0"
Grid.ColumnSpan="2"
IsVisible="False"
TextElement.FontWeight="{DynamicResource ComboBoxHeaderThemeFontWeight}"
FontWeight="{DynamicResource ComboBoxHeaderThemeFontWeight}"
Margin="{DynamicResource ComboBoxTopHeaderMargin}"
VerticalAlignment="Top" />
<Border x:Name="Background"
@ -177,11 +178,11 @@
</Style>
<Style Selector="ComboBox:disabled /template/ ContentPresenter#HeaderContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
</Style>
<Style Selector="ComboBox:disabled /template/ ContentControl#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundDisabled}" />
</Style>
<Style Selector="ComboBox:disabled /template/ TextBlock#PlaceholderTextBlock">
@ -199,7 +200,7 @@
</Style>
<Style Selector="ComboBox:focus-visible /template/ ContentControl#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundFocused}" />
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundFocused}" />
</Style>
<Style Selector="ComboBox:focus-visible /template/ TextBlock#PlaceholderTextBlock">
@ -212,7 +213,7 @@
<!-- Focus Pressed State -->
<Style Selector="ComboBox:focused:pressed /template/ ContentControl#ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{DynamicResource ComboBoxForegroundFocusedPressed}" />
<Setter Property="Foreground" Value="{DynamicResource ComboBoxForegroundFocusedPressed}" />
</Style>
<Style Selector="ComboBox:focused:pressed /template/ TextBlock#PlaceholderTextBlock">

16
src/Avalonia.Themes.Fluent/Controls/ComboBoxItem.xaml

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

18
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

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

103
src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml

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

2
src/Avalonia.Themes.Fluent/Controls/Expander.xaml

@ -110,7 +110,7 @@
BorderThickness="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
TextElement.Foreground="{DynamicResource ExpanderForeground}" />
Foreground="{DynamicResource ExpanderForeground}" />
<Border x:Name="ExpandCollapseChevronBorder"
Grid.Column="1"
Width="32"

1
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

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

18
src/Avalonia.Themes.Fluent/Controls/ListBoxItem.xaml

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

6
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

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

2
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

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

8
src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml

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

6
src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml

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

2
src/Avalonia.Themes.Fluent/Controls/Slider.xaml

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

10
src/Avalonia.Themes.Fluent/Controls/SplitButton.xaml

@ -24,6 +24,7 @@
<x:Double x:Key="SplitButtonPrimaryButtonSize">32</x:Double>
<x:Double x:Key="SplitButtonSecondaryButtonSize">32</x:Double>
<x:Double x:Key="SplitButtonSeparatorWidth">1</x:Double>
<x:Double x:Key="SplitButtonMinHeight">32</x:Double>
<converters:MarginMultiplierConverter x:Key="PrimaryButtonBorderMultiplier" Left="True" Top="True" Bottom="True" Indent="1" />
<converters:MarginMultiplierConverter x:Key="SecondaryButtonBorderMultiplier" Right="True" Top="True" Bottom="True" Indent="1" />
@ -35,8 +36,11 @@
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource SplitButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource SplitButtonBorderThemeThickness}" />
<Setter Property="MinHeight" Value="{DynamicResource SplitButtonMinHeight}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<!--<Setter Property="UseSystemFocusVisuals" Value="True" />
<Setter Property="FocusVisualMargin" Value="-3" />-->
<Setter Property="KeyboardNavigation.IsTabStop" Value="True" />
@ -122,7 +126,7 @@
SplitButton /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter">
<Setter Property="Border.Background" Value="{DynamicResource SplitButtonBackgroundPointerOver}" />
<Setter Property="Border.BorderBrush" Value="{DynamicResource SplitButtonBorderBrushPointerOver}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource SplitButtonForegroundPointerOver}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPointerOver}" />
</Style>
<Style Selector="SplitButton /template/ Button#PART_SecondaryButton:pointerover PathIcon">
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPointerOver}" />
@ -133,7 +137,7 @@
SplitButton /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter">
<Setter Property="Border.Background" Value="{DynamicResource SplitButtonBackgroundPressed}" />
<Setter Property="Border.BorderBrush" Value="{DynamicResource SplitButtonBorderBrushPressed}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
</Style>
<Style Selector="SplitButton /template/ Button#PART_SecondaryButton:pressed PathIcon">
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundPressed}" />
@ -203,7 +207,7 @@
SplitButton:checked /template/ Button#PART_SecondaryButton:pressed /template/ ContentPresenter">
<Setter Property="Border.Background" Value="{DynamicResource SplitButtonBackgroundCheckedPressed}" />
<Setter Property="Border.BorderBrush" Value="{DynamicResource SplitButtonBorderBrushCheckedPressed}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />
</Style>
<Style Selector="SplitButton:checked /template/ Button#PART_SecondaryButton:pressed PathIcon">
<Setter Property="Foreground" Value="{DynamicResource SplitButtonForegroundCheckedPressed}" />

6
src/Avalonia.Themes.Fluent/Controls/TabItem.xaml

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

6
src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml

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

12
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

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

22
src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml

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

14
src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml

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

4
src/Avalonia.Visuals/ApiCompatBaseline.txt

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

1
src/Avalonia.Visuals/Avalonia.Visuals.csproj

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

442
src/Avalonia.Visuals/Media/Color.cs

@ -1,3 +1,10 @@
// Color conversion portions of this source file are adapted from the WinUI project
// (https://github.com/microsoft/microsoft-ui-xaml)
// and the Windows Community Toolkit project.
// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Globalization;
#if !BUILDTASK
@ -14,6 +21,8 @@ namespace Avalonia.Media
#endif
readonly struct Color : IEquatable<Color>
{
private const double byteToDouble = 1.0 / 255;
static Color()
{
#if !BUILDTASK
@ -41,6 +50,13 @@ namespace Avalonia.Media
/// </summary>
public byte B { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Color"/> struct.
/// </summary>
/// <param name="a">The alpha component.</param>
/// <param name="r">The red component.</param>
/// <param name="g">The green component.</param>
/// <param name="b">The blue component.</param>
public Color(byte a, byte r, byte g, byte b)
{
A = a;
@ -144,17 +160,46 @@ namespace Avalonia.Media
return false;
}
if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
if (s[0] == '#' &&
TryParseHexFormat(s.AsSpan(), out color))
{
return true;
}
if (s.Length > 5 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
TryParseCssFormat(s, out color))
{
return true;
}
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
HslColor.TryParse(s, out HslColor hslColor))
{
color = hslColor.ToRgb();
return true;
}
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
HsvColor.TryParse(s, out HsvColor hsvColor))
{
color = hsvColor.ToRgb();
return true;
}
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
@ -172,21 +217,52 @@ namespace Avalonia.Media
if (s.Length == 0)
{
color = default;
return false;
}
if (s[0] == '#')
if (s[0] == '#' &&
TryParseHexFormat(s, out color))
{
return true;
}
// At this point all parsing uses strings
var str = s.ToString();
if (s.Length > 5 &&
(s[0] == 'r' || s[0] == 'R') &&
(s[1] == 'g' || s[1] == 'G') &&
(s[2] == 'b' || s[2] == 'B') &&
TryParseCssFormat(str, out color))
{
return true;
}
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'l' || s[2] == 'L') &&
HslColor.TryParse(str, out HslColor hslColor))
{
return TryParseInternal(s, out color);
color = hslColor.ToRgb();
return true;
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
if (s.Length > 5 &&
(s[0] == 'h' || s[0] == 'H') &&
(s[1] == 's' || s[1] == 'S') &&
(s[2] == 'v' || s[2] == 'V') &&
HsvColor.TryParse(str, out HsvColor hsvColor))
{
color = hsvColor.ToRgb();
return true;
}
var knownColor = KnownColors.GetKnownColor(str);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
@ -195,7 +271,7 @@ namespace Avalonia.Media
return false;
}
private static bool TryParseInternal(ReadOnlySpan<char> s, out Color color)
private static bool TryParseHexFormat(ReadOnlySpan<char> s, out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
{
@ -249,6 +325,91 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
private static bool TryParseCssFormat(string s, out Color color)
{
color = default;
if (s is null)
{
return false;
}
string workingString = s.Trim();
if (workingString.Length == 0 ||
workingString.IndexOf(",", StringComparison.Ordinal) < 0)
{
return false;
}
if (workingString.Length > 6 &&
workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
}
if (workingString.Length > 5 &&
workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
}
string[] components = workingString.Split(',');
if (components.Length == 3) // RGB
{
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue))
{
color = new Color(0xFF, red, green, blue);
return true;
}
}
else if (components.Length == 4) // RGBA
{
if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) &&
TryInternalParse(components[3], out double alpha))
{
color = new Color((byte)(alpha * 255), red, green, blue);
return true;
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
if (percentIndex >= 0)
{
var result = double.TryParse(
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0;
return result;
}
else
{
return double.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble);
}
}
return false;
}
/// <summary>
/// Returns the string representation of the color.
/// </summary>
@ -272,15 +433,24 @@ namespace Avalonia.Media
return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B;
}
/// <summary>
/// Returns the HSL color model equivalent of this RGB color.
/// </summary>
/// <returns>The HSL equivalent color.</returns>
public HslColor ToHsl()
{
// Don't use the HslColor(Color) constructor to avoid an extra HslColor
return Color.ToHsl(R, G, B, A);
}
/// <summary>
/// Returns the HSV color model equivalent of this RGB color.
/// </summary>
/// <returns>The HSV equivalent color.</returns>
public HsvColor ToHsv()
{
// Use the by-channel conversion method directly for performance
// Don't use the HsvColor(Color) constructor to avoid an extra HsvColor
return HsvColor.FromRgb(R, G, B, A);
return Color.ToHsv(R, G, B, A);
}
/// <inheritdoc/>
@ -289,11 +459,13 @@ namespace Avalonia.Media
return A == other.A && R == other.R && G == other.G && B == other.B;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Color other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
unchecked
@ -306,11 +478,261 @@ namespace Avalonia.Media
}
}
/// <summary>
/// Converts the given RGB color to its HSL color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
public static HslColor ToHsl(Color color)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsl(
(byteToDouble * color.R),
(byteToDouble * color.G),
(byteToDouble * color.B),
(byteToDouble * color.A));
}
/// <summary>
/// Converts the given RGBA color component values to their HSL color equivalent.
/// </summary>
/// <param name="red">The Red component in the RGB color model.</param>
/// <param name="green">The Green component in the RGB color model.</param>
/// <param name="blue">The Blue component in the RGB color model.</param>
/// <param name="alpha">The Alpha component.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
public static HslColor ToHsl(
byte red,
byte green,
byte blue,
byte alpha = 0xFF)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsl(
(byteToDouble * red),
(byteToDouble * green),
(byteToDouble * blue),
(byteToDouble * alpha));
}
/// <summary>
/// Converts the given RGBA color component values to their HSL color equivalent.
/// </summary>
/// <remarks>
/// Warning: No bounds checks or clamping is done on the input component values.
/// This method is for internal-use only and the caller must ensure bounds.
/// </remarks>
/// <param name="r">The Red component in the RGB color model within the range 0..1.</param>
/// <param name="g">The Green component in the RGB color model within the range 0..1.</param>
/// <param name="b">The Blue component in the RGB color model within the range 0..1.</param>
/// <param name="a">The Alpha component in the RGB color model within the range 0..1.</param>
/// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
internal static HslColor ToHsl(
double r,
double g,
double b,
double a = 1.0)
{
// Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
// It has been modified.
double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
double chroma = max - min;
double h1;
if (chroma == 0)
{
h1 = 0;
}
else if (max == r)
{
// The % operator doesn't do proper modulo on negative
// numbers, so we'll add 6 before using it
h1 = (((g - b) / chroma) + 6) % 6;
}
else if (max == g)
{
h1 = 2 + ((b - r) / chroma);
}
else
{
h1 = 4 + ((r - g) / chroma);
}
double lightness = 0.5 * (max + min);
double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
return new HslColor(a, 60 * h1, saturation, lightness, clampValues: false);
}
/// <summary>
/// Converts the given RGB color to its HSV color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor ToHsv(Color color)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsv(
(byteToDouble * color.R),
(byteToDouble * color.G),
(byteToDouble * color.B),
(byteToDouble * color.A));
}
/// <summary>
/// Converts the given RGBA color component values to their HSV color equivalent.
/// </summary>
/// <param name="red">The Red component in the RGB color model.</param>
/// <param name="green">The Green component in the RGB color model.</param>
/// <param name="blue">The Blue component in the RGB color model.</param>
/// <param name="alpha">The Alpha component.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor ToHsv(
byte red,
byte green,
byte blue,
byte alpha = 0xFF)
{
// Normalize RGBA components into the 0..1 range
return Color.ToHsv(
(byteToDouble * red),
(byteToDouble * green),
(byteToDouble * blue),
(byteToDouble * alpha));
}
/// <summary>
/// Converts the given RGBA color component values to their HSV color equivalent.
/// </summary>
/// <remarks>
/// Warning: No bounds checks or clamping is done on the input component values.
/// This method is for internal-use only and the caller must ensure bounds.
/// </remarks>
/// <param name="r">The Red component in the RGB color model within the range 0..1.</param>
/// <param name="g">The Green component in the RGB color model within the range 0..1.</param>
/// <param name="b">The Blue component in the RGB color model within the range 0..1.</param>
/// <param name="a">The Alpha component in the RGB color model within the range 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
internal static HsvColor ToHsv(
double r,
double g,
double b,
double a = 1.0)
{
// Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
// https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
// This was used because it is the best documented and likely most optimized for performance
// Alpha support was added
double hue;
double saturation;
double value;
double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
// The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255).
// Conceptually speaking, it represents how much color is present.
// If at least one of R, G, B is 255, then there exists as much color as there can be.
// If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds
// to black (i.e., the absence of any color).
value = max;
// The "chroma" of the color is a value directly proportional to the extent to which
// the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0),
// then the chroma is maximized - this is a pure yellow, no gray of any kind.
// On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero
// implies that this color is pure greyscale, with no actual hue to be found.
var chroma = max - min;
// If the chrome is zero, then hue is technically undefined - a greyscale color
// has no hue. For the sake of convenience, we'll just set hue to zero, since
// it will be unused in this circumstance. Since the color is purely gray,
// saturation is also equal to zero - you can think of saturation as basically
// a measure of hue intensity, such that no hue at all corresponds to a
// nonexistent intensity.
if (chroma == 0)
{
hue = 0.0;
saturation = 0.0;
}
else
{
// In this block, hue is properly defined, so we'll extract both hue
// and saturation information from the RGB color.
// Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees.
// A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red.
// Every other hue is somewhere between either red and green, green and blue, and blue and red,
// so every other hue can be thought of as an angle on this color wheel.
// These if/else statements determines where on this color wheel our color lies.
if (r == max)
{
// If the red channel is the most pronounced channel, then we exist
// somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees
// where red dominates. We figure out where in that section we are exactly
// by considering whether the green or the blue channel is greater - by subtracting green from blue,
// then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then
// we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger,
// since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue
// even if we're very close to greyscale.
hue = 60 * (g - b) / chroma;
}
else if (g == max)
{
// We do the exact same for the case where the green channel is the most pronounced channel,
// only this time we want to see if we should tilt towards the blue direction or the red direction.
// We add 120 to center our value in the green third of the color wheel.
hue = 120 + (60 * (b - r) / chroma);
}
else // blue == max
{
// And we also do the exact same for the case where the blue channel is the most pronounced channel,
// only this time we want to see if we should tilt towards the red direction or the green direction.
// We add 240 to center our value in the blue third of the color wheel.
hue = 240 + (60 * (r - g) / chroma);
}
// Since we want to work within the range [0, 360), we'll add 360 to any value less than zero -
// this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values.
if (hue < 0.0)
{
hue += 360.0;
}
// The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is.
// To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value).
// This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1,
// meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be.
// If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5,
// meaning that this color is partially washed out.
// A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue.
saturation = chroma / value;
}
return new HsvColor(a, hue, saturation, value, clampValues: false);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="Color"/> objects are equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are equal; otherwise, false.</returns>
public static bool operator ==(Color left, Color right)
{
return left.Equals(right);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="Color"/> objects are not equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are not equal; otherwise, false.</returns>
public static bool operator !=(Color left, Color right)
{
return !left.Equals(right);

476
src/Avalonia.Visuals/Media/HslColor.cs

@ -0,0 +1,476 @@
// Color conversion portions of this source file are adapted from the Windows Community Toolkit project.
// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Globalization;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media
{
/// <summary>
/// Defines a color using the hue/saturation/lightness (HSL) model.
/// </summary>
#if !BUILDTASK
public
#endif
readonly struct HslColor : IEquatable<HslColor>
{
/// <summary>
/// Initializes a new instance of the <see cref="HslColor"/> struct.
/// </summary>
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="lightness">The Lightness component in the range from 0..1.</param>
public HslColor(
double alpha,
double hue,
double saturation,
double lightness)
{
A = MathUtilities.Clamp(alpha, 0.0, 1.0);
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
L = MathUtilities.Clamp(lightness, 0.0, 1.0);
// The maximum value of Hue is technically 360 minus epsilon (just below 360).
// This is because, in a color circle, 360 degrees is equivalent to 0 degrees.
// However, that is too tricky to work with in code and isn't as intuitive.
// Therefore, since 360 == 0, just wrap 360 if needed back to 0.
H = (H == 360.0 ? 0 : H);
}
/// <summary>
/// Initializes a new instance of the <see cref="HslColor"/> struct.
/// </summary>
/// <remarks>
/// This constructor exists only for internal use where performance is critical.
/// Whether or not the component values are in the correct ranges must be known.
/// </remarks>
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="lightness">The Lightness component in the range from 0..1.</param>
/// <param name="clampValues">Whether to clamp component values to their required ranges.</param>
internal HslColor(
double alpha,
double hue,
double saturation,
double lightness,
bool clampValues)
{
if (clampValues)
{
A = MathUtilities.Clamp(alpha, 0.0, 1.0);
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
L = MathUtilities.Clamp(lightness, 0.0, 1.0);
// See comments in constructor above
H = (H == 360.0 ? 0 : H);
}
else
{
A = alpha;
H = hue;
S = saturation;
L = lightness;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="HslColor"/> struct.
/// </summary>
/// <param name="color">The RGB color to convert to HSL.</param>
public HslColor(Color color)
{
var hsl = Color.ToHsl(color);
A = hsl.A;
H = hsl.H;
S = hsl.S;
L = hsl.L;
}
/// <summary>
/// Gets the Alpha (transparency) component in the range from 0..1.
/// </summary>
public double A { get; }
/// <summary>
/// Gets the Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
public double H { get; }
/// <summary>
/// Gets the Saturation component in the range from 0..1.
/// </summary>
public double S { get; }
/// <summary>
/// Gets the Lightness component in the range from 0..1.
/// </summary>
public double L { get; }
/// <inheritdoc/>
public bool Equals(HslColor other)
{
return other.A == A &&
other.H == H &&
other.S == S &&
other.L == L;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
if (obj is HslColor hslColor)
{
return Equals(hslColor);
}
else
{
return false;
}
}
/// <summary>
/// Gets a hashcode for this object.
/// Hashcode is not guaranteed to be unique.
/// </summary>
/// <returns>The hashcode for this object.</returns>
public override int GetHashCode()
{
// Same algorithm as Color
// This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements
unchecked
{
int hashCode = A.GetHashCode();
hashCode = (hashCode * 397) ^ H.GetHashCode();
hashCode = (hashCode * 397) ^ S.GetHashCode();
hashCode = (hashCode * 397) ^ L.GetHashCode();
return hashCode;
}
}
/// <summary>
/// Returns the RGB color model equivalent of this HSL color.
/// </summary>
/// <returns>The RGB equivalent color.</returns>
public Color ToRgb()
{
// Use the by-component conversion method directly for performance
return HslColor.ToRgb(H, S, L, A);
}
/// <inheritdoc/>
public override string ToString()
{
var sb = new StringBuilder();
// Use a format similar to CSS. However:
// - To ensure precision is never lost, allow decimal places.
// This is especially important for round-trip serialization.
// - To maintain numerical consistency, do not use percent.
//
// Example:
//
// hsla(hue, saturation, lightness, alpha)
// hsla(230, 1.0, 0.5, 1.0)
//
sb.Append("hsva(");
sb.Append(H.ToString(CultureInfo.InvariantCulture));
sb.Append(", ");
sb.Append(S.ToString(CultureInfo.InvariantCulture));
sb.Append(", ");
sb.Append(L.ToString(CultureInfo.InvariantCulture));
sb.Append(", ");
sb.Append(A.ToString(CultureInfo.InvariantCulture));
sb.Append(')');
return sb.ToString();
}
/// <summary>
/// Parses an HSL color string.
/// </summary>
/// <param name="s">The HSL color string to parse.</param>
/// <returns>The parsed <see cref="HslColor"/>.</returns>
public static HslColor Parse(string s)
{
if (s is null)
{
throw new ArgumentNullException(nameof(s));
}
if (TryParse(s, out HslColor hslColor))
{
return hslColor;
}
throw new FormatException($"Invalid HSL color string: '{s}'.");
}
/// <summary>
/// Parses an HSL color string.
/// </summary>
/// <param name="s">The HSL color string to parse.</param>
/// <param name="hslColor">The parsed <see cref="HslColor"/>.</param>
/// <returns>True if parsing was successful; otherwise, false.</returns>
public static bool TryParse(string s, out HslColor hslColor)
{
hslColor = default;
if (s is null)
{
return false;
}
string workingString = s.Trim();
if (workingString.Length == 0 ||
workingString.IndexOf(",", StringComparison.Ordinal) < 0)
{
return false;
}
if (workingString.Length > 6 &&
workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(5, workingString.Length - 6);
}
if (workingString.Length > 5 &&
workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
workingString.EndsWith(")", StringComparison.Ordinal))
{
workingString = workingString.Substring(4, workingString.Length - 5);
}
string[] components = workingString.Split(',');
if (components.Length == 3) // HSL
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double lightness))
{
hslColor = new HslColor(1.0, hue, saturation, lightness);
return true;
}
}
else if (components.Length == 4) // HSLA
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double lightness) &&
TryInternalParse(components[3], out double alpha))
{
hslColor = new HslColor(alpha, hue, saturation, lightness);
return true;
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
if (percentIndex >= 0)
{
var result = double.TryParse(
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0;
return result;
}
else
{
return double.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble);
}
}
return false;
}
/// <summary>
/// Creates a new <see cref="HslColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="a">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="l">The Lightness component in the range from 0..1.</param>
/// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
public static HslColor FromAhsl(double a, double h, double s, double l)
{
return new HslColor(a, h, s, l);
}
/// <summary>
/// Creates a new <see cref="HslColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="l">The Lightness component in the range from 0..1.</param>
/// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
public static HslColor FromHsl(double h, double s, double l)
{
return new HslColor(1.0, h, s, l);
}
/// <summary>
/// Converts the given HSL color to its RGB color equivalent.
/// </summary>
/// <param name="hslColor">The color in the HSL color model.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
public static Color ToRgb(HslColor hslColor)
{
return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
}
/// <summary>
/// Converts the given HSLA color component values to their RGB color equivalent.
/// </summary>
/// <param name="hue">The Hue component in the HSL color model in the range from 0..360.</param>
/// <param name="saturation">The Saturation component in the HSL color model in the range from 0..1.</param>
/// <param name="lightness">The Lightness component in the HSL color model in the range from 0..1.</param>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
public static Color ToRgb(
double hue,
double saturation,
double lightness,
double alpha = 1.0)
{
// Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
// It has been modified to ensure input ranges and not throw exceptions.
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
while (hue >= 360.0)
{
hue -= 360.0;
}
while (hue < 0.0)
{
hue += 360.0;
}
// We similarly clamp saturation, lightness and alpha between 0 and 1.
saturation = saturation < 0.0 ? 0.0 : saturation;
saturation = saturation > 1.0 ? 1.0 : saturation;
lightness = lightness < 0.0 ? 0.0 : lightness;
lightness = lightness > 1.0 ? 1.0 : lightness;
alpha = alpha < 0.0 ? 0.0 : alpha;
alpha = alpha > 1.0 ? 1.0 : alpha;
double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation;
double h1 = hue / 60;
double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
double m = lightness - (0.5 * chroma);
double r1, g1, b1;
if (h1 < 1)
{
r1 = chroma;
g1 = x;
b1 = 0;
}
else if (h1 < 2)
{
r1 = x;
g1 = chroma;
b1 = 0;
}
else if (h1 < 3)
{
r1 = 0;
g1 = chroma;
b1 = x;
}
else if (h1 < 4)
{
r1 = 0;
g1 = x;
b1 = chroma;
}
else if (h1 < 5)
{
r1 = x;
g1 = 0;
b1 = chroma;
}
else
{
r1 = chroma;
g1 = 0;
b1 = x;
}
return Color.FromArgb(
(byte)Math.Round(255 * alpha),
(byte)Math.Round(255 * (r1 + m)),
(byte)Math.Round(255 * (g1 + m)),
(byte)Math.Round(255 * (b1 + m)));
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HslColor"/> objects are equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are equal; otherwise, false.</returns>
public static bool operator ==(HslColor left, HslColor right)
{
return left.Equals(right);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HslColor"/> objects are not equal.
/// </summary>
/// <param name="left">The first object to compare.</param>
/// <param name="right">The second object to compare.</param>
/// <returns>True if left and right are not equal; otherwise, false.</returns>
public static bool operator !=(HslColor left, HslColor right)
{
return !(left == right);
}
/// <summary>
/// Explicit conversion from an <see cref="HslColor"/> to a <see cref="Color"/>.
/// </summary>
/// <param name="hslColor">The <see cref="HslColor"/> to convert.</param>
public static explicit operator Color(HslColor hslColor)
{
return hslColor.ToRgb();
}
}
}

279
src/Avalonia.Visuals/Media/HsvColor.cs

@ -1,6 +1,6 @@
// Color conversion portions of this source file are adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Color conversion portions of this source file are adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
@ -21,11 +21,11 @@ namespace Avalonia.Media
/// <summary>
/// Initializes a new instance of the <see cref="HsvColor"/> struct.
/// </summary>
/// <param name="alpha">The Alpha (transparency) channel value in the range from 0..1.</param>
/// <param name="hue">The Hue channel value in the range from 0..360.
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation channel value in the range from 0..1.</param>
/// <param name="value">The Value channel value in the range from 0..1.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="value">The Value component in the range from 0..1.</param>
public HsvColor(
double alpha,
double hue,
@ -49,14 +49,14 @@ namespace Avalonia.Media
/// </summary>
/// <remarks>
/// This constructor exists only for internal use where performance is critical.
/// Whether or not the channel values are in the correct ranges must be known.
/// Whether or not the component values are in the correct ranges must be known.
/// </remarks>
/// <param name="alpha">The Alpha (transparency) channel value in the range from 0..1.</param>
/// <param name="hue">The Hue channel value in the range from 0..360.
/// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="hue">The Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
/// <param name="saturation">The Saturation channel value in the range from 0..1.</param>
/// <param name="value">The Value channel value in the range from 0..1.</param>
/// <param name="clampValues">Whether to clamp channel values to their required ranges.</param>
/// <param name="saturation">The Saturation component in the range from 0..1.</param>
/// <param name="value">The Value component in the range from 0..1.</param>
/// <param name="clampValues">Whether to clamp component values to their required ranges.</param>
internal HsvColor(
double alpha,
double hue,
@ -89,7 +89,7 @@ namespace Avalonia.Media
/// <param name="color">The RGB color to convert to HSV.</param>
public HsvColor(Color color)
{
var hsv = HsvColor.FromRgb(color);
var hsv = Color.ToHsv(color);
A = hsv.A;
H = hsv.H;
@ -98,23 +98,23 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets the Alpha (transparency) channel value in the range from 0..1.
/// Gets the Alpha (transparency) component in the range from 0..1.
/// </summary>
public double A { get; }
/// <summary>
/// Gets the Hue channel value in the range from 0..360.
/// Gets the Hue component in the range from 0..360.
/// Note that 360 is equivalent to 0 and will be adjusted automatically.
/// </summary>
public double H { get; }
/// <summary>
/// Gets the Saturation channel value in the range from 0..1.
/// Gets the Saturation component in the range from 0..1.
/// </summary>
public double S { get; }
/// <summary>
/// Gets the Value channel value in the range from 0..1.
/// Gets the Value component in the range from 0..1.
/// </summary>
public double V { get; }
@ -165,7 +165,7 @@ namespace Avalonia.Media
/// <returns>The RGB equivalent color.</returns>
public Color ToRgb()
{
// Use the by-channel conversion method directly for performance
// Use the by-component conversion method directly for performance
return HsvColor.ToRgb(H, S, V, A);
}
@ -174,26 +174,16 @@ namespace Avalonia.Media
{
var sb = new StringBuilder();
// Use a format similar to HSL in HTML/CSS "hsla(0, 100%, 50%, 0.5)"
//
// However:
// - To ensure precision is never lost, allow decimal places
// - To maintain numerical consistency do not use percent
// Use a format similar to CSS. However:
// - To ensure precision is never lost, allow decimal places.
// This is especially important for round-trip serialization.
// - To maintain numerical consistency, do not use percent.
//
// Example:
//
// hsva(hue, saturation, value, alpha)
// hsva(230, 1.0, 0.5, 1.0)
//
// Where:
//
// hue : double from 0 to 360
// saturation : double from 0 to 1
// (HTML uses a percentage)
// value : double from 0 to 1
// (HTML uses a percentage)
// alpha : double from 0 to 1
// (HTML does not use a percentage for alpha)
sb.Append("hsva(");
sb.Append(H.ToString(CultureInfo.InvariantCulture));
@ -270,8 +260,8 @@ namespace Avalonia.Media
if (components.Length == 3) // HSV
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value))
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double value))
{
hsvColor = new HsvColor(1.0, hue, saturation, value);
return true;
@ -280,35 +270,78 @@ namespace Avalonia.Media
else if (components.Length == 4) // HSVA
{
if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
double.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out double saturation) &&
double.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out double value) &&
double.TryParse(components[3], NumberStyles.Number, CultureInfo.InvariantCulture, out double alpha))
TryInternalParse(components[1], out double saturation) &&
TryInternalParse(components[2], out double value) &&
TryInternalParse(components[3], out double alpha))
{
hsvColor = new HsvColor(alpha, hue, saturation, value);
return true;
}
}
// Local function to specially parse a double value with an optional percentage sign
bool TryInternalParse(string inString, out double outDouble)
{
// The percent sign, if it exists, must be at the end of the number
int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
if (percentIndex >= 0)
{
var result = double.TryParse(
inString.Substring(0, percentIndex),
NumberStyles.Number,
CultureInfo.InvariantCulture,
out double percentage);
outDouble = percentage / 100.0;
return result;
}
else
{
return double.TryParse(
inString,
NumberStyles.Number,
CultureInfo.InvariantCulture,
out outDouble);
}
}
return false;
}
/// <summary>
/// Creates a new <see cref="HsvColor"/> from individual color channel values.
/// Creates a new <see cref="HsvColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="a">The Alpha (transparency) channel value in the range from 0..1.</param>
/// <param name="h">The Hue channel value in the range from 0..360.</param>
/// <param name="s">The Saturation channel value in the range from 0..1.</param>
/// <param name="v">The Value channel value in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> built from the individual color channel values.</returns>
/// <param name="a">The Alpha (transparency) component in the range from 0..1.</param>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="v">The Value component in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> built from the individual color component values.</returns>
public static HsvColor FromAhsv(double a, double h, double s, double v)
{
return new HsvColor(a, h, s, v);
}
/// <summary>
/// Creates a new <see cref="HsvColor"/> from individual color component values.
/// </summary>
/// <remarks>
/// This exists for symmetry with the <see cref="Color"/> struct; however, the
/// appropriate constructor should commonly be used instead.
/// </remarks>
/// <param name="h">The Hue component in the range from 0..360.</param>
/// <param name="s">The Saturation component in the range from 0..1.</param>
/// <param name="v">The Value component in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> built from the individual color component values.</returns>
public static HsvColor FromHsv(double h, double s, double v)
{
return new HsvColor(1.0, h, s, v);
}
/// <summary>
/// Converts the given HSV color to its RGB color equivalent.
/// </summary>
@ -320,12 +353,12 @@ namespace Avalonia.Media
}
/// <summary>
/// Converts the given HSVA color channel values to its RGB color equivalent.
/// Converts the given HSVA color component values to their RGB color equivalent.
/// </summary>
/// <param name="hue">The hue channel value in the HSV color model in the range from 0..360.</param>
/// <param name="saturation">The saturation channel value in the HSV color model in the range from 0..1.</param>
/// <param name="value">The value channel value in the HSV color model in the range from 0..1.</param>
/// <param name="alpha">The alpha channel value in the range from 0..1.</param>
/// <param name="hue">The Hue component in the HSV color model in the range from 0..360.</param>
/// <param name="saturation">The Saturation component in the HSV color model in the range from 0..1.</param>
/// <param name="value">The Value component in the HSV color model in the range from 0..1.</param>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new RGB <see cref="Color"/> equivalent to the given HSVA values.</returns>
public static Color ToRgb(
double hue,
@ -336,7 +369,7 @@ namespace Avalonia.Media
// Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
// https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
// This was used because it is the best documented and likely most optimized for performance
// Alpha channel support was added
// Alpha support was added
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
@ -457,152 +490,6 @@ namespace Avalonia.Media
(byte)Math.Round(b * 255));
}
/// <summary>
/// Converts the given RGB color to its HSV color equivalent.
/// </summary>
/// <param name="color">The color in the RGB color model.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor FromRgb(Color color)
{
return HsvColor.FromRgb(color.R, color.G, color.B, color.A);
}
/// <summary>
/// Converts the given RGBA color channel values to its HSV color equivalent.
/// </summary>
/// <param name="red">The red channel value in the RGB color model.</param>
/// <param name="green">The green channel value in the RGB color model.</param>
/// <param name="blue">The blue channel value in the RGB color model.</param>
/// <param name="alpha">The alpha channel value.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor FromRgb(
byte red,
byte green,
byte blue,
byte alpha = 0xFF)
{
// Normalize RGBA channel values into the 0..1 range
return HsvColor.FromRgb(
(red / 255.0),
(green / 255.0),
(blue / 255.0),
(alpha / 255.0));
}
// TODO: Mark the below method Internal and make Internals visible to Avalonia.Controls...
/// <summary>
/// Converts the given RGBA color channel values to its HSV color equivalent.
/// </summary>
/// <remarks>
/// Warning: No bounds checks or clamping is done on the input channel values.
/// This method is for internal-use only and the caller must ensure bounds.
/// </remarks>
/// <param name="r">The red channel value in the RGB color model within the range 0..1.</param>
/// <param name="g">The green channel value in the RGB color model within the range 0..1.</param>
/// <param name="b">The blue channel value in the RGB color model within the range 0..1.</param>
/// <param name="a">The alpha channel value in the RGB color model within the range 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
public static HsvColor FromRgb(
double r,
double g,
double b,
double a = 1.0)
{
// Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
// https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
// This was used because it is the best documented and likely most optimized for performance
// Alpha channel support was added
double hue;
double saturation;
double value;
double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
// The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255).
// Conceptually speaking, it represents how much color is present.
// If at least one of R, G, B is 255, then there exists as much color as there can be.
// If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds
// to black (i.e., the absence of any color).
value = max;
// The "chroma" of the color is a value directly proportional to the extent to which
// the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0),
// then the chroma is maximized - this is a pure yellow, no gray of any kind.
// On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero
// implies that this color is pure greyscale, with no actual hue to be found.
var chroma = max - min;
// If the chrome is zero, then hue is technically undefined - a greyscale color
// has no hue. For the sake of convenience, we'll just set hue to zero, since
// it will be unused in this circumstance. Since the color is purely gray,
// saturation is also equal to zero - you can think of saturation as basically
// a measure of hue intensity, such that no hue at all corresponds to a
// nonexistent intensity.
if (chroma == 0)
{
hue = 0.0;
saturation = 0.0;
}
else
{
// In this block, hue is properly defined, so we'll extract both hue
// and saturation information from the RGB color.
// Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees.
// A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red.
// Every other hue is somewhere between either red and green, green and blue, and blue and red,
// so every other hue can be thought of as an angle on this color wheel.
// These if/else statements determines where on this color wheel our color lies.
if (r == max)
{
// If the red channel is the most pronounced channel, then we exist
// somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees
// where red dominates. We figure out where in that section we are exactly
// by considering whether the green or the blue channel is greater - by subtracting green from blue,
// then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then
// we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger,
// since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue
// even if we're very close to greyscale.
hue = 60 * (g - b) / chroma;
}
else if (g == max)
{
// We do the exact same for the case where the green channel is the most pronounced channel,
// only this time we want to see if we should tilt towards the blue direction or the red direction.
// We add 120 to center our value in the green third of the color wheel.
hue = 120 + (60 * (b - r) / chroma);
}
else // blue == max
{
// And we also do the exact same for the case where the blue channel is the most pronounced channel,
// only this time we want to see if we should tilt towards the red direction or the green direction.
// We add 240 to center our value in the blue third of the color wheel.
hue = 240 + (60 * (r - g) / chroma);
}
// Since we want to work within the range [0, 360), we'll add 360 to any value less than zero -
// this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values.
if (hue < 0.0)
{
hue += 360.0;
}
// The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is.
// To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value).
// This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1,
// meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be.
// If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5,
// meaning that this color is partially washed out.
// A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue.
saturation = chroma / value;
}
return new HsvColor(a, hue, saturation, value, false);
}
/// <summary>
/// Indicates whether the values of two specified <see cref="HsvColor"/> objects are equal.
/// </summary>

6
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

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

6
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

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

19
src/Avalonia.Visuals/Visual.cs

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

6
src/Avalonia.Visuals/VisualExtensions.cs

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

5
src/Avalonia.Visuals/VisualTree/IVisual.cs

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

3
src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj

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

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

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

1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

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

2
src/Shared/ModuleInitializer.cs

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

3
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

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

1
src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj

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

1
src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj

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

3
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

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

2
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

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

17
src/tools/DevAnalyzers/DevAnalyzers.csproj

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

40
src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs

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

8
src/tools/DevAnalyzers/GlobalSuppressions.cs

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

2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

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

149
tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs

@ -0,0 +1,149 @@
using System.Runtime.CompilerServices;
using Avalonia.Data;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObject_Binding
{
private static TestClass _target = null!;
private static TestBindingObservable<string?> s_stringSource = new();
private static TestBindingObservable<Struct1> s_struct1Source = new();
private static TestBindingObservable<Struct2> s_struct2Source = new();
private static TestBindingObservable<Struct3> s_struct3Source = new();
private static TestBindingObservable<Struct4> s_struct4Source = new();
private static TestBindingObservable<Struct5> s_struct5Source = new();
private static TestBindingObservable<Struct6> s_struct6Source = new();
private static TestBindingObservable<Struct7> s_struct7Source = new();
private static TestBindingObservable<Struct8> s_struct8Source = new();
public AvaloniaObject_Binding()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[GlobalSetup]
public void Setup()
{
_target = new TestClass();
}
[Benchmark]
public void Setup_Dispose_LocalValue_Bindings()
{
var target = _target;
for (var i = 0; i < 100; ++i)
{
using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
}
}
[Benchmark]
public void Fire_LocalValue_Bindings()
{
var target = _target;
using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
for (var i = 0; i < 100; ++i)
{
s_stringSource.OnNext(i.ToString());
s_struct1Source.OnNext(new(i + 1));
s_struct2Source.OnNext(new(i + 1));
s_struct3Source.OnNext(new(i + 1));
s_struct4Source.OnNext(new(i + 1));
s_struct5Source.OnNext(new(i + 1));
s_struct6Source.OnNext(new(i + 1));
s_struct7Source.OnNext(new(i + 1));
s_struct8Source.OnNext(new(i + 1));
}
}
[GlobalSetup(Target = nameof(Fire_LocalValue_Bindings_With_Style_Values))]
public void SetupStyleValues()
{
_target = new TestClass();
_target.SetValue(TestClass.StringProperty, "foo", BindingPriority.Style);
_target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
_target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
_target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
_target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
_target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
_target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
_target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
_target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
}
[Benchmark]
public void Fire_LocalValue_Bindings_With_Style_Values()
{
var target = _target;
using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
for (var i = 0; i < 100; ++i)
{
s_stringSource.OnNext(i.ToString());
s_struct1Source.OnNext(new(i + 1));
s_struct2Source.OnNext(new(i + 1));
s_struct3Source.OnNext(new(i + 1));
s_struct4Source.OnNext(new(i + 1));
s_struct5Source.OnNext(new(i + 1));
s_struct6Source.OnNext(new(i + 1));
s_struct7Source.OnNext(new(i + 1));
s_struct8Source.OnNext(new(i + 1));
}
}
private class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string?> StringProperty =
AvaloniaProperty.Register<TestClass, string?>("String");
public static readonly StyledProperty<Struct1> Struct1Property =
AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
public static readonly StyledProperty<Struct2> Struct2Property =
AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
public static readonly StyledProperty<Struct3> Struct3Property =
AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
public static readonly StyledProperty<Struct4> Struct4Property =
AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
public static readonly StyledProperty<Struct5> Struct5Property =
AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
public static readonly StyledProperty<Struct6> Struct6Property =
AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
public static readonly StyledProperty<Struct7> Struct7Property =
AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
public static readonly StyledProperty<Struct8> Struct8Property =
AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
}
}
}

81
tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs

@ -0,0 +1,81 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObject_Construct
{
public AvaloniaObject_Construct()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[Benchmark(Baseline = true)]
public void ConstructClrObject_And_Set_Values()
{
var target = new BaselineTestClass();
target.StringProperty = "foo";
target.Struct1Property = new(1);
target.Struct2Property = new(1);
target.Struct3Property = new(1);
target.Struct4Property = new(1);
target.Struct5Property = new(1);
target.Struct6Property = new(1);
target.Struct7Property = new(1);
target.Struct8Property = new(1);
}
[Benchmark]
public void Construct_And_Set_Values()
{
var target = new TestClass();
target.SetValue(TestClass.StringProperty, "foo");
target.SetValue(TestClass.Struct1Property, new(1));
target.SetValue(TestClass.Struct2Property, new(1));
target.SetValue(TestClass.Struct3Property, new(1));
target.SetValue(TestClass.Struct4Property, new(1));
target.SetValue(TestClass.Struct5Property, new(1));
target.SetValue(TestClass.Struct6Property, new(1));
target.SetValue(TestClass.Struct7Property, new(1));
target.SetValue(TestClass.Struct8Property, new(1));
}
private class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> StringProperty =
AvaloniaProperty.Register<TestClass, string>("String");
public static readonly StyledProperty<Struct1> Struct1Property =
AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
public static readonly StyledProperty<Struct2> Struct2Property =
AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
public static readonly StyledProperty<Struct3> Struct3Property =
AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
public static readonly StyledProperty<Struct4> Struct4Property =
AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
public static readonly StyledProperty<Struct5> Struct5Property =
AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
public static readonly StyledProperty<Struct6> Struct6Property =
AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
public static readonly StyledProperty<Struct7> Struct7Property =
AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
public static readonly StyledProperty<Struct8> Struct8Property =
AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
}
private class BaselineTestClass
{
public string? StringProperty { get; set; }
public Struct1 Struct1Property { get; set; }
public Struct2 Struct2Property { get; set; }
public Struct3 Struct3Property { get; set; }
public Struct4 Struct4Property { get; set; }
public Struct5 Struct5Property { get; set; }
public Struct6 Struct6Property { get; set; }
public Struct7 Struct7Property { get; set; }
public Struct8 Struct8Property { get; set; }
}
}
}

171
tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs

@ -0,0 +1,171 @@
using System;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObject_GetObservable
{
private TestClass _target = null!;
public static int result;
public AvaloniaObject_GetObservable()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[GlobalSetup]
public void Setup()
{
_target = new();
}
[Benchmark(Baseline = true)]
public void PropertyChangedSubscription()
{
var target = _target;
static void ChangeHandler(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == TestClass.StringProperty)
{
var ev = (AvaloniaPropertyChangedEventArgs<string?>)e;
result += ev.NewValue.Value?.Length ?? 0;
}
else if (e.Property == TestClass.Struct1Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct1>)e;
result += ev.NewValue.Value.Int1;
}
else if (e.Property == TestClass.Struct2Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct2>)e;
result += ev.NewValue.Value.Int1;
}
else if (e.Property == TestClass.Struct3Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct3>)e;
result += ev.NewValue.Value.Int1;
}
else if (e.Property == TestClass.Struct4Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct4>)e;
result += ev.NewValue.Value.Int1;
}
else if (e.Property == TestClass.Struct5Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct5>)e;
result += ev.NewValue.Value.Int1;
}
else if (e.Property == TestClass.Struct6Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct6>)e;
result += ev.NewValue.Value.Int1;
}
else if (e.Property == TestClass.Struct7Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct7>)e;
result += ev.NewValue.Value.Int1;
}
else if (e.Property == TestClass.Struct8Property)
{
var ev = (AvaloniaPropertyChangedEventArgs<Struct8>)e;
result += ev.NewValue.Value.Int1;
}
}
target.PropertyChanged += ChangeHandler;
// GetObservable fires with the initial value so to compare like-for-like we also need
// to get the initial value here.
result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
result += target.GetValue(TestClass.Struct1Property).Int1;
result += target.GetValue(TestClass.Struct2Property).Int1;
result += target.GetValue(TestClass.Struct3Property).Int1;
result += target.GetValue(TestClass.Struct4Property).Int1;
result += target.GetValue(TestClass.Struct5Property).Int1;
result += target.GetValue(TestClass.Struct6Property).Int1;
result += target.GetValue(TestClass.Struct7Property).Int1;
result += target.GetValue(TestClass.Struct8Property).Int1;
for (var i = 0; i < 100; ++i)
{
target.SetValue(TestClass.StringProperty, "foo" + i);
target.SetValue(TestClass.Struct1Property, new(i + 1));
target.SetValue(TestClass.Struct2Property, new(i + 1));
target.SetValue(TestClass.Struct3Property, new(i + 1));
target.SetValue(TestClass.Struct4Property, new(i + 1));
target.SetValue(TestClass.Struct5Property, new(i + 1));
target.SetValue(TestClass.Struct6Property, new(i + 1));
target.SetValue(TestClass.Struct7Property, new(i + 1));
target.SetValue(TestClass.Struct8Property, new(i + 1));
}
target.PropertyChanged -= ChangeHandler;
}
[Benchmark]
public void GetObservables()
{
var target = _target;
var sub1 = target.GetObservable(TestClass.StringProperty).Subscribe(x => result += x?.Length ?? 0);
var sub2 = target.GetObservable(TestClass.Struct1Property).Subscribe(x => result += x.Int1);
var sub3 = target.GetObservable(TestClass.Struct2Property).Subscribe(x => result += x.Int1);
var sub4 = target.GetObservable(TestClass.Struct3Property).Subscribe(x => result += x.Int1);
var sub5 = target.GetObservable(TestClass.Struct4Property).Subscribe(x => result += x.Int1);
var sub6 = target.GetObservable(TestClass.Struct5Property).Subscribe(x => result += x.Int1);
var sub7 = target.GetObservable(TestClass.Struct6Property).Subscribe(x => result += x.Int1);
var sub8 = target.GetObservable(TestClass.Struct7Property).Subscribe(x => result += x.Int1);
var sub9 = target.GetObservable(TestClass.Struct8Property).Subscribe(x => result += x.Int1);
for (var i = 0; i < 100; ++i)
{
target.SetValue(TestClass.StringProperty, "foo" + i);
target.SetValue(TestClass.Struct1Property, new(i + 1));
target.SetValue(TestClass.Struct2Property, new(i + 1));
target.SetValue(TestClass.Struct3Property, new(i + 1));
target.SetValue(TestClass.Struct4Property, new(i + 1));
target.SetValue(TestClass.Struct5Property, new(i + 1));
target.SetValue(TestClass.Struct6Property, new(i + 1));
target.SetValue(TestClass.Struct7Property, new(i + 1));
target.SetValue(TestClass.Struct8Property, new(i + 1));
}
sub1.Dispose();
sub2.Dispose();
sub3.Dispose();
sub4.Dispose();
sub5.Dispose();
sub6.Dispose();
sub7.Dispose();
sub8.Dispose();
sub9.Dispose();
}
private class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> StringProperty =
AvaloniaProperty.Register<TestClass, string>("String");
public static readonly StyledProperty<Struct1> Struct1Property =
AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
public static readonly StyledProperty<Struct2> Struct2Property =
AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
public static readonly StyledProperty<Struct3> Struct3Property =
AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
public static readonly StyledProperty<Struct4> Struct4Property =
AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
public static readonly StyledProperty<Struct5> Struct5Property =
AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
public static readonly StyledProperty<Struct6> Struct6Property =
AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
public static readonly StyledProperty<Struct7> Struct7Property =
AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
public static readonly StyledProperty<Struct8> Struct8Property =
AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
}
}
}

181
tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs

@ -0,0 +1,181 @@
using System.Runtime.CompilerServices;
using Avalonia.Data;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObject_GetValue
{
private BaselineTestClass _baseline = new(){ StringProperty = "foo" };
private TestClass _target = new();
public AvaloniaObject_GetValue()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[Benchmark(Baseline = true)]
public int GetClrPropertyValues()
{
var target = _baseline;
var result = 0;
for (var i = 0; i < 100; ++i)
{
result += target.StringProperty?.Length ?? 0;
result += target.Struct1Property.Int1;
result += target.Struct2Property.Int1;
result += target.Struct3Property.Int1;
result += target.Struct4Property.Int1;
result += target.Struct5Property.Int1;
result += target.Struct6Property.Int1;
result += target.Struct7Property.Int1;
result += target.Struct8Property.Int1;
}
return result;
}
[Benchmark]
public int GetDefaultValues()
{
var target = _target;
var result = 0;
for (var i = 0; i < 100; ++i)
{
result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
result += target.GetValue(TestClass.Struct1Property).Int1;
result += target.GetValue(TestClass.Struct2Property).Int1;
result += target.GetValue(TestClass.Struct3Property).Int1;
result += target.GetValue(TestClass.Struct4Property).Int1;
result += target.GetValue(TestClass.Struct5Property).Int1;
result += target.GetValue(TestClass.Struct6Property).Int1;
result += target.GetValue(TestClass.Struct7Property).Int1;
result += target.GetValue(TestClass.Struct8Property).Int1;
}
return result;
}
[GlobalSetup(Target = nameof(Get_Local_Values))]
public void SetupLocalValues()
{
_target.SetValue(TestClass.StringProperty, "foo");
_target.SetValue(TestClass.Struct1Property, new(1));
_target.SetValue(TestClass.Struct2Property, new(1));
_target.SetValue(TestClass.Struct3Property, new(1));
_target.SetValue(TestClass.Struct4Property, new(1));
_target.SetValue(TestClass.Struct5Property, new(1));
_target.SetValue(TestClass.Struct6Property, new(1));
_target.SetValue(TestClass.Struct7Property, new(1));
_target.SetValue(TestClass.Struct8Property, new(1));
}
[Benchmark]
public int Get_Local_Values()
{
var target = _target;
var result = 0;
for (var i = 0; i < 100; ++i)
{
result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
result += target.GetValue(TestClass.Struct1Property).Int1;
result += target.GetValue(TestClass.Struct2Property).Int1;
result += target.GetValue(TestClass.Struct3Property).Int1;
result += target.GetValue(TestClass.Struct4Property).Int1;
result += target.GetValue(TestClass.Struct5Property).Int1;
result += target.GetValue(TestClass.Struct6Property).Int1;
result += target.GetValue(TestClass.Struct7Property).Int1;
result += target.GetValue(TestClass.Struct8Property).Int1;
}
return result;
}
[GlobalSetup(Target = nameof(Get_Local_Values_With_Style_Values))]
public void SetupLocalValuesAndStyleValues()
{
var target = _target;
target.SetValue(TestClass.StringProperty, "foo");
target.SetValue(TestClass.Struct1Property, new(1));
target.SetValue(TestClass.Struct2Property, new(1));
target.SetValue(TestClass.Struct3Property, new(1));
target.SetValue(TestClass.Struct4Property, new(1));
target.SetValue(TestClass.Struct5Property, new(1));
target.SetValue(TestClass.Struct6Property, new(1));
target.SetValue(TestClass.Struct7Property, new(1));
target.SetValue(TestClass.Struct8Property, new(1));
target.SetValue(TestClass.StringProperty, "bar", BindingPriority.Style);
target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
}
[Benchmark]
public int Get_Local_Values_With_Style_Values()
{
var target = _target;
var result = 0;
for (var i = 0; i < 100; ++i)
{
result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
result += target.GetValue(TestClass.Struct1Property).Int1;
result += target.GetValue(TestClass.Struct2Property).Int1;
result += target.GetValue(TestClass.Struct3Property).Int1;
result += target.GetValue(TestClass.Struct4Property).Int1;
result += target.GetValue(TestClass.Struct5Property).Int1;
result += target.GetValue(TestClass.Struct6Property).Int1;
result += target.GetValue(TestClass.Struct7Property).Int1;
result += target.GetValue(TestClass.Struct8Property).Int1;
}
return result;
}
private class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> StringProperty =
AvaloniaProperty.Register<TestClass, string>("String");
public static readonly StyledProperty<Struct1> Struct1Property =
AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
public static readonly StyledProperty<Struct2> Struct2Property =
AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
public static readonly StyledProperty<Struct3> Struct3Property =
AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
public static readonly StyledProperty<Struct4> Struct4Property =
AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
public static readonly StyledProperty<Struct5> Struct5Property =
AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
public static readonly StyledProperty<Struct6> Struct6Property =
AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
public static readonly StyledProperty<Struct7> Struct7Property =
AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
public static readonly StyledProperty<Struct8> Struct8Property =
AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
}
private class BaselineTestClass
{
public string? StringProperty { get; set; }
public Struct1 Struct1Property { get; set; }
public Struct2 Struct2Property { get; set; }
public Struct3 Struct3Property { get; set; }
public Struct4 Struct4Property { get; set; }
public Struct5 Struct5Property { get; set; }
public Struct6 Struct6Property { get; set; }
public Struct7 Struct7Property { get; set; }
public Struct8 Struct8Property { get; set; }
}
}
}

95
tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObject_GetValueInherited
{
private TestClass _root = null!;
private TestClass _target = null!;
public AvaloniaObject_GetValueInherited()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[Params(1, 2, 10, 50, 100, 200)]
public int Depth { get; set; }
[GlobalSetup]
public void Setup()
{
_root = new();
_root.SetValue(TestClass.StringProperty, "foo");
_root.SetValue(TestClass.Struct1Property, new(1));
_root.SetValue(TestClass.Struct2Property, new(1));
_root.SetValue(TestClass.Struct3Property, new(1));
_root.SetValue(TestClass.Struct4Property, new(1));
_root.SetValue(TestClass.Struct5Property, new(1));
_root.SetValue(TestClass.Struct6Property, new(1));
_root.SetValue(TestClass.Struct7Property, new(1));
_root.SetValue(TestClass.Struct8Property, new(1));
var parent = _root;
for (var i = 0; i < Depth; ++i)
{
var c = new TestClass();
((ISetLogicalParent)c).SetParent(parent);
parent = c;
}
_target = parent;
}
[Benchmark]
public int GetInheritedValues()
{
var target = _target;
var result = 0;
for (var i = 0; i < 100; ++i)
{
result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
result += target.GetValue(TestClass.Struct1Property).Int1;
result += target.GetValue(TestClass.Struct2Property).Int1;
result += target.GetValue(TestClass.Struct3Property).Int1;
result += target.GetValue(TestClass.Struct4Property).Int1;
result += target.GetValue(TestClass.Struct5Property).Int1;
result += target.GetValue(TestClass.Struct6Property).Int1;
result += target.GetValue(TestClass.Struct7Property).Int1;
result += target.GetValue(TestClass.Struct8Property).Int1;
}
return result;
}
private class TestClass : Control
{
public static readonly StyledProperty<string> StringProperty =
AvaloniaProperty.Register<TestClass, string>("String", inherits: true);
public static readonly StyledProperty<Struct1> Struct1Property =
AvaloniaProperty.Register<TestClass, Struct1>("Struct1", inherits: true);
public static readonly StyledProperty<Struct2> Struct2Property =
AvaloniaProperty.Register<TestClass, Struct2>("Struct2", inherits: true);
public static readonly StyledProperty<Struct3> Struct3Property =
AvaloniaProperty.Register<TestClass, Struct3>("Struct3", inherits: true);
public static readonly StyledProperty<Struct4> Struct4Property =
AvaloniaProperty.Register<TestClass, Struct4>("Struct4", inherits: true);
public static readonly StyledProperty<Struct5> Struct5Property =
AvaloniaProperty.Register<TestClass, Struct5>("Struct5", inherits: true);
public static readonly StyledProperty<Struct6> Struct6Property =
AvaloniaProperty.Register<TestClass, Struct6>("Struct6", inherits: true);
public static readonly StyledProperty<Struct7> Struct7Property =
AvaloniaProperty.Register<TestClass, Struct7>("Struct7", inherits: true);
public static readonly StyledProperty<Struct8> Struct8Property =
AvaloniaProperty.Register<TestClass, Struct8>("Struct8", inherits: true);
}
}
}

130
tests/Avalonia.Benchmarks/Base/AvaloniaObject_SetValue.cs

@ -0,0 +1,130 @@
using System.Runtime.CompilerServices;
using Avalonia.Data;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class AvaloniaObject_SetValue
{
private BaselineTestClass _baseline = new();
private TestClass _target = new();
public AvaloniaObject_SetValue()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[Benchmark(Baseline = true)]
public int SetClrPropertyValues()
{
var target = _baseline;
var result = 0;
for (var i = 0; i < 100; ++i)
{
target.StringProperty = "foo";
target.Struct1Property = new(i + 1);
target.Struct2Property = new(i + 1);
target.Struct3Property = new(i + 1);
target.Struct4Property = new(i + 1);
target.Struct5Property = new(i + 1);
target.Struct6Property = new(i + 1);
target.Struct7Property = new(i + 1);
target.Struct8Property = new(i + 1);
}
return result;
}
[Benchmark]
public void SetValues()
{
var target = _target;
for (var i = 0; i < 100; ++i)
{
target.SetValue(TestClass.StringProperty, "foo");
target.SetValue(TestClass.Struct1Property, new(i + 1));
target.SetValue(TestClass.Struct2Property, new(i + 1));
target.SetValue(TestClass.Struct3Property, new(i + 1));
target.SetValue(TestClass.Struct4Property, new(i + 1));
target.SetValue(TestClass.Struct5Property, new(i + 1));
target.SetValue(TestClass.Struct6Property, new(i + 1));
target.SetValue(TestClass.Struct7Property, new(i + 1));
target.SetValue(TestClass.Struct8Property, new(i + 1));
}
}
[GlobalSetup(Target = nameof(Set_Local_Values_With_Style_Values))]
public void SetupStyleValues()
{
var target = _target;
target.SetValue(TestClass.StringProperty, "foo", BindingPriority.Style);
target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
}
[Benchmark]
public void Set_Local_Values_With_Style_Values()
{
var target = _target;
for (var i = 0; i < 100; ++i)
{
target.SetValue(TestClass.StringProperty, "foo");
target.SetValue(TestClass.Struct1Property, new(i + 1));
target.SetValue(TestClass.Struct2Property, new(i + 1));
target.SetValue(TestClass.Struct3Property, new(i + 1));
target.SetValue(TestClass.Struct4Property, new(i + 1));
target.SetValue(TestClass.Struct5Property, new(i + 1));
target.SetValue(TestClass.Struct6Property, new(i + 1));
target.SetValue(TestClass.Struct7Property, new(i + 1));
target.SetValue(TestClass.Struct8Property, new(i + 1));
}
}
private class TestClass : AvaloniaObject
{
public static readonly StyledProperty<string> StringProperty =
AvaloniaProperty.Register<TestClass, string>("String");
public static readonly StyledProperty<Struct1> Struct1Property =
AvaloniaProperty.Register<TestClass, Struct1>("Struct1");
public static readonly StyledProperty<Struct2> Struct2Property =
AvaloniaProperty.Register<TestClass, Struct2>("Struct2");
public static readonly StyledProperty<Struct3> Struct3Property =
AvaloniaProperty.Register<TestClass, Struct3>("Struct3");
public static readonly StyledProperty<Struct4> Struct4Property =
AvaloniaProperty.Register<TestClass, Struct4>("Struct4");
public static readonly StyledProperty<Struct5> Struct5Property =
AvaloniaProperty.Register<TestClass, Struct5>("Struct5");
public static readonly StyledProperty<Struct6> Struct6Property =
AvaloniaProperty.Register<TestClass, Struct6>("Struct6");
public static readonly StyledProperty<Struct7> Struct7Property =
AvaloniaProperty.Register<TestClass, Struct7>("Struct7");
public static readonly StyledProperty<Struct8> Struct8Property =
AvaloniaProperty.Register<TestClass, Struct8>("Struct8");
}
private class BaselineTestClass
{
public string? StringProperty { get; set; }
public Struct1 Struct1Property { get; set; }
public Struct2 Struct2Property { get; set; }
public Struct3 Struct3Property { get; set; }
public Struct4 Struct4Property { get; set; }
public Struct5 Struct5Property { get; set; }
public Struct6 Struct6Property { get; set; }
public Struct7 Struct7Property { get; set; }
public Struct8 Struct8Property { get; set; }
}
}
}

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

Loading…
Cancel
Save