Browse Source

Merge pull request #10439 from robloo/colorpicker-updates-5

ColorPicker Updates & Fixes
pull/10801/head
Max Katz 4 years ago
committed by GitHub
parent
commit
2a08114cf8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  2. 2
      src/Avalonia.Base/Media/Color.cs
  3. 2
      src/Avalonia.Base/Media/HsvColor.cs
  4. 4
      src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs
  5. 2
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  6. 30
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  7. 10
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  8. 11
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  9. 26
      src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs
  10. 18
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
  11. 35
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs
  12. 169
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  13. 146
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  14. 2
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  15. 5
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  16. 67
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
  17. 3
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  18. 4
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  19. 67
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml
  20. 3
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  21. 8
      src/Avalonia.Controls/Slider.cs

35
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -28,6 +28,41 @@
<Grid Grid.Column="2" <Grid Grid.Column="2"
Grid.Row="0" Grid.Row="0"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid.Resources>
<x:Double x:Key="ColorSliderSize">24</x:Double>
<x:Double x:Key="ColorSliderTrackSize">18</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">12</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">9</CornerRadius>
<!-- Due to 'SystemControlForegroundBaseHighBrush' usage this only works in Fluent theme. -->
<!-- Otherwise it would be necessary to make custom light/dark resources. -->
<ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<Setter Property="BorderThickness" Value="5" />
<Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Ellipse Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Fill="Transparent"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</ControlTheme>
</Grid.Resources>
<ColorSpectrum x:Name="ColorSpectrum1" <ColorSpectrum x:Name="ColorSpectrum1"
Grid.Row="0" Grid.Row="0"
Color="Red" Color="Red"

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

@ -309,7 +309,7 @@ namespace Avalonia.Media
if (input.Length == 3 || input.Length == 4) if (input.Length == 3 || input.Length == 4)
{ {
var extendedLength = 2 * input.Length; var extendedLength = 2 * input.Length;
#if !BUILDTASK #if !BUILDTASK
Span<char> extended = stackalloc char[extendedLength]; Span<char> extended = stackalloc char[extendedLength];
#else #else

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

@ -131,7 +131,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <list type="bullet"> /// <list type="bullet">
/// <item>0 is a shade of gray (no color).</item> /// <item>0 is fully white (or a shade of gray) and shows no color.</item>
/// <item>1 is the full color.</item> /// <item>1 is the full color.</item>
/// </list> /// </list>
/// </remarks> /// </remarks>

4
src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs

@ -1,6 +1,4 @@
using Avalonia.Controls.Primitives; namespace Avalonia.Controls
namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down.

2
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs

@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives
if (accentStep != 0) if (accentStep != 0)
{ {
// ColorChanged will be invoked in OnPropertyChanged if the value is different // ColorChanged will be invoked in OnPropertyChanged if the value is different
HsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); SetCurrentValue(HsvColorProperty, AccentColorConverter.GetAccent(hsvColor, accentStep));
} }
} }
} }

30
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs

@ -96,8 +96,22 @@ namespace Avalonia.Controls.Primitives
// independent pixels of controls. // independent pixels of controls.
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var pixelWidth = Convert.ToInt32(Bounds.Width * scale); int pixelWidth;
var pixelHeight = Convert.ToInt32(Bounds.Height * scale); int pixelHeight;
if (base._track != null)
{
pixelWidth = Convert.ToInt32(base._track.Bounds.Width * scale);
pixelHeight = Convert.ToInt32(base._track.Bounds.Height * scale);
}
else
{
// As a fallback, attempt to calculate using the overall control size
// This shouldn't happen as a track is a required template part of a slider
// However, if it does, the spectrum will still be shown
pixelWidth = Convert.ToInt32(Bounds.Width * scale);
pixelHeight = Convert.ToInt32(Bounds.Height * scale);
}
if (pixelWidth != 0 && pixelHeight != 0) if (pixelWidth != 0 && pixelHeight != 0)
{ {
@ -373,7 +387,7 @@ namespace Avalonia.Controls.Primitives
ignorePropertyChanged = true; ignorePropertyChanged = true;
// Always keep the two color properties in sync // Always keep the two color properties in sync
HsvColor = Color.ToHsv(); SetCurrentValue(HsvColorProperty, Color.ToHsv());
SetColorToSliderValues(); SetColorToSliderValues();
UpdateBackground(); UpdateBackground();
@ -403,7 +417,7 @@ namespace Avalonia.Controls.Primitives
ignorePropertyChanged = true; ignorePropertyChanged = true;
// Always keep the two color properties in sync // Always keep the two color properties in sync
Color = HsvColor.ToRgb(); SetCurrentValue(ColorProperty, HsvColor.ToRgb());
SetColorToSliderValues(); SetColorToSliderValues();
UpdateBackground(); UpdateBackground();
@ -440,13 +454,13 @@ namespace Avalonia.Controls.Primitives
if (ColorModel == ColorModel.Hsva) if (ColorModel == ColorModel.Hsva)
{ {
HsvColor = hsvColor; SetCurrentValue(HsvColorProperty, hsvColor);
Color = hsvColor.ToRgb(); SetCurrentValue(ColorProperty, hsvColor.ToRgb());
} }
else else
{ {
Color = color; SetCurrentValue(ColorProperty, color);
HsvColor = color.ToHsv(); SetCurrentValue(HsvColorProperty, color.ToHsv());
} }
UpdatePseudoClasses(); UpdatePseudoClasses();

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

@ -96,10 +96,10 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Defines the <see cref="ThirdComponent"/> property. /// Defines the <see cref="ThirdComponent"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<ColorComponent> ThirdComponentProperty = public static readonly DirectProperty<ColorSpectrum, ColorComponent> ThirdComponentProperty =
AvaloniaProperty.Register<ColorSpectrum, ColorComponent>( AvaloniaProperty.RegisterDirect<ColorSpectrum, ColorComponent>(
nameof(ThirdComponent), nameof(ThirdComponent),
ColorComponent.Component3); // Value o => o.ThirdComponent);
/// <summary> /// <summary>
/// Gets or sets the currently selected color in the RGB color model. /// Gets or sets the currently selected color in the RGB color model.
@ -239,8 +239,8 @@ namespace Avalonia.Controls.Primitives
/// </remarks> /// </remarks>
public ColorComponent ThirdComponent public ColorComponent ThirdComponent
{ {
get => GetValue(ThirdComponentProperty); get => _thirdComponent;
protected set => SetValue(ThirdComponentProperty, value); private set => SetAndRaise(ThirdComponentProperty, ref _thirdComponent, value);
} }
} }
} }

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

