Browse Source

Merge pull request #7842 from robloo/colorspectrum

Add new ColorSpectrum Primitive
pull/8034/head
Max Katz 4 years ago
committed by GitHub
parent
commit
4f0ad9921b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      Avalonia.sln
  2. 13
      samples/ControlCatalog/App.xaml.cs
  3. 1
      samples/ControlCatalog/ControlCatalog.csproj
  4. 3
      samples/ControlCatalog/MainView.xaml
  5. 12
      samples/ControlCatalog/MainView.xaml.cs
  6. 29
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  7. 19
      samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
  8. 1
      samples/Sandbox/Sandbox.csproj
  9. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  10. 25
      src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
  11. 41
      src/Avalonia.Controls.ColorPicker/ColorChangedEventArgs.cs
  12. 414
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs
  13. 207
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  14. 1578
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  15. 86
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs
  16. 23
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs
  17. 23
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs
  18. 94
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs
  19. 73
      src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs
  20. 26
      src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs
  21. 47
      src/Avalonia.Controls.ColorPicker/HsvComponent.cs
  22. 8
      src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs
  23. 134
      src/Avalonia.Controls.ColorPicker/Themes/Default.xaml
  24. 134
      src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml
  25. 17
      src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs
  26. 46
      src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs
  27. 18
      src/Avalonia.Controls/Converters/Corners.cs
  28. 54
      src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs
  29. 1
      src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
  30. 2
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  31. 1
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  32. 4
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  33. 1
      tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

26
Avalonia.sln

@ -169,6 +169,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{1ECC012A-8837-4AE2-9BDA-3E2857898727}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
@ -1963,6 +1965,30 @@ Global
{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
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.Build.0 = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.ActiveCfg = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.Build.0 = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

13
samples/ControlCatalog/App.xaml.cs

@ -18,6 +18,16 @@ namespace ControlCatalog
DataContext = new ApplicationViewModel();
}
public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml")
};
public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml")
};
public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
@ -69,7 +79,8 @@ namespace ControlCatalog
public override void Initialize()
{
Styles.Insert(0, Fluent);
Styles.Insert(1, DataGridFluent);
Styles.Insert(1, ColorPickerFluent);
Styles.Insert(2, DataGridFluent);
AvaloniaXamlLoader.Load(this);
}

1
samples/ControlCatalog/ControlCatalog.csproj

@ -23,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
<ProjectReference Include="..\SampleControls\ControlSamples.csproj" />

3
samples/ControlCatalog/MainView.xaml

@ -43,6 +43,9 @@
<TabItem Header="Clipboard">
<pages:ClipboardPage />
</TabItem>
<TabItem Header="ColorPicker">
<pages:ColorPickerPage />
</TabItem>
<TabItem Header="ComboBox">
<pages:ComboBoxPage />
</TabItem>

12
samples/ControlCatalog/MainView.xaml.cs

@ -49,7 +49,8 @@ namespace ControlCatalog
App.Fluent.Mode = FluentThemeMode.Light;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.DataGridFluent;
Application.Current.Styles[1] = App.ColorPickerFluent;
Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.FluentDark)
{
@ -59,19 +60,22 @@ namespace ControlCatalog
App.Fluent.Mode = FluentThemeMode.Dark;
}
Application.Current.Styles[0] = App.Fluent;
Application.Current.Styles[1] = App.DataGridFluent;
Application.Current.Styles[1] = App.ColorPickerFluent;
Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.DefaultLight)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light;
Application.Current.Styles[0] = App.DefaultLight;
Application.Current.Styles[1] = App.DataGridDefault;
Application.Current.Styles[1] = App.ColorPickerDefault;
Application.Current.Styles[2] = App.DataGridDefault;
}
else if (theme == CatalogTheme.DefaultDark)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark;
Application.Current.Styles[0] = App.DefaultDark;
Application.Current.Styles[1] = App.DataGridDefault;
Application.Current.Styles[1] = App.ColorPickerDefault;
Application.Current.Styles[2] = App.DataGridDefault;
}
}
};

29
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -0,0 +1,29 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.ColorPickerPage">
<Grid ColumnDefinitions="Auto,Auto"
RowDefinitions="Auto,Auto">
<ColorSpectrum Grid.Column="0"
Grid.Row="0"
Color="Red"
Height="256"
Width="256" />
<ColorSpectrum Grid.Column="1"
Grid.Row="0"
Color="Green"
Shape="Ring"
Height="256"
Width="256" />
<ColorSpectrum Grid.Column="0"
Grid.Row="1"
CornerRadius="10"
Color="Blue"
Height="256"
Width="256" />
</Grid>
</UserControl>

