From 25a34efd9a01cfb86698038cc54c4292ec1947ff Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:08:33 -0400 Subject: [PATCH] Implement color display name in ColorSpectrum --- .../ColorPreviewer.Properties.cs | 2 +- .../ColorSpectrum/ColorSpectrum.cs | 60 +++++---- .../Helpers/ColorHelpers.cs | 24 +--- .../Helpers/ColorNameHelpers.cs | 116 ++++++++++++++++++ .../Themes/Default/ColorSpectrum.xaml | 16 +-- .../Themes/Fluent/ColorSpectrum.xaml | 16 +-- 6 files changed, 174 insertions(+), 60 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index 903b5fb52b..f90f02551d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -41,7 +41,7 @@ namespace Avalonia.Controls.Primitives /// public bool ShowAccentColors { - get => (bool)this.GetValue(ShowAccentColorsProperty); + get => GetValue(ShowAccentColorsProperty); set => SetValue(ShowAccentColorsProperty, value); } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index fe9a2fac43..9b2459265d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -20,7 +20,6 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// - [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] [TemplatePart("PART_InputTarget", typeof(Canvas))] [TemplatePart("PART_LayoutRoot", typeof(Panel))] [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] @@ -60,7 +59,6 @@ namespace Avalonia.Controls.Primitives private Ellipse? _spectrumOverlayEllipse; private Canvas? _inputTarget; private Panel? _selectionEllipsePanel; - private ToolTip? _colorNameToolTip; // Put the spectrum images in a bitmap, which is then given to an ImageBrush. private WriteableBitmap? _hueRedBitmap; @@ -117,7 +115,6 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); // Failsafe - _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); @@ -152,10 +149,10 @@ namespace Avalonia.Controls.Primitives }); } - if (ColorHelpers.ToDisplayNameExists && - _colorNameToolTip != null) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color)); } // If we haven't yet created our bitmaps, do so now. @@ -338,26 +335,45 @@ namespace Avalonia.Controls.Primitives protected override void OnGotFocus(GotFocusEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, true); + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); + + base.OnGotFocus(e); } /// protected override void OnLostFocus(RoutedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) + { + ToolTip.SetIsOpen(_selectionEllipsePanel, false); + } + + UpdatePseudoClasses(); + + base.OnLostFocus(e); + } + + /// + protected override void OnPointerLeave(PointerEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, false); + ToolTip.SetIsOpen(_selectionEllipsePanel, false); } UpdatePseudoClasses(); + + base.OnPointerLeave(e); } /// @@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); ColorChanged?.Invoke(this, colorChangedEventArgs); - if (ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); - } + ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color)); } } } @@ -811,15 +825,11 @@ namespace Avalonia.Controls.Primitives Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. - if (ColorHelpers.ToDisplayNameExists) + if (IsFocused && + _selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, - // so toggling IsEnabled induces it to do that without incurring any visual glitches. - _colorNameToolTip.IsEnabled = false; - _colorNameToolTip.IsEnabled = true; - } + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs index 6500d10fe9..36ee478c7b 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs @@ -17,18 +17,6 @@ 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; - } - /// /// Generates a new bitmap of the specified size by changing a specific color component. /// This will produce a gradient representing a sweep of all possible values of the color component. @@ -325,7 +313,7 @@ namespace Avalonia.Controls.Primitives { Hsv newHsv = originalHsv; - if (amount == IncrementAmount.Small || !ToDisplayNameExists) + if (amount == IncrementAmount.Small || !ColorNameHelpers.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. @@ -416,7 +404,7 @@ namespace Avalonia.Controls.Primitives // in the middle of that color's bounds. Hsv newHsv = originalHsv; - string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); + string originalColorName = ColorNameHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); string newColorName = originalColorName; // Note: *newValue replaced with ref local variable for C#, must be initialized @@ -471,7 +459,7 @@ namespace Avalonia.Controls.Primitives { newValue = maxBound; shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); break; } } @@ -486,7 +474,7 @@ namespace Avalonia.Controls.Primitives { newValue = minBound; shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); break; } } @@ -501,7 +489,7 @@ namespace Avalonia.Controls.Primitives break; } - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); } if (shouldFindMidPoint) @@ -574,7 +562,7 @@ namespace Avalonia.Controls.Primitives } } - currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); + currentColorName = ColorNameHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); } newValue = (startValue + currentValue + startEndOffset) / 2; diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs new file mode 100644 index 0000000000..303a52f00b --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs @@ -0,0 +1,116 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using Avalonia.Media; +using System.Text; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains helpers useful when working with color names. + /// + public static class ColorNameHelpers + { + private static readonly Dictionary cachedDisplayNames = new Dictionary(); + private static readonly object cacheMutex = new object(); + + /// + /// Determines if color display names are supported based on the current thread culture. + /// + /// + /// Only English names are currently supported following known color names. + /// In the future known color names could be localized. + /// + public static bool ToDisplayNameExists + { + get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines an approximate display name for the given color. + /// + /// The color to get the display name for. + /// The approximate color display name. + public static string ToDisplayName(Color color) + { + // 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 + // reduce the color space into something more useful. + 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)); + + // Attempt to use a previously cached display name + lock (cacheMutex) + { + if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) + { + return displayName; + } + } + + // Find the closest known color by measuring 3D Euclidean distance (ignore alpha) + 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' + { + // Transparent is skipped since alpha is ignored making it equivalent to White + if (knownColors[i] != KnownColor.Transparent) + { + Color knownColor = KnownColors.ToColor(knownColors[i]); + + 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)); + + if (distance < closestKnownColorDistance) + { + closestKnownColor = knownColors[i]; + closestKnownColorDistance = distance; + } + } + } + + // Return the closest known color as the display name + // Cache results for next time as well + if (closestKnownColor != KnownColor.None) + { + StringBuilder sb = new StringBuilder(); + string name = closestKnownColor.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]); + } + + string displayName = sb.ToString(); + + lock (cacheMutex) + { + cachedDisplayNames.Add(roundedColor, displayName); + } + + return displayName; + } + else + { + return string.Empty; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 9596ca9653..78e6da8aa3 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -48,7 +48,10 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + - - - - + VerticalAlignment="Stretch" /> + + + - + + - - - - + VerticalAlignment="Stretch" /> + + +