@ -13,9 +13,9 @@ using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Reactive;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -48,6 +48,7 @@ namespace Avalonia.Controls.Primitives
private bool _isPointerPressed = false; private bool _isPointerPressed = false;
private bool _shouldShowLargeSelection = false; private bool _shouldShowLargeSelection = false;
private List<Hsv> _hsvValues = new List<Hsv>(); private List<Hsv> _hsvValues = new List<Hsv>();
private ColorComponent _thirdComponent = ColorComponent.Component3; // HsvComponent.Value
private IDisposable? _layoutRootDisposable; private IDisposable? _layoutRootDisposable;
private IDisposable? _selectionEllipsePanelDisposable; private IDisposable? _selectionEllipsePanelDisposable;
@ -403,7 +404,7 @@ namespace Avalonia.Controls.Primitives
_updatingHsvColor = true; _updatingHsvColor = true;
Hsv newHsv = (new Rgb(color)).ToHsv(); Hsv newHsv = (new Rgb(color)).ToHsv();
HsvColor = newHsv.ToHsvColor(color.A / 255.0); SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(color.A / 255.0));
_updatingHsvColor = false; _updatingHsvColor = false;
UpdateEllipse(); UpdateEllipse();
@ -534,7 +535,7 @@ namespace Avalonia.Controls.Primitives
_updatingColor = true; _updatingColor = true;
Rgb newRgb = (new Hsv(hsvColor)).ToRgb(); Rgb newRgb = (new Hsv(hsvColor)).ToRgb();
Color = newRgb.ToColor(hsvColor.A); SetCurrentValue(ColorProperty, newRgb.ToColor(hsvColor.A));
_updatingColor = false; _updatingColor = false;
@ -608,8 +609,8 @@ namespace Avalonia.Controls.Primitives
Rgb newRgb = newHsv.ToRgb(); Rgb newRgb = newHsv.ToRgb();
double alpha = HsvColor.A; double alpha = HsvColor.A;
Color = newRgb.ToColor(alpha); SetCurrentValue(ColorProperty, newRgb.ToColor(alpha));
HsvColor = newHsv.ToHsvColor(alpha); SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha));
UpdateEllipse(); UpdateEllipse();
UpdatePseudoClasses(); UpdatePseudoClasses();

26
src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs

@ -0,0 +1,26 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines the position of a color's alpha component relative to all other components.
/// </summary>
public enum AlphaComponentPosition
{
/// <summary>
/// The alpha component occurs before all other components.
/// </summary>
/// <remarks>
/// For example, this may indicate the #AARRGGBB or ARGB format which
/// is the default format for XAML itself and the Color struct.
/// </remarks>
Leading,
/// <summary>
/// The alpha component occurs after all other components.
/// </summary>
/// <remarks>
/// For example, this may indicate the #RRGGBBAA or RGBA format which
/// is the default format for CSS.
/// </remarks>
Trailing,
}
}

18
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs

@ -42,6 +42,14 @@ namespace Avalonia.Controls
nameof(ColorSpectrumShape), nameof(ColorSpectrumShape),
ColorSpectrumShape.Box); ColorSpectrumShape.Box);
/// <summary>
/// Defines the <see cref="HexInputAlphaPosition"/> property.
/// </summary>
public static readonly StyledProperty<AlphaComponentPosition> HexInputAlphaPositionProperty =
AvaloniaProperty.Register<ColorView, AlphaComponentPosition>(
nameof(HexInputAlphaPosition),
AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI
/// <summary> /// <summary>
/// Defines the <see cref="HsvColor"/> property. /// Defines the <see cref="HsvColor"/> property.
/// </summary> /// </summary>
@ -260,6 +268,16 @@ namespace Avalonia.Controls
set => SetValue(ColorSpectrumShapeProperty, value); set => SetValue(ColorSpectrumShapeProperty, value);
} }
/// <summary>
/// Gets or sets the position of the alpha component in the hexadecimal input box relative to
/// all other color components.
/// </summary>
public AlphaComponentPosition HexInputAlphaPosition
{
get => GetValue(HexInputAlphaPositionProperty);
set => SetValue(HexInputAlphaPositionProperty, value);
}
/// <inheritdoc cref="ColorSpectrum.HsvColor"/> /// <inheritdoc cref="ColorSpectrum.HsvColor"/>
public HsvColor HsvColor public HsvColor HsvColor
{ {

35
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs

@ -1,14 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using Avalonia.Controls.Converters; using Avalonia.Controls.Converters;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -28,8 +24,7 @@ namespace Avalonia.Controls
private TextBox? _hexTextBox; private TextBox? _hexTextBox;
private TabControl? _tabControl; private TabControl? _tabControl;
private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); protected bool _ignorePropertyChanged = false;
protected bool ignorePropertyChanged = false;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ColorView"/> class. /// Initializes a new instance of the <see cref="ColorView"/> class.
@ -46,11 +41,11 @@ namespace Avalonia.Controls
{ {
if (_hexTextBox != null) if (_hexTextBox != null)
{ {
var convertedColor = colorToHexConverter.ConvertBack(_hexTextBox.Text, typeof(Color), null, CultureInfo.CurrentCulture); var convertedColor = ColorToHexConverter.ParseHexString(_hexTextBox.Text ?? string.Empty, HexInputAlphaPosition);
if (convertedColor is Color color) if (convertedColor is Color color)
{ {
Color = color; SetCurrentValue(ColorProperty, color);
} }
// Re-apply the hex value // Re-apply the hex value
@ -66,7 +61,7 @@ namespace Avalonia.Controls
{ {
if (_hexTextBox != null) if (_hexTextBox != null)
{ {
_hexTextBox.Text = colorToHexConverter.Convert(Color, typeof(string), null, CultureInfo.CurrentCulture) as string; _hexTextBox.Text = ColorToHexConverter.ToHexString(Color, HexInputAlphaPosition);
} }
} }
@ -167,7 +162,7 @@ namespace Avalonia.Controls
// The work-around for this is done here where SelectedIndex is forcefully // The work-around for this is done here where SelectedIndex is forcefully
// synchronized with whatever the TabControl property value is. This is // synchronized with whatever the TabControl property value is. This is
// possible since selection validation is already done by this method. // possible since selection validation is already done by this method.
SelectedIndex = _tabControl.SelectedIndex; SetCurrentValue(SelectedIndexProperty, _tabControl.SelectedIndex);
} }
return; return;
@ -200,7 +195,7 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (ignorePropertyChanged) if (_ignorePropertyChanged)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
return; return;
@ -209,29 +204,29 @@ namespace Avalonia.Controls
// Always keep the two color properties in sync // Always keep the two color properties in sync
if (change.Property == ColorProperty) if (change.Property == ColorProperty)
{ {
ignorePropertyChanged = true; _ignorePropertyChanged = true;
HsvColor = Color.ToHsv(); SetCurrentValue(HsvColorProperty, Color.ToHsv());
SetColorToHexTextBox(); SetColorToHexTextBox();
OnColorChanged(new ColorChangedEventArgs( OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue<Color>(), change.GetOldValue<Color>(),
change.GetNewValue<Color>())); change.GetNewValue<Color>()));
ignorePropertyChanged = false; _ignorePropertyChanged = false;
} }
else if (change.Property == HsvColorProperty) else if (change.Property == HsvColorProperty)
{ {
ignorePropertyChanged = true; _ignorePropertyChanged = true;
Color = HsvColor.ToRgb(); SetCurrentValue(ColorProperty, HsvColor.ToRgb());
SetColorToHexTextBox(); SetColorToHexTextBox();
OnColorChanged(new ColorChangedEventArgs( OnColorChanged(new ColorChangedEventArgs(
change.GetOldValue<HsvColor>().ToRgb(), change.GetOldValue<HsvColor>().ToRgb(),
change.GetNewValue<HsvColor>().ToRgb())); change.GetNewValue<HsvColor>().ToRgb()));
ignorePropertyChanged = false; _ignorePropertyChanged = false;
} }
else if (change.Property == PaletteProperty) else if (change.Property == PaletteProperty)
{ {
@ -241,7 +236,7 @@ namespace Avalonia.Controls
// bound properties controlling the palette grid // bound properties controlling the palette grid
if (palette != null) if (palette != null)
{ {
PaletteColumnCount = palette.ColorCount; SetCurrentValue(PaletteColumnCountProperty, palette.ColorCount);
List<Color> newPaletteColors = new List<Color>(); List<Color> newPaletteColors = new List<Color>();
for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++) for (int shadeIndex = 0; shadeIndex < palette.ShadeCount; shadeIndex++)
@ -252,14 +247,14 @@ namespace Avalonia.Controls
} }
} }
PaletteColors = newPaletteColors; SetCurrentValue(PaletteColorsProperty, newPaletteColors);
} }
} }
else if (change.Property == IsAlphaEnabledProperty) else if (change.Property == IsAlphaEnabledProperty)
{ {
// Manually coerce the HsvColor value // Manually coerce the HsvColor value
// (Color will be coerced automatically if HsvColor changes) // (Color will be coerced automatically if HsvColor changes)
HsvColor = OnCoerceHsvColor(HsvColor); SetCurrentValue(HsvColorProperty, OnCoerceHsvColor(HsvColor));
} }
else if (change.Property == IsColorComponentsVisibleProperty || else if (change.Property == IsColorComponentsVisibleProperty ||
change.Property == IsColorPaletteVisibleProperty || change.Property == IsColorPaletteVisibleProperty ||

169
src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs

@ -2,6 +2,7 @@
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters namespace Avalonia.Controls.Converters
{ {
@ -10,6 +11,11 @@ namespace Avalonia.Controls.Converters
/// </summary> /// </summary>
public class ColorToHexConverter : IValueConverter public class ColorToHexConverter : IValueConverter
{ {
/// <summary>
/// Gets or sets the position of a color's alpha component relative to all other components.
/// </summary>
public AlphaComponentPosition AlphaPosition { get; set; } = AlphaComponentPosition.Leading;
/// <inheritdoc/> /// <inheritdoc/>
public object? Convert( public object? Convert(
object? value, object? value,
@ -42,16 +48,7 @@ namespace Avalonia.Controls.Converters
return AvaloniaProperty.UnsetValue; return AvaloniaProperty.UnsetValue;
} }
string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); return ToHexString(color, AlphaPosition, includeSymbol);
if (includeSymbol == false)
{
// TODO: When .net standard 2.0 is dropped, replace the below line
//hexColor = hexColor.Replace("#", string.Empty, StringComparison.Ordinal);
hexColor = hexColor.Replace("#", string.Empty);
}
return hexColor;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -62,21 +59,159 @@ namespace Avalonia.Controls.Converters
CultureInfo culture) CultureInfo culture)
{ {
string hexValue = value?.ToString() ?? string.Empty; string hexValue = value?.ToString() ?? string.Empty;
return ParseHexString(hexValue, AlphaPosition) ?? AvaloniaProperty.UnsetValue;
}
/// <summary>
/// Converts the given color to its hex color value string representation.
/// </summary>
/// <param name="color">The color to represent as a hex value string.</param>
/// <param name="alphaPosition">The output position of the alpha component.</param>
/// <param name="includeSymbol">Whether the hex symbol '#' will be added.</param>
/// <returns>The input color converted to its hex value string.</returns>
public static string ToHexString(
Color color,
AlphaComponentPosition alphaPosition,
bool includeSymbol = false)
{
uint intColor;
if (alphaPosition == AlphaComponentPosition.Trailing)
{
intColor = ((uint)color.R << 24) | ((uint)color.G << 16) | ((uint)color.B << 8) | (uint)color.A;
}
else
{
// Default is Leading alpha
intColor = ((uint)color.A << 24) | ((uint)color.R << 16) | ((uint)color.G << 8) | (uint)color.B;
}
if (Color.TryParse(hexValue, out Color color)) string hexColor = intColor.ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant();
if (includeSymbol)
{
hexColor = '#' + hexColor;
}
return hexColor;
}
/// <summary>
/// Parses a hex color value string into a new <see cref="Color"/>.
/// </summary>
/// <param name="hexColor">The hex color string to parse.</param>
/// <param name="alphaPosition">The input position of the alpha component.</param>
/// <returns>The parsed <see cref="Color"/>; otherwise, null.</returns>
public static Color? ParseHexString(
string hexColor,
AlphaComponentPosition alphaPosition)
{
hexColor = hexColor.Trim();
if (!hexColor.StartsWith("#", StringComparison.Ordinal))
{
hexColor = "#" + hexColor;
}
if (TryParseHexFormat(hexColor.AsSpan(), alphaPosition, out Color color))
{ {
return color; return color;
} }
else if (hexValue.StartsWith("#", StringComparison.Ordinal) == false &&
Color.TryParse("#" + hexValue, out Color color2)) return null;
}
/// <summary>
/// Parses the given span of characters representing a hex color value into a new <see cref="Color"/>.
/// </summary>
/// <remarks>
/// This is based on the Color.TryParseHexFormat() method.
/// It is copied because it needs to be extended to handle alpha position.
/// However, the alpha position enum is only available in the controls namespace with the ColorPicker control.
/// </remarks>
private static bool TryParseHexFormat(
ReadOnlySpan<char> s,
AlphaComponentPosition alphaPosition,
out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> input, AlphaComponentPosition alphaPosition, ref Color color)
{ {
return color2; var alphaComponent = 0u;
if (input.Length == 6)
{
if (alphaPosition == AlphaComponentPosition.Trailing)
{
alphaComponent = 0x000000FF;
}
else
{
alphaComponent = 0xFF000000;
}
}
else if (input.Length != 8)
{
return false;
}
if (!input.TryParseUInt(NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
{
return false;
}
if (alphaComponent != 0)
{
if (alphaPosition == AlphaComponentPosition.Trailing)
{
parsed = (parsed << 8) | alphaComponent;
}
else
{
parsed = parsed | alphaComponent;
}
}
if (alphaPosition == AlphaComponentPosition.Trailing)
{
// #RRGGBBAA
color = new Color(
a: (byte)(parsed & 0xFF),
r: (byte)((parsed >> 24) & 0xFF),
g: (byte)((parsed >> 16) & 0xFF),
b: (byte)((parsed >> 8) & 0xFF));
}
else
{
// #AARRGGBB
color = new Color(
a: (byte)((parsed >> 24) & 0xFF),
r: (byte)((parsed >> 16) & 0xFF),
g: (byte)((parsed >> 8) & 0xFF),
b: (byte)(parsed & 0xFF));
}
return true;
} }
else
color = default;
ReadOnlySpan<char> input = s.Slice(1);
// Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB).
if (input.Length == 3 || input.Length == 4)
{ {
// Invalid hex color value provided var extendedLength = 2 * input.Length;
return AvaloniaProperty.UnsetValue; Span<char> extended = stackalloc char[extendedLength];
for (int i = 0; i < input.Length; i++)
{
extended[2 * i + 0] = input[i];
extended[2 * i + 1] = input[i];
}
return TryParseCore(extended, alphaPosition, ref color);
} }
return TryParseCore(input, alphaPosition, ref color);
} }
} }
} }