19
samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public partial class ColorPickerPage : UserControl
{
public ColorPickerPage()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

1
samples/Sandbox/Sandbox.csproj

@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
</ItemGroup>

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -19,6 +19,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.ColorPicker, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

25
src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
<PackageId>Avalonia.Controls.ColorPicker</PackageId>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<!-- Compatibility with old apps -->
<EmbeddedResource Include="Themes\**\*.xaml" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\JetBrains.Annotations.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
</Project>

41
src/Avalonia.Controls.ColorPicker/ColorChangedEventArgs.cs

@ -0,0 +1,41 @@
// Portions of this source file are adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using System;
using Avalonia.Media;
namespace Avalonia.Controls
{
/// <summary>
/// Holds the details of a ColorChanged event.
/// </summary>
/// <remarks>
/// HSV color information is intentionally not provided.
/// Use <see cref="Color.ToHsv()"/> to obtain it.
/// </remarks>
public class ColorChangedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorChangedEventArgs"/> class.
/// </summary>
/// <param name="oldColor">The old/original color from before the change event.</param>
/// <param name="newColor">The new/updated color that triggered the change event.</param>
public ColorChangedEventArgs(Color oldColor, Color newColor)
{
OldColor = oldColor;
NewColor = newColor;
}
/// <summary>
/// Gets the old/original color from before the change event.
/// </summary>
public Color OldColor { get; private set; }
/// <summary>
/// Gets the new/updated color that triggered the change event.
/// </summary>
public Color NewColor { get; private set; }
}
}

414
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs

@ -0,0 +1,414 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Controls.Primitives
{
internal static class ColorHelpers
{
public const int CheckerSize = 4;
public static bool ToDisplayNameExists
{
get => false;
}
public static string ToDisplayName(Color color)
{
return string.Empty;
}
public static Hsv IncrementColorComponent(
Hsv originalHsv,
HsvComponent component,
IncrementDirection direction,
IncrementAmount amount,
bool shouldWrap,
double minBound,
double maxBound)
{
Hsv newHsv = originalHsv;
if (amount == IncrementAmount.Small || !ToDisplayNameExists)
{
// In order to avoid working with small values that can incur rounding issues,
// we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
newHsv.S *= 100;
newHsv.V *= 100;
// Note: *valueToIncrement replaced with ref local variable for C#, must be initialized
ref double valueToIncrement = ref newHsv.H;
double incrementAmount = 0.0;
// If we're adding a small increment, then we'll just add or subtract 1.
// If we're adding a large increment, then we want to snap to the next
// or previous major value - for hue, this is every increment of 30;
// for saturation and value, this is every increment of 10.
switch (component)
{
case HsvComponent.Hue:
valueToIncrement = ref newHsv.H;
incrementAmount = amount == IncrementAmount.Small ? 1 : 30;
break;
case HsvComponent.Saturation:
valueToIncrement = ref newHsv.S;
incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
break;
case HsvComponent.Value:
valueToIncrement = ref newHsv.V;
incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
double previousValue = valueToIncrement;
valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount);
// If the value has reached outside the bounds, we were previous at the boundary, and we should wrap,
// then we'll place the selection on the other side of the spectrum.
// Otherwise, we'll place it on the boundary that was exceeded.
if (valueToIncrement < minBound)
{
valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound;
}
if (valueToIncrement > maxBound)
{
valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound;
}
// We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range.
newHsv.S /= 100;
newHsv.V /= 100;
}
else
{
// While working with named colors, we're going to need to be working in actual HSV units,
// so we'll divide the min bound and max bound by 100 in the case of saturation or value,
// since we'll have received units between 0-100 and we need them within 0-1.
if (component == HsvComponent.Saturation ||
component == HsvComponent.Value)
{
minBound /= 100;
maxBound /= 100;
}
newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound);
}
return newHsv;
}
public static Hsv FindNextNamedColor(
Hsv originalHsv,
HsvComponent component,
IncrementDirection direction,
bool shouldWrap,
double minBound,
double maxBound)
{
// There's no easy way to directly get the next named color, so what we'll do
// is just iterate in the direction that we want to find it until we find a color
// in that direction that has a color name different than our current color name.
// Once we find a new color name, then we'll iterate across that color name until
// we find its bounds on the other side, and then select the color that is exactly
// in the middle of that color's bounds.
Hsv newHsv = originalHsv;
string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor());
string newColorName = originalColorName;
// Note: *newValue replaced with ref local variable for C#, must be initialized
double originalValue = 0.0;
ref double newValue = ref newHsv.H;
double incrementAmount = 0.0;
switch (component)
{
case HsvComponent.Hue:
originalValue = originalHsv.H;
newValue = ref newHsv.H;
incrementAmount = 1;
break;
case HsvComponent.Saturation:
originalValue = originalHsv.S;
newValue = ref newHsv.S;
incrementAmount = 0.01;
break;
case HsvComponent.Value:
originalValue = originalHsv.V;
newValue = ref newHsv.V;
incrementAmount = 0.01;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
bool shouldFindMidPoint = true;
while (newColorName == originalColorName)
{
double previousValue = newValue;
newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
bool justWrapped = false;
// If we've hit a boundary, then either we should wrap or we shouldn't.
// If we should, then we'll perform that wrapping if we were previously up against
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
if (newValue > maxBound)
{
if (shouldWrap)
{
newValue = minBound;
justWrapped = true;
}
else
{
newValue = maxBound;
shouldFindMidPoint = false;
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
else if (newValue < minBound)
{
if (shouldWrap)
{
newValue = maxBound;
justWrapped = true;
}
else
{
newValue = minBound;
shouldFindMidPoint = false;
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
break;
}
}
if (!justWrapped &&
previousValue != originalValue &&
Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue))
{
// If we've wrapped all the way back to the start and have failed to find a new color name,
// then we'll just quit - there isn't a new color name that we're going to find.
shouldFindMidPoint = false;
break;
}
newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
}
if (shouldFindMidPoint)
{
Hsv startHsv = newHsv;
Hsv currentHsv = startHsv;
double startEndOffset = 0;
string currentColorName = newColorName;
// Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized
ref double startValue = ref startHsv.H;
ref double currentValue = ref currentHsv.H;
double wrapIncrement = 0;
switch (component)
{
case HsvComponent.Hue:
startValue = ref startHsv.H;
currentValue = ref currentHsv.H;
wrapIncrement = 360.0;
break;
case HsvComponent.Saturation:
startValue = ref startHsv.S;
currentValue = ref currentHsv.S;
wrapIncrement = 1.0;
break;
case HsvComponent.Value:
startValue = ref startHsv.V;
currentValue = ref currentHsv.V;
wrapIncrement = 1.0;
break;
default:
throw new InvalidOperationException("Invalid HsvComponent.");
}
while (newColorName == currentColorName)
{
currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
// If we've hit a boundary, then either we should wrap or we shouldn't.
// If we should, then we'll perform that wrapping if we were previously up against
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
if (currentValue > maxBound)
{
if (shouldWrap)
{
currentValue = minBound;
startEndOffset = maxBound - minBound;
}
else
{
currentValue = maxBound;
break;
}
}
else if (currentValue < minBound)
{
if (shouldWrap)
{
currentValue = maxBound;
startEndOffset = minBound - maxBound;
}
else
{
currentValue = minBound;
break;
}
}
currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor());
}
newValue = (startValue + currentValue + startEndOffset) / 2;
// Dividing by 2 may have gotten us halfway through a single step, so we'll
// remove that half-step if it exists.
double leftoverValue = Math.Abs(newValue);
while (leftoverValue > incrementAmount)
{
leftoverValue -= incrementAmount;
}
newValue -= leftoverValue;
while (newValue < minBound)
{
newValue += wrapIncrement;
}
while (newValue > maxBound)
{
newValue -= wrapIncrement;
}
}
return newHsv;
}
public static double IncrementAlphaComponent(
double originalAlpha,
IncrementDirection direction,
IncrementAmount amount,
bool shouldWrap,
double minBound,
double maxBound)
{
// In order to avoid working with small values that can incur rounding issues,
// we'll multiple alpha by 100 to put it in the range of 0-100 instead of 0-1.
originalAlpha *= 100;
const double smallIncrementAmount = 1;
const double largeIncrementAmount = 10;
if (amount == IncrementAmount.Small)
{
originalAlpha += (direction == IncrementDirection.Lower ? -1 : 1) * smallIncrementAmount;
}
else
{
if (direction == IncrementDirection.Lower)
{
originalAlpha = Math.Ceiling((originalAlpha - largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount;
}
else
{
originalAlpha = Math.Floor((originalAlpha + largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount;
}
}
// If the value has reached outside the bounds and we should wrap, then we'll place the selection
// on the other side of the spectrum. Otherwise, we'll place it on the boundary that was exceeded.
if (originalAlpha < minBound)
{
originalAlpha = shouldWrap ? maxBound : minBound;
}
if (originalAlpha > maxBound)
{
originalAlpha = shouldWrap ? minBound : maxBound;
}
// We multiplied alpha by 100 previously, so now we want to put it back in the 0-1 range.
return originalAlpha / 100;
}
public static WriteableBitmap CreateBitmapFromPixelData(
int pixelWidth,
int pixelHeight,
List<byte> bgraPixelData)
{
Vector dpi = new Vector(96, 96); // Standard may need to change on some devices
WriteableBitmap bitmap = new WriteableBitmap(
new PixelSize(pixelWidth, pixelHeight),
dpi,
PixelFormat.Bgra8888,
AlphaFormat.Premul);
// Warning: This is highly questionable
using (var frameBuffer = bitmap.Lock())
{
Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count);
}
return bitmap;
}
/// <summary>
/// Gets the relative (perceptual) luminance/brightness of the given color.
/// 1 is closer to white while 0 is closer to black.
/// </summary>
/// <param name="color">The color to calculate relative luminance for.</param>
/// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
public static double GetRelativeLuminance(Color color)
{
// The equation for relative luminance is given by
//
// L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
//
// where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
//
// If L is closer to 1, then the color is closer to white; if it is closer to 0,
// then the color is closer to black. This is based on the fact that the human
// eye perceives green to be much brighter than red, which in turn is perceived to be
// brighter than blue.
double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4);
double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4);
double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4);
return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg);
}
}
}

