diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 649256ba83..b759720cf2 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -28,6 +28,41 @@ + + + 24 + 18 + 12 + 9 + + + + + + + + + + + + + + + + + + + + + extended = stackalloc char[extendedLength]; #else diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index f97457c54d..df68252065 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -131,7 +131,7 @@ namespace Avalonia.Media /// /// /// - /// 0 is a shade of gray (no color). + /// 0 is fully white (or a shade of gray) and shows no color. /// 1 is the full color. /// /// diff --git a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs index fc7c174ed6..c90c4cb5ac 100644 --- a/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs +++ b/src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs @@ -228,6 +228,44 @@ static unsafe class PixelFormatReader public void Reset(IntPtr address) => _address = (Rgba64*)address; } + + public unsafe struct Rgb24PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + _address += 3; + return new Rgba8888Pixel + { + R = addr[0], + G = addr[1], + B = addr[2], + A = 255, + }; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } + + public unsafe struct Bgr24PixelFormatReader : IPixelFormatReader + { + private byte* _address; + public Rgba8888Pixel ReadNext() + { + var addr = _address; + _address += 3; + return new Rgba8888Pixel + { + R = addr[2], + G = addr[1], + B = addr[0], + A = 255, + }; + } + + public void Reset(IntPtr address) => _address = (byte*)address; + } public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst, PixelFormat format) @@ -242,6 +280,10 @@ static unsafe class PixelFormatReader Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Gray16) Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Rgb24) + Transcode(dst, src, size, strideSrc, strideDst); + else if (format == PixelFormats.Bgr24) + Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Gray32Float) Transcode(dst, src, size, strideSrc, strideDst); else if (format == PixelFormats.Rgba64) @@ -258,7 +300,9 @@ static unsafe class PixelFormatReader || format == PixelFormats.Gray8 || format == PixelFormats.Gray16 || format == PixelFormats.Gray32Float - || format == PixelFormats.Rgba64; + || format == PixelFormats.Rgba64 + || format == PixelFormats.Bgr24 + || format == PixelFormats.Rgb24; } public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader diff --git a/src/Avalonia.Base/Platform/PixelFormat.cs b/src/Avalonia.Base/Platform/PixelFormat.cs index 99fe17055d..95f49bdb25 100644 --- a/src/Avalonia.Base/Platform/PixelFormat.cs +++ b/src/Avalonia.Base/Platform/PixelFormat.cs @@ -13,7 +13,9 @@ namespace Avalonia.Platform Gray8, Gray16, Gray32Float, - Rgba64 + Rgba64, + Rgb24, + Bgr24 } public record struct PixelFormat @@ -35,6 +37,8 @@ namespace Avalonia.Platform else if (FormatEnum == PixelFormatEnum.Rgb565 || FormatEnum == PixelFormatEnum.Gray16) return 16; + else if (FormatEnum is PixelFormatEnum.Bgr24 or PixelFormatEnum.Rgb24) + return 24; else if (FormatEnum == PixelFormatEnum.Rgba64) return 64; @@ -70,5 +74,7 @@ namespace Avalonia.Platform public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8); public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16); public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float); + public static PixelFormat Rgb24 { get; } = new PixelFormat(PixelFormatEnum.Rgb24); + public static PixelFormat Bgr24 { get; } = new PixelFormat(PixelFormatEnum.Bgr24); } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs index 01cb745ba7..92d8535272 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPicker/ColorPicker.cs @@ -1,6 +1,4 @@ -using Avalonia.Controls.Primitives; - -namespace Avalonia.Controls +namespace Avalonia.Controls { /// /// Presents a color for user editing using a spectrum, palette and component sliders within a drop down. diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 6f49430505..58702ecb61 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives if (accentStep != 0) { // ColorChanged will be invoked in OnPropertyChanged if the value is different - HsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); + SetCurrentValue(HsvColorProperty, AccentColorConverter.GetAccent(hsvColor, accentStep)); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index dd5e7d5b01..ce47a797ec 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -96,8 +96,22 @@ namespace Avalonia.Controls.Primitives // independent pixels of controls. var scale = LayoutHelper.GetLayoutScale(this); - var pixelWidth = Convert.ToInt32(Bounds.Width * scale); - var pixelHeight = Convert.ToInt32(Bounds.Height * scale); + int pixelWidth; + 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) { @@ -373,7 +387,7 @@ namespace Avalonia.Controls.Primitives ignorePropertyChanged = true; // Always keep the two color properties in sync - HsvColor = Color.ToHsv(); + SetCurrentValue(HsvColorProperty, Color.ToHsv()); SetColorToSliderValues(); UpdateBackground(); @@ -403,7 +417,7 @@ namespace Avalonia.Controls.Primitives ignorePropertyChanged = true; // Always keep the two color properties in sync - Color = HsvColor.ToRgb(); + SetCurrentValue(ColorProperty, HsvColor.ToRgb()); SetColorToSliderValues(); UpdateBackground(); @@ -440,13 +454,13 @@ namespace Avalonia.Controls.Primitives if (ColorModel == ColorModel.Hsva) { - HsvColor = hsvColor; - Color = hsvColor.ToRgb(); + SetCurrentValue(HsvColorProperty, hsvColor); + SetCurrentValue(ColorProperty, hsvColor.ToRgb()); } else { - Color = color; - HsvColor = color.ToHsv(); + SetCurrentValue(ColorProperty, color); + SetCurrentValue(HsvColorProperty, color.ToHsv()); } UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 5c7de2459b..2245eb8022 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -96,10 +96,10 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property. /// - public static readonly StyledProperty ThirdComponentProperty = - AvaloniaProperty.Register( + public static readonly DirectProperty ThirdComponentProperty = + AvaloniaProperty.RegisterDirect( nameof(ThirdComponent), - ColorComponent.Component3); // Value + o => o.ThirdComponent); /// /// Gets or sets the currently selected color in the RGB color model. @@ -239,8 +239,8 @@ namespace Avalonia.Controls.Primitives /// public ColorComponent ThirdComponent { - get => GetValue(ThirdComponentProperty); - protected set => SetValue(ThirdComponentProperty, value); + get => _thirdComponent; + private set => SetAndRaise(ThirdComponentProperty, ref _thirdComponent, value); } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 6f4c0003a8..9198a2f237 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -13,9 +13,9 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.Reactive; namespace Avalonia.Controls.Primitives { @@ -48,6 +48,7 @@ namespace Avalonia.Controls.Primitives private bool _isPointerPressed = false; private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); + private ColorComponent _thirdComponent = ColorComponent.Component3; // HsvComponent.Value private IDisposable? _layoutRootDisposable; private IDisposable? _selectionEllipsePanelDisposable; @@ -403,7 +404,7 @@ namespace Avalonia.Controls.Primitives _updatingHsvColor = true; Hsv newHsv = (new Rgb(color)).ToHsv(); - HsvColor = newHsv.ToHsvColor(color.A / 255.0); + SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(color.A / 255.0)); _updatingHsvColor = false; UpdateEllipse(); @@ -534,7 +535,7 @@ namespace Avalonia.Controls.Primitives _updatingColor = true; Rgb newRgb = (new Hsv(hsvColor)).ToRgb(); - Color = newRgb.ToColor(hsvColor.A); + SetCurrentValue(ColorProperty, newRgb.ToColor(hsvColor.A)); _updatingColor = false; @@ -608,8 +609,8 @@ namespace Avalonia.Controls.Primitives Rgb newRgb = newHsv.ToRgb(); double alpha = HsvColor.A; - Color = newRgb.ToColor(alpha); - HsvColor = newHsv.ToHsvColor(alpha); + SetCurrentValue(ColorProperty, newRgb.ToColor(alpha)); + SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha)); UpdateEllipse(); UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs b/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs new file mode 100644 index 0000000000..4f3ae46a24 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorView/AlphaComponentPosition.cs @@ -0,0 +1,26 @@ +namespace Avalonia.Controls +{ + /// + /// Defines the position of a color's alpha component relative to all other components. + /// + public enum AlphaComponentPosition + { + /// + /// The alpha component occurs before all other components. + /// + /// + /// For example, this may indicate the #AARRGGBB or ARGB format which + /// is the default format for XAML itself and the Color struct. + /// + Leading, + + /// + /// The alpha component occurs after all other components. + /// + /// + /// For example, this may indicate the #RRGGBBAA or RGBA format which + /// is the default format for CSS. + /// + Trailing, + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs index b76059037b..e334a1d323 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs @@ -42,6 +42,14 @@ namespace Avalonia.Controls nameof(ColorSpectrumShape), ColorSpectrumShape.Box); + /// + /// Defines the property. + /// + public static readonly StyledProperty HexInputAlphaPositionProperty = + AvaloniaProperty.Register( + nameof(HexInputAlphaPosition), + AlphaComponentPosition.Trailing); // Match CSS (and default slider order) instead of XAML/WinUI + /// /// Defines the property. /// @@ -260,6 +268,16 @@ namespace Avalonia.Controls set => SetValue(ColorSpectrumShapeProperty, value); } + /// + /// Gets or sets the position of the alpha component in the hexadecimal input box relative to + /// all other color components. + /// + public AlphaComponentPosition HexInputAlphaPosition + { + get => GetValue(HexInputAlphaPositionProperty); + set => SetValue(HexInputAlphaPositionProperty, value); + } + /// public HsvColor HsvColor { diff --git a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs index 977b1f5c84..274e7f5851 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorView/ColorView.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; using Avalonia.Controls.Converters; using Avalonia.Controls.Metadata; -using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Media; using Avalonia.Threading; -using Avalonia.VisualTree; namespace Avalonia.Controls { @@ -28,8 +24,7 @@ namespace Avalonia.Controls private TextBox? _hexTextBox; private TabControl? _tabControl; - private ColorToHexConverter colorToHexConverter = new ColorToHexConverter(); - protected bool ignorePropertyChanged = false; + protected bool _ignorePropertyChanged = false; /// /// Initializes a new instance of the class. @@ -46,11 +41,11 @@ namespace Avalonia.Controls { 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) { - Color = color; + SetCurrentValue(ColorProperty, color); } // Re-apply the hex value @@ -66,7 +61,7 @@ namespace Avalonia.Controls { 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 // synchronized with whatever the TabControl property value is. This is // possible since selection validation is already done by this method. - SelectedIndex = _tabControl.SelectedIndex; + SetCurrentValue(SelectedIndexProperty, _tabControl.SelectedIndex); } return; @@ -200,7 +195,7 @@ namespace Avalonia.Controls /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (ignorePropertyChanged) + if (_ignorePropertyChanged) { base.OnPropertyChanged(change); return; @@ -209,29 +204,29 @@ namespace Avalonia.Controls // Always keep the two color properties in sync if (change.Property == ColorProperty) { - ignorePropertyChanged = true; + _ignorePropertyChanged = true; - HsvColor = Color.ToHsv(); + SetCurrentValue(HsvColorProperty, Color.ToHsv()); SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue(), change.GetNewValue())); - ignorePropertyChanged = false; + _ignorePropertyChanged = false; } else if (change.Property == HsvColorProperty) { - ignorePropertyChanged = true; + _ignorePropertyChanged = true; - Color = HsvColor.ToRgb(); + SetCurrentValue(ColorProperty, HsvColor.ToRgb()); SetColorToHexTextBox(); OnColorChanged(new ColorChangedEventArgs( change.GetOldValue().ToRgb(), change.GetNewValue().ToRgb())); - ignorePropertyChanged = false; + _ignorePropertyChanged = false; } else if (change.Property == PaletteProperty) { @@ -241,7 +236,7 @@ namespace Avalonia.Controls // bound properties controlling the palette grid if (palette != null) { - PaletteColumnCount = palette.ColorCount; + SetCurrentValue(PaletteColumnCountProperty, palette.ColorCount); List newPaletteColors = new List(); 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) { // Manually coerce the HsvColor value // (Color will be coerced automatically if HsvColor changes) - HsvColor = OnCoerceHsvColor(HsvColor); + SetCurrentValue(HsvColorProperty, OnCoerceHsvColor(HsvColor)); } else if (change.Property == IsColorComponentsVisibleProperty || change.Property == IsColorPaletteVisibleProperty || diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs index 8d5f2332be..8798f874f4 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -2,6 +2,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Controls.Converters { @@ -10,6 +11,11 @@ namespace Avalonia.Controls.Converters /// public class ColorToHexConverter : IValueConverter { + /// + /// Gets or sets the position of a color's alpha component relative to all other components. + /// + public AlphaComponentPosition AlphaPosition { get; set; } = AlphaComponentPosition.Leading; + /// public object? Convert( object? value, @@ -42,16 +48,7 @@ namespace Avalonia.Controls.Converters return AvaloniaProperty.UnsetValue; } - string hexColor = color.ToUint32().ToString("x8", CultureInfo.InvariantCulture).ToUpperInvariant(); - - 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; + return ToHexString(color, AlphaPosition, includeSymbol); } /// @@ -62,21 +59,159 @@ namespace Avalonia.Controls.Converters CultureInfo culture) { string hexValue = value?.ToString() ?? string.Empty; + return ParseHexString(hexValue, AlphaPosition) ?? AvaloniaProperty.UnsetValue; + } + + /// + /// Converts the given color to its hex color value string representation. + /// + /// The color to represent as a hex value string. + /// The output position of the alpha component. + /// Whether the hex symbol '#' will be added. + /// The input color converted to its hex value string. + 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; + } + + /// + /// Parses a hex color value string into a new . + /// + /// The hex color string to parse. + /// The input position of the alpha component. + /// The parsed ; otherwise, null. + 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; } - else if (hexValue.StartsWith("#", StringComparison.Ordinal) == false && - Color.TryParse("#" + hexValue, out Color color2)) + + return null; + } + + /// + /// Parses the given span of characters representing a hex color value into a new . + /// + /// + /// 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. + /// + private static bool TryParseHexFormat( + ReadOnlySpan s, + AlphaComponentPosition alphaPosition, + out Color color) + { + static bool TryParseCore(ReadOnlySpan 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 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 - return AvaloniaProperty.UnsetValue; + var extendedLength = 2 * input.Length; + Span 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); } } } diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index c1a03b1b77..c9801c432b 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -1,6 +1,6 @@ using System; -using System.Globalization; using System.Collections.Generic; +using System.Globalization; using Avalonia.Media; using Avalonia.Utilities; @@ -11,8 +11,11 @@ namespace Avalonia.Controls.Primitives /// public static class ColorHelper { - private static readonly Dictionary cachedDisplayNames = new Dictionary(); - private static readonly object cacheMutex = new object(); + private static readonly Dictionary _cachedDisplayNames = new Dictionary(); + private static readonly Dictionary _cachedKnownColorNames = new Dictionary(); + private static readonly object _displayNameCacheMutex = new object(); + private static readonly object _knownColorCacheMutex = new object(); + private static readonly KnownColor[] _knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); /// /// Gets the relative (perceptual) luminance/brightness of the given color. @@ -59,7 +62,36 @@ namespace Avalonia.Controls.Primitives /// The approximate color display name. 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. // 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 @@ -68,42 +100,67 @@ namespace Avalonia.Controls.Primitives // The rounding value of 5 is specially chosen. // It is a factor of 255 and therefore evenly divisible which improves // the quality of the calculations. - double rounding = 5; - var roundedColor = new Color( - 0xFF, - Convert.ToByte(Math.Round(color.R / rounding) * rounding), - Convert.ToByte(Math.Round(color.G / rounding) * rounding), - Convert.ToByte(Math.Round(color.B / rounding) * rounding)); + var roundedHsvColor = new HsvColor( + 1.0, + Math.Round(hsvColor.H, 0), + Math.Round(hsvColor.S, 1), + Math.Round(hsvColor.V, 1)); // 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; } } + // 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) + // This is done in HSV color space to most closely match user-perception var closestKnownColor = KnownColor.None; 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 - 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( - Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) + - Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) + - Math.Pow((double)(roundedColor.B - knownColor.B), 2.0)); + Math.Pow((roundedHsvColor.H - knownHsvColor.H), 2.0) + + Math.Pow((roundedHsvColor.S - knownHsvColor.S), 2.0) + + Math.Pow((roundedHsvColor.V - knownHsvColor.V), 2.0)); if (distance < closestKnownColorDistance) { - closestKnownColor = knownColors[i]; + closestKnownColor = knownColor; closestKnownColorDistance = distance; } } @@ -113,26 +170,19 @@ namespace Avalonia.Controls.Primitives // Cache results for next time as well if (closestKnownColor != KnownColor.None) { - var sb = StringBuilderCache.Acquire(); - string name = closestKnownColor.ToString(); + string? displayName; - // Add spaces converting PascalCase to human-readable names - for (int i = 0; i < name.Length; i++) + lock (_knownColorCacheMutex) { - if (i != 0 && - char.IsUpper(name[i])) + if (!_cachedKnownColorNames.TryGetValue(closestKnownColor, out displayName)) { - sb.Append(' '); + displayName = GetDisplayName(closestKnownColor); } - - sb.Append(name[i]); } - string displayName = StringBuilderCache.GetStringAndRelease(sb); - - lock (cacheMutex) + lock (_displayNameCacheMutex) { - cachedDisplayNames.Add(roundedColor, displayName); + _cachedDisplayNames.Add(roundedHsvColor, displayName); } return displayName; @@ -142,5 +192,35 @@ namespace Avalonia.Controls.Primitives return string.Empty; } } + + /// + /// Gets the human-readable display name for the given . + /// + /// + /// This currently uses the enum value's C# name directly + /// which limits it to the EN language only. In the future this should be localized + /// to other cultures. + /// + /// The to get the display name for. + /// The human-readable display name for the given . + 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); + } } } diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index 819d745772..dbd92d4ac5 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -4,8 +4,6 @@ // Licensed to The Avalonia Project under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Layout; diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 807e4de0b1..a9f52b93c7 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -42,7 +42,8 @@ - + + 20 + 20 + 10 + 10 + - + @@ -25,27 +32,28 @@