146
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@ -1,6 +1,6 @@
using System; using System;
using System.Globalization;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -11,8 +11,11 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public static class ColorHelper public static class ColorHelper
{ {
private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>(); private static readonly Dictionary<HsvColor, string> _cachedDisplayNames = new Dictionary<HsvColor, string>();
private static readonly object cacheMutex = new object(); private static readonly Dictionary<KnownColor, string> _cachedKnownColorNames = new Dictionary<KnownColor, string>();
private static readonly object _displayNameCacheMutex = new object();
private static readonly object _knownColorCacheMutex = new object();
private static readonly KnownColor[] _knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
/// <summary> /// <summary>
/// Gets the relative (perceptual) luminance/brightness of the given color. /// Gets the relative (perceptual) luminance/brightness of the given color.
@ -59,7 +62,36 @@ namespace Avalonia.Controls.Primitives
/// <returns>The approximate color display name.</returns> /// <returns>The approximate color display name.</returns>
public static string ToDisplayName(Color color) public static string ToDisplayName(Color color)
{ {
// Without rounding, there are 16,777,216 possible RGB colors (without alpha). var hsvColor = color.ToHsv();
// Handle extremes that are outside the below algorithm
if (color.A == 0x00)
{
return GetDisplayName(KnownColor.Transparent);
}
// HSV ----------------------------------------------------------------------
//
// There are far too many possible HSV colors to cache and search through
// for performance reasons. Therefore, the HSV color is rounded.
// Rounding is tolerable in this algorithm because it is perception based.
// Hue is the most important for user perception so is rounded the least.
// Then there is a lot of loss in rounding the saturation and value components
// which are not as closely related to perceived color.
//
// Hue : Round to nearest int (0..360)
// Saturation : Round to the nearest 1/10 (0..1)
// Value : Round to the nearest 1/10 (0..1)
// Alpha : Is ignored in this algorithm
//
// Rounding results in ~36_000 values to cache in the worse case.
//
// RGB ----------------------------------------------------------------------
//
// The original algorithm worked in RGB color space.
// If this code is every adjusted to work in RGB again note the following:
//
// Without rounding, there are 16_777_216 possible RGB colors (without alpha).
// This is too many to cache and search through for performance reasons. // This is too many to cache and search through for performance reasons.
// It is also needlessly large as there are only ~140 known/named colors. // It is also needlessly large as there are only ~140 known/named colors.
// Therefore, rounding of the input color's component values is done to // Therefore, rounding of the input color's component values is done to
@ -68,42 +100,67 @@ namespace Avalonia.Controls.Primitives
// The rounding value of 5 is specially chosen. // The rounding value of 5 is specially chosen.
// It is a factor of 255 and therefore evenly divisible which improves // It is a factor of 255 and therefore evenly divisible which improves
// the quality of the calculations. // the quality of the calculations.
double rounding = 5; var roundedHsvColor = new HsvColor(
var roundedColor = new Color( 1.0,
0xFF, Math.Round(hsvColor.H, 0),
Convert.ToByte(Math.Round(color.R / rounding) * rounding), Math.Round(hsvColor.S, 1),
Convert.ToByte(Math.Round(color.G / rounding) * rounding), Math.Round(hsvColor.V, 1));
Convert.ToByte(Math.Round(color.B / rounding) * rounding));
// Attempt to use a previously cached display name // Attempt to use a previously cached display name
lock (cacheMutex) lock (_displayNameCacheMutex)
{ {
if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) if (_cachedDisplayNames.TryGetValue(roundedHsvColor, out var displayName))
{ {
return displayName; return displayName;
} }
} }
// Build the KnownColor name cache if it doesn't already exist
lock (_knownColorCacheMutex)
{
if (_cachedKnownColorNames.Count == 0)
{
for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1
{
KnownColor knownColor = _knownColors[i];
// Some known colors have the same numerical value. For example:
// - Aqua = 0xff00ffff
// - Cyan = 0xff00ffff
//
// This is not possible to represent in a dictionary which requires
// unique values. Therefore, only the first value is used.
if (!_cachedKnownColorNames.ContainsKey(knownColor))
{
_cachedKnownColorNames.Add(knownColor, GetDisplayName(knownColor));
}
}
}
}
// Find the closest known color by measuring 3D Euclidean distance (ignore alpha) // Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
// This is done in HSV color space to most closely match user-perception
var closestKnownColor = KnownColor.None; var closestKnownColor = KnownColor.None;
var closestKnownColorDistance = double.PositiveInfinity; var closestKnownColorDistance = double.PositiveInfinity;
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
for (int i = 1; i < knownColors.Length; i++) // Skip 'None' for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1
{ {
KnownColor knownColor = _knownColors[i];
// Transparent is skipped since alpha is ignored making it equivalent to White // Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColors[i] != KnownColor.Transparent) if (knownColor != KnownColor.Transparent)
{ {
Color knownColor = KnownColors.ToColor(knownColors[i]); HsvColor knownHsvColor = KnownColors.ToColor(knownColor).ToHsv();
double distance = Math.Sqrt( double distance = Math.Sqrt(
Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) + Math.Pow((roundedHsvColor.H - knownHsvColor.H), 2.0) +
Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) + Math.Pow((roundedHsvColor.S - knownHsvColor.S), 2.0) +
Math.Pow((double)(roundedColor.B - knownColor.B), 2.0)); Math.Pow((roundedHsvColor.V - knownHsvColor.V), 2.0));
if (distance < closestKnownColorDistance) if (distance < closestKnownColorDistance)
{ {
closestKnownColor = knownColors[i]; closestKnownColor = knownColor;
closestKnownColorDistance = distance; closestKnownColorDistance = distance;
} }
} }
@ -113,26 +170,19 @@ namespace Avalonia.Controls.Primitives
// Cache results for next time as well // Cache results for next time as well
if (closestKnownColor != KnownColor.None) if (closestKnownColor != KnownColor.None)
{ {
var sb = StringBuilderCache.Acquire(); string? displayName;
string name = closestKnownColor.ToString();
// Add spaces converting PascalCase to human-readable names lock (_knownColorCacheMutex)
for (int i = 0; i < name.Length; i++)
{ {
if (i != 0 && if (!_cachedKnownColorNames.TryGetValue(closestKnownColor, out displayName))
char.IsUpper(name[i]))
{ {
sb.Append(' '); displayName = GetDisplayName(closestKnownColor);
} }
sb.Append(name[i]);
} }
string displayName = StringBuilderCache.GetStringAndRelease(sb); lock (_displayNameCacheMutex)
lock (cacheMutex)
{ {
cachedDisplayNames.Add(roundedColor, displayName); _cachedDisplayNames.Add(roundedHsvColor, displayName);
} }
return displayName; return displayName;
@ -142,5 +192,35 @@ namespace Avalonia.Controls.Primitives
return string.Empty; return string.Empty;
} }
} }
/// <summary>
/// Gets the human-readable display name for the given <see cref="KnownColor"/>.
/// </summary>
/// <remarks>
/// This currently uses the <see cref="KnownColor"/> enum value's C# name directly
/// which limits it to the EN language only. In the future this should be localized
/// to other cultures.
/// </remarks>
/// <param name="knownColor">The <see cref="KnownColor"/> to get the display name for.</param>
/// <returns>The human-readable display name for the given <see cref="KnownColor"/>.</returns>
private static string GetDisplayName(KnownColor knownColor)
{
var sb = StringBuilderCache.Acquire();
string name = knownColor.ToString();
// Add spaces converting PascalCase to human-readable names
for (int i = 0; i < name.Length; i++)
{
if (i != 0 &&
char.IsUpper(name[i]))
{
sb.Append(' ');
}
sb.Append(name[i]);
}
return StringBuilderCache.GetStringAndRelease(sb);
}
} }
} }