207
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@ -0,0 +1,207 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
/// <inheritdoc/>
public partial class ColorSpectrum
{
/// <summary>
/// Gets or sets the currently selected color in the RGB color model.
/// </summary>
/// <remarks>
/// For control authors use <see cref="HsvColor"/> instead to avoid loss
/// of precision and color drifting.
/// </remarks>
public Color Color
{
get => GetValue(ColorProperty);
set => SetValue(ColorProperty, value);
}
/// <summary>
/// Defines the <see cref="Color"/> property.
/// </summary>
public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<ColorSpectrum, Color>(
nameof(Color),
Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF));
/// <summary>
/// Gets or sets the two HSV color components displayed by the spectrum.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public ColorSpectrumComponents Components
{
get => GetValue(ComponentsProperty);
set => SetValue(ComponentsProperty, value);
}
/// <summary>
/// Defines the <see cref="Components"/> property.
/// </summary>
public static readonly StyledProperty<ColorSpectrumComponents> ComponentsProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumComponents>(
nameof(Components),
ColorSpectrumComponents.HueSaturation);
/// <summary>
/// Gets or sets the currently selected color in the HSV color model.
/// </summary>
/// <remarks>
/// This should be used in all cases instead of the <see cref="Color"/> property.
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model and using
/// this property will avoid loss of precision and color drifting.
/// </remarks>
public HsvColor HsvColor
{
get => GetValue(HsvColorProperty);
set => SetValue(HsvColorProperty, value);
}
/// <summary>
/// Defines the <see cref="HsvColor"/> property.
/// </summary>
public static readonly StyledProperty<HsvColor> HsvColorProperty =
AvaloniaProperty.Register<ColorSpectrum, HsvColor>(
nameof(HsvColor),
new HsvColor(1, 0, 0, 1));
/// <summary>
/// Gets or sets the maximum value of the Hue component in the range from 0..359.
/// This property must be greater than <see cref="MinHue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MaxHue
{
get => GetValue(MaxHueProperty);
set => SetValue(MaxHueProperty, value);
}
/// <summary>
/// Defines the <see cref="MaxHue"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxHueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxHue), 359);
/// <summary>
/// Gets or sets the maximum value of the Saturation component in the range from 0..100.
/// This property must be greater than <see cref="MinSaturation"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MaxSaturation
{
get => GetValue(MaxSaturationProperty);
set => SetValue(MaxSaturationProperty, value);
}
/// <summary>
/// Defines the <see cref="MaxSaturation"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxSaturationProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxSaturation), 100);
/// <summary>
/// Gets or sets the maximum value of the Value component in the range from 0..100.
/// This property must be greater than <see cref="MinValue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MaxValue
{
get => GetValue(MaxValueProperty);
set => SetValue(MaxValueProperty, value);
}
/// <summary>
/// Defines the <see cref="MaxValue"/> property.
/// </summary>
public static readonly StyledProperty<int> MaxValueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxValue), 100);
/// <summary>
/// Gets or sets the minimum value of the Hue component in the range from 0..359.
/// This property must be less than <see cref="MaxHue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MinHue
{
get => GetValue(MinHueProperty);
set => SetValue(MinHueProperty, value);
}
/// <summary>
/// Defines the <see cref="MinHue"/> property.
/// </summary>
public static readonly StyledProperty<int> MinHueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinHue), 0);
/// <summary>
/// Gets or sets the minimum value of the Saturation component in the range from 0..100.
/// This property must be less than <see cref="MaxSaturation"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MinSaturation
{
get => GetValue(MinSaturationProperty);
set => SetValue(MinSaturationProperty, value);
}
/// <summary>
/// Defines the <see cref="MinSaturation"/> property.
/// </summary>
public static readonly StyledProperty<int> MinSaturationProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinSaturation), 0);
/// <summary>
/// Gets or sets the minimum value of the Value component in the range from 0..100.
/// This property must be less than <see cref="MaxValue"/>.
/// </summary>
/// <remarks>
/// Internally, the <see cref="ColorSpectrum"/> uses the HSV color model.
/// </remarks>
public int MinValue
{
get => GetValue(MinValueProperty);
set => SetValue(MinValueProperty, value);
}
/// <summary>
/// Defines the <see cref="MinValue"/> property.
/// </summary>
public static readonly StyledProperty<int> MinValueProperty =
AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinValue), 0);
/// <summary>
/// Gets or sets the displayed shape of the spectrum.
/// </summary>
public ColorSpectrumShape Shape
{
get => GetValue(ShapeProperty);
set => SetValue(ShapeProperty, value);
}
/// <summary>
/// Defines the <see cref="Shape"/> property.
/// </summary>
public static readonly StyledProperty<ColorSpectrumShape> ShapeProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumShape>(
nameof(Shape),
ColorSpectrumShape.Box);
}
}

1578
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

File diff suppressed because it is too large

86
src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs

@ -0,0 +1,86 @@
// Portions of this source file are adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Contains and allows modification of Hue, Saturation and Value components.
/// </summary>
/// <remarks>
/// The is a specialized struct optimized for permanence and memory:
/// <list type="bullet">
/// <item>This is not a read-only struct like <see cref="HsvColor"/> and allows editing the fields</item>
/// <item>Removes the alpha component unnecessary in core calculations</item>
/// <item>No component bounds checks or clamping is done.</item>
/// </list>
/// </remarks>
internal struct Hsv
{
/// <summary>
/// The Hue component in the range from 0..359.
/// </summary>
public double H;
/// <summary>
/// The Saturation component in the range from 0..1.
/// </summary>
public double S;
/// <summary>
/// The Value component in the range from 0..1.
/// </summary>
public double V;
/// <summary>
/// Initializes a new instance of the <see cref="Hsv"/> struct.
/// </summary>
/// <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>
public Hsv(double h, double s, double v)
{
H = h;
S = s;
V = v;
}
/// <summary>
/// Initializes a new instance of the <see cref="Hsv"/> struct.
/// </summary>
/// <param name="hsvColor">An existing <see cref="HsvColor"/> to convert to <see cref="Hsv"/>.</param>
public Hsv(HsvColor hsvColor)
{
H = hsvColor.H;
S = hsvColor.S;
V = hsvColor.V;
}
/// <summary>
/// Converts this <see cref="Hsv"/> struct into a standard <see cref="HsvColor"/>.
/// </summary>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new <see cref="HsvColor"/> representing this <see cref="Hsv"/> struct.</returns>
public HsvColor ToHsvColor(double alpha = 1.0)
{
// Clamping is done automatically in the constructor
return HsvColor.FromAhsv(alpha, H, S, V);
}
/// <summary>
/// Returns the <see cref="Rgb"/> color model equivalent of this <see cref="Hsv"/> color.
/// </summary>
/// <returns>The <see cref="Rgb"/> equivalent color.</returns>
public Rgb ToRgb()
{
// Instantiating a Color is unfortunately necessary to use existing conversions
// Clamping is done internally in the conversion method
Color color = HsvColor.ToRgb(H, S, V);
return new Rgb(color);
}
}
}

23
src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs

@ -0,0 +1,23 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Defines a relative amount that a color component should be incremented.
/// </summary>
internal enum IncrementAmount
{
/// <summary>
/// A smaller change in value.
/// </summary>
Small,
/// <summary>
/// A larger change in value.
/// </summary>
Large,
};
}

23
src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs

@ -0,0 +1,23 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Defines the direction a color component should be incremented.
/// </summary>
internal enum IncrementDirection
{
/// <summary>
/// Decreasing in value towards zero.
/// </summary>
Lower,
/// <summary>
/// Increasing in value towards positive infinity.
/// </summary>
Higher,
};
}

94
src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs

@ -0,0 +1,94 @@
// Portions of this source file are adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Contains and allows modification of Red, Green and Blue components.
/// </summary>
/// <remarks>
/// The is a specialized struct optimized for permanence and memory:
/// <list type="bullet">
/// <item>This is not a read-only struct like <see cref="Color"/> and allows editing the fields</item>
/// <item>Removes the alpha component unnecessary in core calculations</item>
/// <item>Normalizes RGB components in the range of 0..1 to simplify calculations.</item>
/// <item>No component bounds checks or clamping is done.</item>
/// </list>
/// </remarks>
internal struct Rgb
{
/// <summary>
/// The Red component in the range from 0..1.
/// </summary>
public double R;
/// <summary>
/// The Green component in the range from 0..1.
/// </summary>
public double G;
/// <summary>
/// The Blue component in the range from 0..1.
/// </summary>
public double B;
/// <summary>
/// Initializes a new instance of the <see cref="Rgb"/> struct.
/// </summary>
/// <param name="r">The Red component in the range from 0..1.</param>
/// <param name="g">The Green component in the range from 0..1.</param>
/// <param name="b">The Blue component in the range from 0..1.</param>
public Rgb(double r, double g, double b)
{
R = r;
G = g;
B = b;
}
/// <summary>
/// Initializes a new instance of the <see cref="Rgb"/> struct.
/// </summary>
/// <param name="color">An existing <see cref="Color"/> to convert to <see cref="Rgb"/>.</param>
public Rgb(Color color)
{
R = color.R / 255.0;
G = color.G / 255.0;
B = color.B / 255.0;
}
/// <summary>
/// Converts this <see cref="Rgb"/> struct into a standard <see cref="Color"/>.
/// </summary>
/// <param name="alpha">The Alpha component in the range from 0..1.</param>
/// <returns>A new <see cref="Color"/> representing this <see cref="Rgb"/> struct.</returns>
public Color ToColor(double alpha = 1.0)
{
return Color.FromArgb(
(byte)MathUtilities.Clamp(alpha * 255.0, 0x00, 0xFF),
(byte)MathUtilities.Clamp(R * 255.0, 0x00, 0xFF),
(byte)MathUtilities.Clamp(G * 255.0, 0x00, 0xFF),
(byte)MathUtilities.Clamp(B * 255.0, 0x00, 0xFF));
}
/// <summary>
/// Returns the <see cref="Hsv"/> color model equivalent of this <see cref="Rgb"/> color.
/// </summary>
/// <returns>The <see cref="Hsv"/> equivalent color.</returns>
public Hsv ToHsv()
{
// Instantiating an HsvColor is unfortunately necessary to use existing conversions
// Clamping must be done here as it isn't done in the conversion method (internal-use only)
HsvColor hsvColor = Color.ToHsv(
MathUtilities.Clamp(R, 0.0, 1.0),
MathUtilities.Clamp(G, 0.0, 1.0),
MathUtilities.Clamp(B, 0.0, 1.0));
return new Hsv(hsvColor);
}
}
}