2
src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs

@ -4,8 +4,6 @@
// Licensed to The Avalonia Project under the MIT License. // Licensed to The Avalonia Project under the MIT License.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Layout; using Avalonia.Layout;

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

@ -42,7 +42,8 @@
</Panel> </Panel>
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout FlyoutPresenterClasses="nopadding"> <Flyout FlyoutPresenterClasses="nopadding"
Placement="Top">
<!-- The following is copy-pasted from the ColorView's control template. <!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master). It MUST always be kept in sync with the ColorView (which is master).
@ -216,6 +217,7 @@
Content="RGB" Content="RGB"
CornerRadius="4,0,0,4" CornerRadius="4,0,0,4"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -223,6 +225,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,4,4,0" CornerRadius="0,4,4,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

67
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml

@ -1,13 +1,20 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Note that the Slider thumb should generally follow the overall Slider dimensions.
Therefore, there are not currently separate resources to control it. -->
<x:Double x:Key="ColorSliderSize">20</x:Double>
<x:Double x:Key="ColorSliderTrackSize">20</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">10</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">10</CornerRadius>
<ControlTheme x:Key="ColorSliderThumbTheme" <ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb"> TargetType="Thumb">
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
<!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />--> <!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />-->
<Setter Property="BorderThickness" Value="3" /> <Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate> <ControlTemplate>
@ -25,27 +32,28 @@
<Style Selector="^:horizontal"> <Style Selector="^:horizontal">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Height" Value="20" /> <Setter Property="Height" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{TemplateBinding Background}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
Height="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}" Value="{TemplateBinding Value, Mode=TwoWay}"
@ -82,7 +90,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"
@ -97,26 +105,27 @@
<Style Selector="^:vertical"> <Style Selector="^:vertical">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Width" Value="20" /> <Setter Property="Width" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
HorizontalAlignment="Stretch" Width="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
@ -154,7 +163,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"