73
src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs

@ -0,0 +1,73 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
/// <summary>
/// Defines the two HSV color components displayed by a <see cref="ColorSpectrum"/>.
/// </summary>
/// <remarks>
/// Order of the color components is important and correspond with an X/Y axis in Box
/// shape or a degree/radius in Ring shape.
/// </remarks>
public enum ColorSpectrumComponents
{
/// <summary>
/// The Hue and Value components.
/// </summary>
/// <remarks>
/// In Box shape, Hue is mapped to the X-axis and Value is mapped to the Y-axis.
/// In Ring shape, Hue is mapped to degrees and Value is mapped to radius.
/// </remarks>
HueValue,
/// <summary>
/// The Value and Hue components.
/// </summary>
/// <remarks>
/// In Box shape, Value is mapped to the X-axis and Hue is mapped to the Y-axis.
/// In Ring shape, Value is mapped to degrees and Hue is mapped to radius.
/// </remarks>
ValueHue,
/// <summary>
/// The Hue and Saturation components.
/// </summary>
/// <remarks>
/// In Box shape, Hue is mapped to the X-axis and Saturation is mapped to the Y-axis.
/// In Ring shape, Hue is mapped to degrees and Saturation is mapped to radius.
/// </remarks>
HueSaturation,
/// <summary>
/// The Saturation and Hue components.
/// </summary>
/// <remarks>
/// In Box shape, Saturation is mapped to the X-axis and Hue is mapped to the Y-axis.
/// In Ring shape, Saturation is mapped to degrees and Hue is mapped to radius.
/// </remarks>
SaturationHue,
/// <summary>
/// The Saturation and Value components.
/// </summary>
/// <remarks>
/// In Box shape, Saturation is mapped to the X-axis and Value is mapped to the Y-axis.
/// In Ring shape, Saturation is mapped to degrees and Value is mapped to radius.
/// </remarks>
SaturationValue,
/// <summary>
/// The Value and Saturation components.
/// </summary>
/// <remarks>
/// In Box shape, Value is mapped to the X-axis and Saturation is mapped to the Y-axis.
/// In Ring shape, Value is mapped to degrees and Saturation is mapped to radius.
/// </remarks>
ValueSaturation,
};
}

26
src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs

@ -0,0 +1,26 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
/// <summary>
/// Defines the shape of a <see cref="ColorSpectrum"/>.
/// </summary>
public enum ColorSpectrumShape
{
/// <summary>
/// The spectrum is in the shape of a rectangular or square box.
/// Note that more colors are visible to the user in Box shape.
/// </summary>
Box,
/// <summary>
/// The spectrum is in the shape of an ellipse or circle.
/// </summary>
Ring,
};
}

47
src/Avalonia.Controls.ColorPicker/HsvComponent.cs

@ -0,0 +1,47 @@
// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Media;
namespace Avalonia.Controls
{
/// <summary>
/// Defines a specific component in the HSV color model.
/// </summary>
public enum HsvComponent
{
/// <summary>
/// The Hue component.
/// </summary>
/// <remarks>
/// Also see: <see cref="HsvColor.H"/>
/// </remarks>
Hue,
/// <summary>
/// The Saturation component.
/// </summary>
/// <remarks>
/// Also see: <see cref="HsvColor.S"/>
/// </remarks>
Saturation,
/// <summary>
/// The Value component.
/// </summary>
/// <remarks>
/// Also see: <see cref="HsvColor.V"/>
/// </remarks>
Value,
/// <summary>
/// The Alpha component.
/// </summary>
/// <remarks>
/// Also see: <see cref="HsvColor.A"/>
/// </remarks>
Alpha
};
}

8
src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs

@ -0,0 +1,8 @@
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]

134
src/Avalonia.Controls.ColorPicker/Themes/Default.xaml