3
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -8,7 +8,6 @@
<pc:ContrastBrushConverter x:Key="ContrastBrushConverter" /> <pc:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" /> <converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:ColorToHexConverter x:Key="ColorToHexConverter" />
<converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" /> <converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
<globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" /> <globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
<x:Double x:Key="ColorViewTabStripHeight">48</x:Double> <x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
@ -465,6 +464,7 @@
Content="RGB" Content="RGB"
CornerRadius="4,0,0,4" CornerRadius="4,0,0,4"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -472,6 +472,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,4,4,0" CornerRadius="0,4,4,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

4
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -42,7 +42,7 @@
</Panel> </Panel>
</DropDownButton.Content> </DropDownButton.Content>
<DropDownButton.Flyout> <DropDownButton.Flyout>
<Flyout> <Flyout Placement="Top">
<!-- The following is copy-pasted from the ColorView's control template. <!-- The following is copy-pasted from the ColorView's control template.
It MUST always be kept in sync with the ColorView (which is master). It MUST always be kept in sync with the ColorView (which is master).
@ -216,6 +216,7 @@
Content="RGB" Content="RGB"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -223,6 +224,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

67
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorSlider.xaml

@ -1,13 +1,20 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" <ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Note that the Slider thumb should generally follow the overall Slider dimensions.
Therefore, there are not currently separate resources to control it. -->
<x:Double x:Key="ColorSliderSize">20</x:Double>
<x:Double x:Key="ColorSliderTrackSize">20</x:Double>
<CornerRadius x:Key="ColorSliderCornerRadius">10</CornerRadius>
<CornerRadius x:Key="ColorSliderTrackCornerRadius">10</CornerRadius>
<ControlTheme x:Key="ColorSliderThumbTheme" <ControlTheme x:Key="ColorSliderThumbTheme"
TargetType="Thumb"> TargetType="Thumb">
<Setter Property="Background" Value="Transparent" /> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
<!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />--> <!--TODO: <Setter Property="BorderBrush" Value="{DynamicResource ColorControlDefaultSelectorBrush}" />-->
<Setter Property="BorderThickness" Value="3" /> <Setter Property="BorderThickness" Value="3" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate> <ControlTemplate>
@ -25,27 +32,28 @@
<Style Selector="^:horizontal"> <Style Selector="^:horizontal">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Height" Value="20" /> <Setter Property="Height" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Fill="{TemplateBinding Background}" Height="{Binding ElementName=PART_Track, Path=Bounds.Height}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
Height="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Center"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}" Value="{TemplateBinding Value, Mode=TwoWay}"
@ -82,7 +90,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"
@ -97,26 +105,27 @@
<Style Selector="^:vertical"> <Style Selector="^:vertical">
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" /> <Setter Property="CornerRadius" Value="{DynamicResource ColorSliderCornerRadius}" />
<Setter Property="Width" Value="20" /> <Setter Property="Width" Value="{DynamicResource ColorSliderSize}" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="{x:Type ColorSlider}"> <ControlTemplate TargetType="{x:Type ColorSlider}">
<Border BorderThickness="{TemplateBinding BorderThickness}" <Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Margin="{TemplateBinding Padding}"> <Grid Margin="{TemplateBinding Padding}">
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{StaticResource ColorControlCheckeredBackgroundBrush}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Rectangle HorizontalAlignment="Stretch" <Border HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Fill="{TemplateBinding Background}" Width="{Binding ElementName=PART_Track, Path=Bounds.Width}"
RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadiusConverter}}" Background="{TemplateBinding Background}"
RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadiusConverter}}" /> CornerRadius="{DynamicResource ColorSliderTrackCornerRadius}" />
<Track Name="PART_Track" <Track Name="PART_Track"
HorizontalAlignment="Stretch" Width="{DynamicResource ColorSliderTrackSize}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Minimum="{TemplateBinding Minimum}" Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}" Maximum="{TemplateBinding Maximum}"
@ -154,7 +163,7 @@
</RepeatButton> </RepeatButton>
</Track.IncreaseButton> </Track.IncreaseButton>
<Thumb Name="ColorSliderThumb" <Thumb Name="ColorSliderThumb"
Theme="{StaticResource ColorSliderThumbTheme}" Theme="{DynamicResource ColorSliderThumbTheme}"
Margin="0" Margin="0"
Padding="0" Padding="0"
DataContext="{TemplateBinding Value}" DataContext="{TemplateBinding Value}"