@ -0,0 +1,134 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True"
xmlns:converters="using:Avalonia.Controls.Converters">
<Styles.Resources>
<converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
<converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
</Styles.Resources>
<Style Selector="ColorSpectrum">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Panel x:Name="PART_LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel x:Name="PART_SizingPanel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ClipToBounds="True">
<Rectangle x:Name="PART_SpectrumRectangle"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Rectangle x:Name="PART_SpectrumOverlayRectangle"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Ellipse x:Name="PART_SpectrumEllipse"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
<Ellipse x:Name="PART_SpectrumOverlayEllipse"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
<Canvas x:Name="PART_InputTarget"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel x:Name="PART_SelectionEllipsePanel">
<Ellipse x:Name="FocusEllipse"
Margin="-2"
StrokeThickness="2"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Ellipse x:Name="SelectionEllipse"
StrokeThickness="2"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ToolTip.VerticalOffset="-20"
ToolTip.Placement="Top">
<ToolTip.Tip>
<ToolTip x:Name="PART_ColorNameToolTip" />
</ToolTip.Tip>
</Ellipse>
</Panel>
</Canvas>
<Rectangle x:Name="BorderRectangle"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Ellipse x:Name="BorderEllipse"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
</Panel>
</Panel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Normal -->
<!-- Separating this allows easier customization in applications -->
<Style Selector="ColorSpectrum /template/ Ellipse#BorderEllipse,
ColorSpectrum /template/ Rectangle#BorderRectangle">
<Setter Property="Stroke" Value="{DynamicResource ThemeBorderLowBrush}" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
<!-- Focus -->
<Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ColorSpectrum:focus-visible /template/ Ellipse#FocusEllipse">
<Setter Property="IsVisible" Value="True" />
</Style>
<!-- Selector Color -->
<Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse">
<Setter Property="Stroke" Value="White" />
</Style>
<Style Selector="ColorSpectrum /template/ Ellipse#SelectionEllipse">
<Setter Property="Stroke" Value="Black" />
</Style>
<Style Selector="ColorSpectrum:light-selector /template/ Ellipse#FocusEllipse">
<Setter Property="Stroke" Value="Black" />
</Style>
<Style Selector="ColorSpectrum:light-selector /template/ Ellipse#SelectionEllipse">
<Setter Property="Stroke" Value="White" />
</Style>
<Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse">
<Setter Property="Opacity" Value="0.8" />
</Style>
<!-- Selector Size -->
<Style Selector="ColorSpectrum /template/ Panel#PART_SelectionEllipsePanel">
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
</Style>
<Style Selector="ColorSpectrum:large-selector /template/ Panel#PART_SelectionEllipsePanel">
<Setter Property="Width" Value="48" />
<Setter Property="Height" Value="48" />
</Style>
</Styles>

134
src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml

@ -0,0 +1,134 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:CompileBindings="True"
xmlns:converters="using:Avalonia.Controls.Converters">
<Styles.Resources>
<converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
<converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
<converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
</Styles.Resources>
<Style Selector="ColorSpectrum">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Panel x:Name="PART_LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel x:Name="PART_SizingPanel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ClipToBounds="True">
<Rectangle x:Name="PART_SpectrumRectangle"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Rectangle x:Name="PART_SpectrumOverlayRectangle"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Ellipse x:Name="PART_SpectrumEllipse"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
<Ellipse x:Name="PART_SpectrumOverlayEllipse"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
<Canvas x:Name="PART_InputTarget"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Panel x:Name="PART_SelectionEllipsePanel">
<Ellipse x:Name="FocusEllipse"
Margin="-2"
StrokeThickness="2"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
<Ellipse x:Name="SelectionEllipse"
StrokeThickness="2"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ToolTip.VerticalOffset="-20"
ToolTip.Placement="Top">
<ToolTip.Tip>
<ToolTip x:Name="PART_ColorNameToolTip" />
</ToolTip.Tip>
</Ellipse>
</Panel>
</Canvas>
<Rectangle x:Name="BorderRectangle"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Box'}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
<Ellipse x:Name="BorderEllipse"
IsHitTestVisible="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{TemplateBinding Shape, Converter={StaticResource EnumValueEquals}, ConverterParameter='Ring'}" />
</Panel>
</Panel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Normal -->
<!-- Separating this allows easier customization in applications -->
<Style Selector="ColorSpectrum /template/ Ellipse#BorderEllipse,
ColorSpectrum /template/ Rectangle#BorderRectangle">
<Setter Property="Stroke" Value="{DynamicResource SystemControlForegroundListLowBrush}" />
<Setter Property="StrokeThickness" Value="1" />
</Style>
<!-- Focus -->
<Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ColorSpectrum:focus-visible /template/ Ellipse#FocusEllipse">
<Setter Property="IsVisible" Value="True" />
</Style>
<!-- Selector Color -->
<Style Selector="ColorSpectrum /template/ Ellipse#FocusEllipse">
<Setter Property="Stroke" Value="{DynamicResource SystemControlBackgroundChromeWhiteBrush}" />
</Style>
<Style Selector="ColorSpectrum /template/ Ellipse#SelectionEllipse">
<Setter Property="Stroke" Value="{DynamicResource SystemControlBackgroundChromeBlackHighBrush}" />
</Style>
<Style Selector="ColorSpectrum:light-selector /template/ Ellipse#FocusEllipse">
<Setter Property="Stroke" Value="{DynamicResource SystemControlBackgroundChromeBlackHighBrush}" />
</Style>
<Style Selector="ColorSpectrum:light-selector /template/ Ellipse#SelectionEllipse">
<Setter Property="Stroke" Value="{DynamicResource SystemControlBackgroundChromeWhiteBrush}" />
</Style>
<Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse">
<Setter Property="Opacity" Value="0.8" />
</Style>
<!-- Selector Size -->
<Style Selector="ColorSpectrum /template/ Panel#PART_SelectionEllipsePanel">
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
</Style>
<Style Selector="ColorSpectrum:large-selector /template/ Panel#PART_SelectionEllipsePanel">
<Setter Property="Width" Value="48" />
<Setter Property="Height" Value="48" />
</Style>
</Styles>

17
src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs

@ -7,17 +7,18 @@ namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts an existing CornerRadius struct to a new CornerRadius struct,
/// with filters applied to extract only the specified fields, leaving the others set to 0.
/// with filters applied to extract only the specified corners, leaving the others set to 0.
/// </summary>
public class CornerRadiusFilterConverter : IValueConverter
{
/// <summary>
/// Gets or sets the type of the filter applied to the <see cref="CornerRadiusFilterConverter"/>.
/// Gets or sets the corners to filter by.
/// Only the specified corners will be included in the converted <see cref="CornerRadius"/>.
/// </summary>
public CornerRadiusFilterKinds Filter { get; set; }
public Corners Filter { get; set; }
/// <summary>
/// Gets or sets the scale multiplier applied to the <see cref="CornerRadiusFilterConverter"/>.
/// Gets or sets the scale multiplier applied uniformly to each corner.
/// </summary>
public double Scale { get; set; } = 1;
@ -29,10 +30,10 @@ namespace Avalonia.Controls.Converters
}
return new CornerRadius(
Filter.HasAllFlags(CornerRadiusFilterKinds.TopLeft) ? radius.TopLeft * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.TopRight) ? radius.TopRight * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.BottomRight) ? radius.BottomRight * Scale : 0,
Filter.HasAllFlags(CornerRadiusFilterKinds.BottomLeft) ? radius.BottomLeft * Scale : 0);
Filter.HasAllFlags(Corners.TopLeft) ? radius.TopLeft * Scale : 0,
Filter.HasAllFlags(Corners.TopRight) ? radius.TopRight * Scale : 0,
Filter.HasAllFlags(Corners.BottomRight) ? radius.BottomRight * Scale : 0,
Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)

46
src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs

@ -0,0 +1,46 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts one corner of a <see cref="CornerRadius"/> to its double value.
/// </summary>
public class CornerRadiusToDoubleConverter : IValueConverter
{
/// <summary>
/// Gets or sets the specific corner of the <see cref="CornerRadius"/> to convert to double.
/// </summary>
public Corners Corner { get; set; }
/// <inheritdoc/>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (!(value is CornerRadius cornerRadius))
{
return AvaloniaProperty.UnsetValue;
}
switch (Corner)
{
case Corners.TopLeft:
return cornerRadius.TopLeft;
case Corners.TopRight:
return cornerRadius.TopRight;
case Corners.BottomRight:
return cornerRadius.BottomRight;
case Corners.BottomLeft:
return cornerRadius.BottomLeft;
default:
return 0.0;
}
}
/// <inheritdoc/>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

18
src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs → src/Avalonia.Controls/Converters/Corners.cs

@ -3,29 +3,33 @@
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Defines constants that specify the filter type for a <see cref="CornerRadiusFilterConverter"/> instance.
/// Defines constants that specify one or more corners of a <see cref="CornerRadius"/>.
/// </summary>
[Flags]
public enum CornerRadiusFilterKinds
public enum Corners
{
/// <summary>
/// No filter applied.
/// No corner.
/// </summary>
None,
/// <summary>
/// Filters TopLeft value.
/// The TopLeft corner.
/// </summary>
TopLeft = 1,
/// <summary>
/// Filters TopRight value.
/// The TopRight corner.
/// </summary>
TopRight = 2,
/// <summary>
/// Filters BottomLeft value.
/// The BottomLeft corner.
/// </summary>
BottomLeft = 4,
/// <summary>
/// Filters BottomRight value.
/// The BottomRight corner.
/// </summary>
BottomRight = 8
}

54
src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs

@ -0,0 +1,54 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converter that checks if an enum value is equal to the given parameter enum value.
/// </summary>
public class EnumValueEqualsConverter : IValueConverter
{
/// <inheritdoc/>
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
// Note: Unlike string comparisons, null/empty is not supported
// Both 'value' and 'parameter' must exist and if both are missing they are not considered equal
if (value != null &&
parameter != null)
{
Type type = value.GetType();
if (type.IsEnum)
{
var valueStr = value?.ToString();
var paramStr = parameter?.ToString();
if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
/*
// TODO: When .net Standard 2.0 is no longer supported the code can be changed to below
// This is a little more type safe
if (type.IsEnum &&
Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) &&
Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum))
{
return valueEnum == paramEnum;
}
*/
}
return false;
}
/// <inheritdoc/>
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new System.NotImplementedException();
}
}
}

1
src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs

@ -35,7 +35,6 @@ namespace Avalonia.Controls.Converters
Bottom ? Indent * thicknessDepth.Bottom : 0);
}
return new Thickness(0);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)

2
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -228,6 +228,8 @@ namespace Avalonia.Controls
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
UnregisterEvents();
UnregisterFlyoutEvents(Flyout);

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

@ -13,6 +13,7 @@
<Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />

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

@ -61,8 +61,8 @@
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/SplitButton.xaml" />
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/SplitView.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/DatePicker.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/TimePicker.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/DatePicker.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/TimePicker.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml"/>
<!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all-->

1
tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj

@ -9,6 +9,7 @@
<Import Project="..\..\build\NetFX.props" />
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />

Loading…
Cancel
Save