3
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -8,7 +8,6 @@
<pc:ContrastBrushConverter x:Key="ContrastBrushConverter" /> <pc:ContrastBrushConverter x:Key="ContrastBrushConverter" />
<converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" /> <converters:ColorToDisplayNameConverter x:Key="ColorToDisplayNameConverter" />
<converters:ColorToHexConverter x:Key="ColorToHexConverter" />
<converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" /> <converters:DoNothingForNullConverter x:Key="DoNothingForNullConverter" />
<globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" /> <globalization:NumberFormatInfo x:Key="ColorViewComponentNumberFormat" NumberDecimalDigits="0" />
<x:Double x:Key="ColorViewTabStripHeight">48</x:Double> <x:Double x:Key="ColorViewTabStripHeight">48</x:Double>
@ -427,6 +426,7 @@
Content="RGB" Content="RGB"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="1,1,0,1" BorderThickness="1,1,0,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Rgba}, Mode=TwoWay}" />
<RadioButton x:Name="HsvRadioButton" <RadioButton x:Name="HsvRadioButton"
Theme="{StaticResource ColorViewColorModelRadioButtonTheme}" Theme="{StaticResource ColorViewColorModelRadioButtonTheme}"
@ -434,6 +434,7 @@
Content="HSV" Content="HSV"
CornerRadius="0,0,0,0" CornerRadius="0,0,0,0"
BorderThickness="0,1,1,1" BorderThickness="0,1,1,1"
Height="{Binding ElementName=PART_HexTextBox, Path=Bounds.Height}"
IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" /> IsChecked="{TemplateBinding ColorModel, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static controls:ColorModel.Hsva}, Mode=TwoWay}" />
</Grid> </Grid>
<Grid x:Name="HexInputGrid" <Grid x:Name="HexInputGrid"

8
src/Avalonia.Controls/Slider.cs

@ -86,10 +86,10 @@ namespace Avalonia.Controls
TickBar.TicksProperty.AddOwner<Slider>(); TickBar.TicksProperty.AddOwner<Slider>();
// Slider required parts // Slider required parts
private bool _isDragging; protected bool _isDragging;
private Track? _track; protected Track? _track;
private Button? _decreaseButton; protected Button? _decreaseButton;
private Button? _increaseButton; protected Button? _increaseButton;
private IDisposable? _decreaseButtonPressDispose; private IDisposable? _decreaseButtonPressDispose;
private IDisposable? _decreaseButtonReleaseDispose; private IDisposable? _decreaseButtonReleaseDispose;
private IDisposable? _increaseButtonSubscription; private IDisposable? _increaseButtonSubscription;

Loading…
Cancel
Save