Ver Fonte

Merge pull request #11264 from robloo/colorpicker-updates-7

ColorPicker Updates
Max Katz há 2 anos atrás
pai
commit
16bc3224f9

+ 2 - 1
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@@ -103,7 +103,8 @@
                    HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />-->
       <ColorPreviewer Grid.Row="8"
                       IsAccentColorsVisible="False"
-                      HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
+                      HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}"
+                      Margin="0,2,0,0" />
     </Grid>
   </Grid>
 </UserControl>

+ 0 - 32
src/Avalonia.Base/Media/Color.cs

@@ -478,7 +478,6 @@ namespace Avalonia.Media
         /// <returns>The HSL equivalent color.</returns>
         public HslColor ToHsl()
         {
-            // Don't use the HslColor(Color) constructor to avoid an extra HslColor
             return Color.ToHsl(R, G, B, A);
         }
 
@@ -488,7 +487,6 @@ namespace Avalonia.Media
         /// <returns>The HSV equivalent color.</returns>
         public HsvColor ToHsv()
         {
-            // Don't use the HsvColor(Color) constructor to avoid an extra HsvColor
             return Color.ToHsv(R, G, B, A);
         }
 
@@ -517,21 +515,6 @@ namespace Avalonia.Media
             }
         }
 
-        /// <summary>
-        /// Converts the given RGB color to its HSL color equivalent.
-        /// </summary>
-        /// <param name="color">The color in the RGB color model.</param>
-        /// <returns>A new <see cref="HslColor"/> equivalent to the given RGBA values.</returns>
-        public static HslColor ToHsl(Color color)
-        {
-            // Normalize RGBA components into the 0..1 range
-            return Color.ToHsl(
-                (byteToDouble * color.R),
-                (byteToDouble * color.G),
-                (byteToDouble * color.B),
-                (byteToDouble * color.A));
-        }
-
         /// <summary>
         /// Converts the given RGBA color component values to their HSL color equivalent.
         /// </summary>
@@ -606,21 +589,6 @@ namespace Avalonia.Media
             return new HslColor(a, 60 * h1, saturation, lightness, clampValues: false);
         }
 
-        /// <summary>
-        /// Converts the given RGB color to its HSV color equivalent.
-        /// </summary>
-        /// <param name="color">The color in the RGB color model.</param>
-        /// <returns>A new <see cref="HsvColor"/> equivalent to the given RGBA values.</returns>
-        public static HsvColor ToHsv(Color color)
-        {
-            // Normalize RGBA components into the 0..1 range
-            return Color.ToHsv(
-                (byteToDouble * color.R),
-                (byteToDouble * color.G),
-                (byteToDouble * color.B),
-                (byteToDouble * color.A));
-        }
-
         /// <summary>
         /// Converts the given RGBA color component values to their HSV color equivalent.
         /// </summary>

+ 65 - 13
src/Avalonia.Base/Media/HslColor.cs

@@ -90,7 +90,7 @@ namespace Avalonia.Media
         /// <param name="color">The RGB color to convert to HSL.</param>
         public HslColor(Color color)
         {
-            var hsl = Color.ToHsl(color);
+            var hsl = color.ToHsl();
 
             A = hsl.A;
             H = hsl.H;
@@ -165,10 +165,18 @@ namespace Avalonia.Media
         /// <returns>The RGB equivalent color.</returns>
         public Color ToRgb()
         {
-            // Use the by-component conversion method directly for performance
             return HslColor.ToRgb(H, S, L, A);
         }
 
+        /// <summary>
+        /// Returns the HSV color model equivalent of this HSL color.
+        /// </summary>
+        /// <returns>The HSV equivalent color.</returns>
+        public HsvColor ToHsv()
+        {
+            return HslColor.ToHsv(H, S, L, A);
+        }
+
         /// <inheritdoc/>
         public override string ToString()
         {
@@ -349,16 +357,6 @@ namespace Avalonia.Media
             return new HslColor(1.0, h, s, l);
         }
 
-        /// <summary>
-        /// Converts the given HSL color to its RGB color equivalent.
-        /// </summary>
-        /// <param name="hslColor">The color in the HSL color model.</param>
-        /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
-        public static Color ToRgb(HslColor hslColor)
-        {
-            return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
-        }
-
         /// <summary>
         /// Converts the given HSLA color component values to their RGB color equivalent.
         /// </summary>
@@ -442,13 +440,67 @@ namespace Avalonia.Media
                 b1 = x;
             }
 
-            return Color.FromArgb(
+            return new Color(
                 (byte)Math.Round(255 * alpha),
                 (byte)Math.Round(255 * (r1 + m)),
                 (byte)Math.Round(255 * (g1 + m)),
                 (byte)Math.Round(255 * (b1 + m)));
         }
 
+        /// <summary>
+        /// Converts the given HSLA color component values to their HSV color equivalent.
+        /// </summary>
+        /// <param name="hue">The Hue component in the HSL color model in the range from 0..360.</param>
+        /// <param name="saturation">The Saturation component in the HSL color model in the range from 0..1.</param>
+        /// <param name="lightness">The Lightness component in the HSL color model in the range from 0..1.</param>
+        /// <param name="alpha">The Alpha component in the range from 0..1.</param>
+        /// <returns>A new <see cref="HsvColor"/> equivalent to the given HSLA values.</returns>
+        public static HsvColor ToHsv(
+            double hue,
+            double saturation,
+            double lightness,
+            double alpha = 1.0)
+        {
+            // We want the hue to be between 0 and 359,
+            // so we first ensure that that's the case.
+            while (hue >= 360.0)
+            {
+                hue -= 360.0;
+            }
+
+            while (hue < 0.0)
+            {
+                hue += 360.0;
+            }
+
+            // We similarly clamp saturation, lightness and alpha between 0 and 1.
+            saturation = saturation < 0.0 ? 0.0 : saturation;
+            saturation = saturation > 1.0 ? 1.0 : saturation;
+
+            lightness = lightness < 0.0 ? 0.0 : lightness;
+            lightness = lightness > 1.0 ? 1.0 : lightness;
+
+            alpha = alpha < 0.0 ? 0.0 : alpha;
+            alpha = alpha > 1.0 ? 1.0 : alpha;
+
+            // The conversion algorithm is from the below link
+            // https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion
+
+            double s;
+            double v = lightness + (saturation * Math.Min(lightness, 1.0 - lightness));
+
+            if (v <= 0)
+            {
+                s = 0;
+            }
+            else
+            {
+                s = 2.0 * (1.0 - (lightness / v));
+            }
+
+            return new HsvColor(alpha, hue, s, v);
+        }
+
         /// <summary>
         /// Indicates whether the values of two specified <see cref="HslColor"/> objects are equal.
         /// </summary>

+ 65 - 13
src/Avalonia.Base/Media/HsvColor.cs

@@ -90,7 +90,7 @@ namespace Avalonia.Media
         /// <param name="color">The RGB color to convert to HSV.</param>
         public HsvColor(Color color)
         {
-            var hsv = Color.ToHsv(color);
+            var hsv = color.ToHsv();
 
             A = hsv.A;
             H = hsv.H;
@@ -195,10 +195,18 @@ namespace Avalonia.Media
         /// <returns>The RGB equivalent color.</returns>
         public Color ToRgb()
         {
-            // Use the by-component conversion method directly for performance
             return HsvColor.ToRgb(H, S, V, A);
         }
 
+        /// <summary>
+        /// Returns the HSL color model equivalent of this HSV color.
+        /// </summary>
+        /// <returns>The HSL equivalent color.</returns>
+        public HslColor ToHsl()
+        {
+            return HsvColor.ToHsl(H, S, V, A);
+        }
+
         /// <inheritdoc/>
         public override string ToString()
         {
@@ -379,16 +387,6 @@ namespace Avalonia.Media
             return new HsvColor(1.0, h, s, v);
         }
 
-        /// <summary>
-        /// Converts the given HSV color to its RGB color equivalent.
-        /// </summary>
-        /// <param name="hsvColor">The color in the HSV color model.</param>
-        /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSVA values.</returns>
-        public static Color ToRgb(HsvColor hsvColor)
-        {
-            return HsvColor.ToRgb(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A);
-        }
-
         /// <summary>
         /// Converts the given HSVA color component values to their RGB color equivalent.
         /// </summary>
@@ -520,13 +518,67 @@ namespace Avalonia.Media
                     break;
             }
 
-            return Color.FromArgb(
+            return new Color(
                 (byte)Math.Round(alpha * 255),
                 (byte)Math.Round(r * 255),
                 (byte)Math.Round(g * 255),
                 (byte)Math.Round(b * 255));
         }
 
+        /// <summary>
+        /// Converts the given HSVA color component values to their HSL color equivalent.
+        /// </summary>
+        /// <param name="hue">The Hue component in the HSV color model in the range from 0..360.</param>
+        /// <param name="saturation">The Saturation component in the HSV color model in the range from 0..1.</param>
+        /// <param name="value">The Value component in the HSV color model in the range from 0..1.</param>
+        /// <param name="alpha">The Alpha component in the range from 0..1.</param>
+        /// <returns>A new <see cref="HslColor"/> equivalent to the given HSVA values.</returns>
+        public static HslColor ToHsl(
+            double hue,
+            double saturation,
+            double value,
+            double alpha = 1.0)
+        {
+            // We want the hue to be between 0 and 359,
+            // so we first ensure that that's the case.
+            while (hue >= 360.0)
+            {
+                hue -= 360.0;
+            }
+
+            while (hue < 0.0)
+            {
+                hue += 360.0;
+            }
+
+            // We similarly clamp saturation, value and alpha between 0 and 1.
+            saturation = saturation < 0.0 ? 0.0 : saturation;
+            saturation = saturation > 1.0 ? 1.0 : saturation;
+
+            value = value < 0.0 ? 0.0 : value;
+            value = value > 1.0 ? 1.0 : value;
+
+            alpha = alpha < 0.0 ? 0.0 : alpha;
+            alpha = alpha > 1.0 ? 1.0 : alpha;
+
+            // The conversion algorithm is from the below link
+            // https://en.wikipedia.org/wiki/HSL_and_HSV#Interconversion
+
+            double s;
+            double l = value * (1.0 - (saturation / 2.0));
+
+            if (l <= 0 || l >= 1)
+            {
+                s = 0.0;
+            }
+            else
+            {
+                s = (value - l) / Math.Min(l, 1.0 - l);
+            }
+
+            return new HslColor(alpha, hue, s, l);
+        }
+
         /// <summary>
         /// Indicates whether the values of two specified <see cref="HsvColor"/> objects are equal.
         /// </summary>

+ 44 - 28
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs

@@ -41,11 +41,19 @@ namespace Avalonia.Controls.Primitives
                 defaultBindingMode: BindingMode.TwoWay);
 
         /// <summary>
-        /// Defines the <see cref="IsAlphaMaxForced"/> property.
+        /// Defines the <see cref="IsAlphaVisible"/> property.
         /// </summary>
-        public static readonly StyledProperty<bool> IsAlphaMaxForcedProperty =
+        public static readonly StyledProperty<bool> IsAlphaVisibleProperty =
             AvaloniaProperty.Register<ColorSlider, bool>(
-                nameof(IsAlphaMaxForced),
+                nameof(IsAlphaVisible),
+                false);
+
+        /// <summary>
+        /// Defines the <see cref="IsPerceptive"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsPerceptiveProperty =
+            AvaloniaProperty.Register<ColorSlider, bool>(
+                nameof(IsPerceptive),
                 true);
 
         /// <summary>
@@ -56,14 +64,6 @@ namespace Avalonia.Controls.Primitives
                 nameof(IsRoundingEnabled),
                 false);
 
-        /// <summary>
-        /// Defines the <see cref="IsSaturationValueMaxForced"/> property.
-        /// </summary>
-        public static readonly StyledProperty<bool> IsSaturationValueMaxForcedProperty =
-            AvaloniaProperty.Register<ColorSlider, bool>(
-                nameof(IsSaturationValueMaxForced),
-                true);
-
         /// <summary>
         /// Gets or sets the currently selected color in the RGB color model.
         /// </summary>
@@ -109,14 +109,41 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components
-        /// other than <see cref="ColorComponent"/>.
-        /// This ensures that the background is always visible and never transparent regardless of the actual color.
+        /// Gets or sets a value indicating whether the alpha component is visible and rendered.
+        /// When false, this ensures that the gradient is always visible and never transparent regardless of
+        /// the actual color. This property is ignored when the alpha component itself is being displayed.
+        /// </summary>
+        /// <remarks>
+        /// Setting to false means the alpha component is always forced to maximum for components other than
+        /// <see cref="ColorComponent"/> during rendering. This doesn't change the value of the alpha component
+        /// in the color – it is only for display.
+        /// </remarks>
+        public bool IsAlphaVisible
+        {
+            get => GetValue(IsAlphaVisibleProperty);
+            set => SetValue(IsAlphaVisibleProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the slider adapts rendering to improve user-perception
+        /// over exactness.
         /// </summary>
-        public bool IsAlphaMaxForced
+        /// <remarks>
+        /// When true in the HSVA color model, this ensures that the gradient is always visible and
+        /// never washed out regardless of the actual color. When true in the RGBA color model, this ensures
+        /// the gradient always appears as red, green or blue.
+        /// <br/><br/>
+        /// For example, with Hue in the HSVA color model, the Saturation and Value components are always forced
+        /// to maximum values during rendering. In the RGBA color model, all components other than
+        /// <see cref="ColorComponent"/> are forced to minimum values during rendering.
+        /// <br/><br/>
+        /// Note this property will only adjust components other than <see cref="ColorComponent"/> during rendering.
+        /// This also doesn't change the values of any components in the actual color – it is only for display.
+        /// </remarks>
+        public bool IsPerceptive
         {
-            get => GetValue(IsAlphaMaxForcedProperty);
-            set => SetValue(IsAlphaMaxForcedProperty, value);
+            get => GetValue(IsPerceptiveProperty);
+            set => SetValue(IsPerceptiveProperty, value);
         }
 
         /// <summary>
@@ -131,16 +158,5 @@ namespace Avalonia.Controls.Primitives
             get => GetValue(IsRoundingEnabledProperty);
             set => SetValue(IsRoundingEnabledProperty, value);
         }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values
-        /// when using the HSVA color model. Only component values other than <see cref="ColorComponent"/> will be changed.
-        /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color.
-        /// </summary>
-        public bool IsSaturationValueMaxForced
-        {
-            get => GetValue(IsSaturationValueMaxForcedProperty);
-            set => SetValue(IsSaturationValueMaxForcedProperty, value);
-        }
     }
 }

+ 50 - 38
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs

@@ -52,8 +52,7 @@ namespace Avalonia.Controls.Primitives
             // This means under a certain alpha threshold, neither a white or black selector thumb
             // should be shown and instead the default slider thumb color should be used instead.
             if (Color.A < 128 &&
-                (IsAlphaMaxForced == false ||
-                 ColorComponent == ColorComponent.Alpha))
+                (IsAlphaVisible || ColorComponent == ColorComponent.Alpha))
             {
                 PseudoClasses.Set(pcDarkSelector, false);
                 PseudoClasses.Set(pcLightSelector, false);
@@ -64,11 +63,11 @@ namespace Avalonia.Controls.Primitives
 
                 if (ColorModel == ColorModel.Hsva)
                 {
-                    perceivedColor = GetEquivalentBackgroundColor(HsvColor).ToRgb();
+                    perceivedColor = GetPerceptiveBackgroundColor(HsvColor).ToRgb();
                 }
                 else
                 {
-                    perceivedColor = GetEquivalentBackgroundColor(Color);
+                    perceivedColor = GetPerceptiveBackgroundColor(Color);
                 }
 
                 if (ColorHelper.GetRelativeLuminance(perceivedColor) <= 0.5)
@@ -108,7 +107,7 @@ namespace Avalonia.Controls.Primitives
             {
                 // 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
+                // However, if it does, the spectrum gradient will still be shown
                 pixelWidth = Convert.ToInt32(Bounds.Width * scale);
                 pixelHeight = Convert.ToInt32(Bounds.Height * scale);
             }
@@ -122,8 +121,8 @@ namespace Avalonia.Controls.Primitives
                     ColorModel,
                     ColorComponent,
                     HsvColor,
-                    IsAlphaMaxForced,
-                    IsSaturationValueMaxForced);
+                    IsAlphaVisible,
+                    IsPerceptive);
 
                 if (_backgroundBitmap != null)
                 {
@@ -316,40 +315,35 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         /// <param name="hsvColor">The actual color to get the equivalent background color for.</param>
         /// <returns>The equivalent, perceived background color.</returns>
-        private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor)
+        private HsvColor GetPerceptiveBackgroundColor(HsvColor hsvColor)
         {
             var component = ColorComponent;
-            var isAlphaMaxForced = IsAlphaMaxForced;
-            var isSaturationValueMaxForced = IsSaturationValueMaxForced;
+            var isAlphaVisible = IsAlphaVisible;
+            var isPerceptive = IsPerceptive;
 
-            if (isAlphaMaxForced &&
+            if (isAlphaVisible == false &&
                 component != ColorComponent.Alpha)
             {
                 hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V);
             }
 
-            switch (component)
+            if (isPerceptive)
             {
-                case ColorComponent.Component1:
-                    return new HsvColor(
-                        hsvColor.A,
-                        hsvColor.H,
-                        isSaturationValueMaxForced ? 1.0 : hsvColor.S,
-                        isSaturationValueMaxForced ? 1.0 : hsvColor.V);
-                case ColorComponent.Component2:
-                    return new HsvColor(
-                        hsvColor.A,
-                        hsvColor.H,
-                        hsvColor.S,
-                        isSaturationValueMaxForced ? 1.0 : hsvColor.V);
-                case ColorComponent.Component3:
-                    return new HsvColor(
-                        hsvColor.A,
-                        hsvColor.H,
-                        isSaturationValueMaxForced ? 1.0 : hsvColor.S,
-                        hsvColor.V);
-                default:
-                    return hsvColor;
+                switch (component)
+                {
+                    case ColorComponent.Component1:
+                        return new HsvColor(hsvColor.A, hsvColor.H, 1.0, 1.0);
+                    case ColorComponent.Component2:
+                        return new HsvColor(hsvColor.A, hsvColor.H, hsvColor.S, 1.0);
+                    case ColorComponent.Component3:
+                        return new HsvColor(hsvColor.A, hsvColor.H, 1.0, hsvColor.V);
+                    default:
+                        return hsvColor;
+                }
+            }
+            else
+            {
+                return hsvColor;
             }
         }
 
@@ -359,18 +353,36 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         /// <param name="rgbColor">The actual color to get the equivalent background color for.</param>
         /// <returns>The equivalent, perceived background color.</returns>
-        private Color GetEquivalentBackgroundColor(Color rgbColor)
+        private Color GetPerceptiveBackgroundColor(Color rgbColor)
         {
             var component = ColorComponent;
-            var isAlphaMaxForced = IsAlphaMaxForced;
+            var isAlphaVisible = IsAlphaVisible;
+            var isPerceptive = IsPerceptive;
 
-            if (isAlphaMaxForced &&
+            if (isAlphaVisible == false &&
                 component != ColorComponent.Alpha)
             {
                 rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B);
             }
 
-            return rgbColor;
+            if (isPerceptive)
+            {
+                switch (component)
+                {
+                    case ColorComponent.Component1:
+                        return new Color(rgbColor.A, rgbColor.R, 0, 0);
+                    case ColorComponent.Component2:
+                        return new Color(rgbColor.A, 0, rgbColor.G, 0);
+                    case ColorComponent.Component3:
+                        return new Color(rgbColor.A, 0, 0, rgbColor.B);
+                    default:
+                        return rgbColor;
+                }
+            }
+            else
+            {
+                return rgbColor;
+            }
         }
 
         /// <inheritdoc/>
@@ -401,8 +413,8 @@ namespace Avalonia.Controls.Primitives
             }
             else if (change.Property == ColorComponentProperty ||
                      change.Property == ColorModelProperty ||
-                     change.Property == IsAlphaMaxForcedProperty ||
-                     change.Property == IsSaturationValueMaxForcedProperty)
+                     change.Property == IsAlphaVisibleProperty ||
+                     change.Property == IsPerceptiveProperty)
             {
                 ignorePropertyChanged = true;
 

+ 90 - 1
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@@ -45,6 +45,7 @@ namespace Avalonia.Controls.Primitives
 
         private bool _updatingColor = false;
         private bool _updatingHsvColor = false;
+        private bool _coercedInitialColor = false;
         private bool _isPointerPressed = false;
         private bool _shouldShowLargeSelection = false;
         private List<Hsv> _hsvValues = new List<Hsv>();
@@ -601,14 +602,102 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        /// <summary>
+        /// Changes the currently selected color (always in HSV) and applies all necessary updates.
+        /// </summary>
+        /// <remarks>
+        /// Some additional logic is applied in certain situations to coerce and sync color values.
+        /// Use this method instead of update the <see cref="Color"/> or <see cref="HsvColor"/> directly.
+        /// </remarks>
+        /// <param name="newHsv">The new HSV color to change to.</param>
         private void UpdateColor(Hsv newHsv)
         {
             _updatingColor = true;
             _updatingHsvColor = true;
 
-            Rgb newRgb = newHsv.ToRgb();
             double alpha = HsvColor.A;
 
+            // It is common for the ColorPicker (and therefore the Spectrum) to be initialized
+            // with a #00000000 color value in some use cases. This is usually used to indicate
+            // that no color has been selected by the user. Note that #00000000 is different than
+            // #00FFFFFF (Transparent).
+            //
+            // In this situation, the first time the user clicks on the spectrum the third
+            // component and alpha component will remain zero. This is because the spectrum only
+            // controls two components at any given time.
+            //
+            // This is very unintuitive from a user-standpoint as after the user clicks on the
+            // spectrum they must then increase the alpha and then the third component sliders
+            // to the desired value. In fact, until they increase these slider values no color
+            // will show at all since it is fully transparent and black. In almost all cases
+            // though the desired value is simply full color.
+            //
+            // To work around this usability issue with an initial #00000000 color, the selected
+            // color is coerced (only the first time) into a color with maximum third component
+            // value and maximum alpha. This can only happen once and only if those two components
+            // are already zero.
+            //
+            // Also note this is NOT currently done for #00FFFFFF (Transparent) but based on
+            // further usability study that case may need to be handled here as well. Right now
+            // Transparent is treated as a normal color value with the alpha intentionally set
+            // to zero so the alpha slider must still be adjusted after the spectrum.
+            if (!_coercedInitialColor &&
+                IsLoaded)
+            {
+                bool isAlphaComponentZero = (alpha == 0.0);
+                bool isThirdComponentZero = false;
+
+                switch (Components)
+                {
+                    case ColorSpectrumComponents.HueValue:
+                    case ColorSpectrumComponents.ValueHue:
+                        isThirdComponentZero = (newHsv.S == 0.0);
+                        break;
+
+                    case ColorSpectrumComponents.HueSaturation:
+                    case ColorSpectrumComponents.SaturationHue:
+                        isThirdComponentZero = (newHsv.V == 0.0);
+                        break;
+
+                    case ColorSpectrumComponents.ValueSaturation:
+                    case ColorSpectrumComponents.SaturationValue:
+                        isThirdComponentZero = (newHsv.H == 0.0);
+                        break;
+                }
+
+                if (isAlphaComponentZero && isThirdComponentZero)
+                {
+                    alpha = 1.0;
+
+                    switch (Components)
+                    {
+                        case ColorSpectrumComponents.HueValue:
+                        case ColorSpectrumComponents.ValueHue:
+                            newHsv.S = 1.0;
+                            break;
+
+                        case ColorSpectrumComponents.HueSaturation:
+                        case ColorSpectrumComponents.SaturationHue:
+                            newHsv.V = 1.0;
+                            break;
+
+                        case ColorSpectrumComponents.ValueSaturation:
+                        case ColorSpectrumComponents.SaturationValue:
+                            // Hue is mathematically NOT a special case; however, is one conceptually.
+                            // It doesn't make sense to change the selected Hue value, so why is it set here?
+                            // Setting to 360.0 is equivalent to the max set for other components and is
+                            // internally wrapped back to 0.0 (since 360 degrees = 0 degrees).
+                            // This means effectively there is no change to the hue component value.
+                            newHsv.H = 360.0;
+                            break;
+                    }
+
+                    _coercedInitialColor = true;
+                }
+            }
+
+            Rgb newRgb = newHsv.ToRgb();
+
             SetCurrentValue(ColorProperty, newRgb.ToColor(alpha));
             SetCurrentValue(HsvColorProperty, newHsv.ToHsvColor(alpha));
 

+ 6 - 0
src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs

@@ -504,6 +504,12 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the index of the selected tab/panel/page (subview).
         /// </summary>
+        /// <remarks>
+        /// When using the default control theme, this property is designed to be used with the
+        /// <see cref="ColorViewTab"/> enum. The <see cref="ColorViewTab"/> enum defines the
+        /// index values of each of the three standard tabs.
+        /// Use like `SelectedIndex = (int)ColorViewTab.Palette`.
+        /// </remarks>
         public int SelectedIndex
         {
             get => GetValue(SelectedIndexProperty);

+ 39 - 21
src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs

@@ -29,11 +29,10 @@ namespace Avalonia.Controls.Primitives
         /// <param name="colorModel">The color model being used: RGBA or HSVA.</param>
         /// <param name="component">The specific color component to sweep.</param>
         /// <param name="baseHsvColor">The base HSV color used for components not being changed.</param>
-        /// <param name="isAlphaMaxForced">Fix the alpha component value to maximum during calculation.
-        /// This will remove any alpha/transparency from the other component backgrounds.</param>
-        /// <param name="isSaturationValueMaxForced">Fix the saturation and value components to maximum
-        /// during calculation with the HSVA color model.
-        /// This will ensure colors are always discernible regardless of saturation/value.</param>
+        /// <param name="isAlphaVisible">Whether the alpha component is visible and rendered in the bitmap.
+        /// This property is ignored when the alpha component itself is being rendered.</param>
+        /// <param name="isPerceptive">Whether the slider adapts rendering to improve user-perception over exactness.
+        /// This will ensure colors are always discernible.</param>
         /// <returns>A new bitmap representing a gradient of color component values.</returns>
         public static async Task<ArrayList<byte>> CreateComponentBitmapAsync(
             int width,
@@ -42,8 +41,8 @@ namespace Avalonia.Controls.Primitives
             ColorModel colorModel,
             ColorComponent component,
             HsvColor baseHsvColor,
-            bool isAlphaMaxForced,
-            bool isSaturationValueMaxForced)
+            bool isAlphaVisible,
+            bool isPerceptive)
         {
             if (width == 0 || height == 0)
             {
@@ -67,7 +66,7 @@ namespace Avalonia.Controls.Primitives
                 bgraPixelDataWidth  = width * 4;
 
                 // Maximize alpha component value
-                if (isAlphaMaxForced &&
+                if (isAlphaVisible == false &&
                     component != ColorComponent.Alpha)
                 {
                     baseHsvColor = new HsvColor(1.0, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V);
@@ -79,22 +78,41 @@ namespace Avalonia.Controls.Primitives
                     baseRgbColor = baseHsvColor.ToRgb();
                 }
 
-                // Maximize Saturation and Value components when in HSVA mode
-                if (isSaturationValueMaxForced &&
-                    colorModel == ColorModel.Hsva &&
+                // Apply any perceptive adjustments to the color
+                if (isPerceptive &&
                     component != ColorComponent.Alpha)
                 {
-                    switch (component)
+                    if (colorModel == ColorModel.Hsva)
                     {
-                        case ColorComponent.Component1:
-                            baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0);
-                            break;
-                        case ColorComponent.Component2:
-                            baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0);
-                            break;
-                        case ColorComponent.Component3:
-                            baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V);
-                            break;
+                        // Maximize Saturation and Value components
+                        switch (component)
+                        {
+                            case ColorComponent.Component1: // Hue
+                                baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0);
+                                break;
+                            case ColorComponent.Component2: // Saturation
+                                baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0);
+                                break;
+                            case ColorComponent.Component3: // Value
+                                baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V);
+                                break;
+                        }
+                    }
+                    else
+                    {
+                        // Minimize component values other than the current one
+                        switch (component)
+                        {
+                            case ColorComponent.Component1: // Red
+                                baseRgbColor = new Color(baseRgbColor.A, baseRgbColor.R, 0, 0);
+                                break;
+                            case ColorComponent.Component2: // Green
+                                baseRgbColor = new Color(baseRgbColor.A, 0, baseRgbColor.G, 0);
+                                break;
+                            case ColorComponent.Component3: // Blue
+                                baseRgbColor = new Color(baseRgbColor.A, 0, 0, baseRgbColor.B);
+                                break;
+                        }
                     }
                 }
 

+ 1 - 1
src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Controls.Primitives
     /// Contains and allows modification of Hue, Saturation and Value components.
     /// </summary>
     /// <remarks>
-    ///   The is a specialized struct optimized for permanence and memory:
+    ///   The is a specialized struct optimized for performance and memory:
     ///   <list type="bullet">
     ///     <item>This is not a read-only struct like <see cref="HsvColor"/> and allows editing the fields</item>
     ///     <item>Removes the alpha component unnecessary in core calculations</item>

+ 1 - 1
src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Controls.Primitives
     /// Contains and allows modification of Red, Green and Blue components.
     /// </summary>
     /// <remarks>
-    ///   The is a specialized struct optimized for permanence and memory:
+    ///   The is a specialized struct optimized for performance and memory:
     ///   <list type="bullet">
     ///     <item>This is not a read-only struct like <see cref="Color"/> and allows editing the fields</item>
     ///     <item>Removes the alpha component unnecessary in core calculations</item>

+ 21 - 4
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@@ -113,8 +113,8 @@
                       <primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
                                               AutomationProperties.Name="Third Component"
                                               Grid.Column="0"
-                                              IsAlphaMaxForced="True"
-                                              IsSaturationValueMaxForced="False"
+                                              IsAlphaVisible="False"
+                                              IsPerceptive="True"
                                               Orientation="Vertical"
                                               ColorModel="Hsva"
                                               ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@@ -490,11 +490,11 @@
                   </TabItem>
                 </TabControl>
                 <!-- Previewer -->
-                <!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
+                <!-- Note that the drop shadow is allowed to extend past the control bounds -->
                 <primitives:ColorPreviewer Grid.Row="1"
                                            HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                            IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
-                                           Margin="12,-5,12,7"
+                                           Margin="12,0,12,12"
                                            IsVisible="{TemplateBinding IsColorPreviewVisible}" />
               </Grid>
             </Flyout>
@@ -502,6 +502,23 @@
         </DropDownButton>
       </ControlTemplate>
     </Setter>
+
+    <!--
+    <Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
+      <Setter Property="IsPerceptive" Value="True" />
+    </Style>
+
+    <Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    -->
+
   </ControlTheme>
 
 </ResourceDictionary>

+ 5 - 8
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml

@@ -8,7 +8,9 @@
 
   <ControlTheme x:Key="{x:Type ColorPreviewer}"
                 TargetType="ColorPreviewer">
-    <Setter Property="Height" Value="70" />
+    <Setter Property="Height" Value="50" />
+    <!-- The preview color drop shadow is allowed to extend outside the control bounds -->
+    <Setter Property="ClipToBounds" Value="False" />
     <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
     <Setter Property="Template">
       <ControlTemplate TargetType="{x:Type ColorPreviewer}">
@@ -21,7 +23,6 @@
                   Height="{StaticResource ColorPreviewerAccentSectionHeight}"
                   Width="{StaticResource ColorPreviewerAccentSectionWidth}"
                   ColumnDefinitions="*,*"
-                  Margin="0,0,-10,0"
                   VerticalAlignment="Center">
               <Border Grid.Column="0"
                       Grid.ColumnSpan="2"
@@ -43,7 +44,6 @@
                   Height="{StaticResource ColorPreviewerAccentSectionHeight}"
                   Width="{StaticResource ColorPreviewerAccentSectionWidth}"
                   ColumnDefinitions="*,*"
-                  Margin="-10,0,0,0"
                   VerticalAlignment="Center">
               <Border Grid.Column="0"
                       Grid.ColumnSpan="2"
@@ -64,10 +64,8 @@
             <Border Grid.Column="1"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
-                    Background="Transparent"
                     BoxShadow="0 0 10 2 #BF000000"
-                    CornerRadius="{TemplateBinding CornerRadius}"
-                    Margin="10">
+                    CornerRadius="{TemplateBinding CornerRadius}">
               <Panel>
                 <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
                         CornerRadius="{TemplateBinding CornerRadius}" />
@@ -82,8 +80,7 @@
           <Border CornerRadius="{TemplateBinding CornerRadius}"
                   IsVisible="{TemplateBinding IsAccentColorsVisible, Converter={x:Static BoolConverters.Not}}"
                   HorizontalAlignment="Stretch"
-                  VerticalAlignment="Stretch"
-                  Margin="0,10,0,10">
+                  VerticalAlignment="Stretch">
             <Panel>
               <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
                       CornerRadius="{TemplateBinding CornerRadius}" />

+ 21 - 4
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@@ -360,8 +360,8 @@
                 <primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
                                         AutomationProperties.Name="Third Component"
                                         Grid.Column="0"
-                                        IsAlphaMaxForced="True"
-                                        IsSaturationValueMaxForced="False"
+                                        IsAlphaVisible="False"
+                                        IsPerceptive="True"
                                         Orientation="Vertical"
                                         ColorModel="Hsva"
                                         ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@@ -737,15 +737,32 @@
             </TabItem>
           </TabControl>
           <!-- Previewer -->
-          <!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
+          <!-- Note that the drop shadow is allowed to extend past the control bounds -->
           <primitives:ColorPreviewer Grid.Row="1"
                                      HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                      IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
-                                     Margin="12,-5,12,7"
+                                     Margin="12,0,12,12"
                                      IsVisible="{TemplateBinding IsColorPreviewVisible}" />
         </Grid>
       </ControlTemplate>
     </Setter>
+
+    <!--
+    <Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
+      <Setter Property="IsPerceptive" Value="True" />
+    </Style>
+
+    <Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    -->
+
   </ControlTheme>
 
 </ResourceDictionary>

+ 21 - 4
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@@ -112,8 +112,8 @@
                       <primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
                                               AutomationProperties.Name="Third Component"
                                               Grid.Column="0"
-                                              IsAlphaMaxForced="True"
-                                              IsSaturationValueMaxForced="False"
+                                              IsAlphaVisible="False"
+                                              IsPerceptive="True"
                                               Orientation="Vertical"
                                               ColorModel="Hsva"
                                               ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@@ -489,11 +489,11 @@
                   </TabItem>
                 </TabControl>
                 <!-- Previewer -->
-                <!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
+                <!-- Note that the drop shadow is allowed to extend past the control bounds -->
                 <primitives:ColorPreviewer Grid.Row="1"
                                            HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                            IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
-                                           Margin="12,-5,12,7"
+                                           Margin="12,0,12,12"
                                            IsVisible="{TemplateBinding IsColorPreviewVisible}" />
               </Grid>
             </Flyout>
@@ -501,6 +501,23 @@
         </DropDownButton>
       </ControlTemplate>
     </Setter>
+
+    <!--
+    <Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
+      <Setter Property="IsPerceptive" Value="True" />
+    </Style>
+
+    <Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    -->
+
   </ControlTheme>
 
 </ResourceDictionary>

+ 5 - 8
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml

@@ -8,7 +8,9 @@
 
   <ControlTheme x:Key="{x:Type ColorPreviewer}"
                 TargetType="ColorPreviewer">
-    <Setter Property="Height" Value="70" />
+    <Setter Property="Height" Value="50" />
+    <!-- The preview color drop shadow is allowed to extend outside the control bounds -->
+    <Setter Property="ClipToBounds" Value="False" />
     <Setter Property="CornerRadius" Value="0" />
     <Setter Property="Template">
       <ControlTemplate TargetType="{x:Type ColorPreviewer}">
@@ -21,7 +23,6 @@
                   Height="{StaticResource ColorPreviewerAccentSectionHeight}"
                   Width="{StaticResource ColorPreviewerAccentSectionWidth}"
                   ColumnDefinitions="*,*"
-                  Margin="0,0,-10,0"
                   VerticalAlignment="Center">
               <Border Grid.Column="0"
                       Grid.ColumnSpan="2"
@@ -43,7 +44,6 @@
                   Height="{StaticResource ColorPreviewerAccentSectionHeight}"
                   Width="{StaticResource ColorPreviewerAccentSectionWidth}"
                   ColumnDefinitions="*,*"
-                  Margin="-10,0,0,0"
                   VerticalAlignment="Center">
               <Border Grid.Column="0"
                       Grid.ColumnSpan="2"
@@ -64,10 +64,8 @@
             <Border Grid.Column="1"
                     HorizontalAlignment="Stretch"
                     VerticalAlignment="Stretch"
-                    Background="Transparent"
                     BoxShadow="0 0 10 2 #BF000000"
-                    CornerRadius="{TemplateBinding CornerRadius}"
-                    Margin="10">
+                    CornerRadius="{TemplateBinding CornerRadius}">
               <Panel>
                 <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
                         CornerRadius="{TemplateBinding CornerRadius}" />
@@ -82,8 +80,7 @@
           <Border CornerRadius="{TemplateBinding CornerRadius}"
                   IsVisible="{TemplateBinding IsAccentColorsVisible, Converter={x:Static BoolConverters.Not}}"
                   HorizontalAlignment="Stretch"
-                  VerticalAlignment="Stretch"
-                  Margin="0,10,0,10">
+                  VerticalAlignment="Stretch">
             <Panel>
               <Border Background="{StaticResource ColorControlCheckeredBackgroundBrush}"
                       CornerRadius="{TemplateBinding CornerRadius}" />

+ 21 - 4
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@@ -322,8 +322,8 @@
                 <primitives:ColorSlider x:Name="ColorSpectrumThirdComponentSlider"
                                         AutomationProperties.Name="Third Component"
                                         Grid.Column="0"
-                                        IsAlphaMaxForced="True"
-                                        IsSaturationValueMaxForced="False"
+                                        IsAlphaVisible="False"
+                                        IsPerceptive="True"
                                         Orientation="Vertical"
                                         ColorModel="Hsva"
                                         ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}"
@@ -699,15 +699,32 @@
             </TabItem>
           </TabControl>
           <!-- Previewer -->
-          <!-- Note that top/bottom margins have -5 to remove for drop shadow padding -->
+          <!-- Note that the drop shadow is allowed to extend past the control bounds -->
           <primitives:ColorPreviewer Grid.Row="1"
                                      HsvColor="{Binding HsvColor, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                      IsAccentColorsVisible="{TemplateBinding IsAccentColorsVisible}"
-                                     Margin="12,-5,12,7"
+                                     Margin="12,0,12,12"
                                      IsVisible="{TemplateBinding IsColorPreviewVisible}" />
         </Grid>
       </ControlTemplate>
     </Setter>
+
+    <!--
+    <Style Selector="^ /template/ primitives|ColorSlider#ColorSpectrumThirdComponentSlider[ColorComponent=Component1]">
+      <Setter Property="IsPerceptive" Value="True" />
+    </Style>
+
+    <Style Selector="^ /template/ primitives|ColorSlider#Component1Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component2Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    <Style Selector="^ /template/ primitives|ColorSlider#Component3Slider[ColorModel=Rgba]">
+      <Setter Property="IsPerceptive" Value="False" />
+    </Style>
+    -->
+
   </ControlTheme>
 
 </ResourceDictionary>

+ 29 - 0
tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

@@ -335,5 +335,34 @@ namespace Avalonia.Base.UnitTests.Media
                 Assert.True(dataPoint.Item2 == parsedColor);
             }
         }
+
+        [Fact]
+        public void Hsv_To_From_Hsl_Conversion()
+        {
+            // Note that conversion of values more representative of actual colors is not done due to rounding error
+            // It would be necessary to introduce a different equality comparison that accounts for rounding differences in values
+            // This is a result of the math in the conversion itself
+            // RGB doesn't have this problem because it uses whole numbers
+            var data = new Tuple<HsvColor, HslColor>[]
+            {
+                Tuple.Create(new HsvColor(1.0, 0.0, 0.0, 0.0), new HslColor(1.0, 0.0, 0.0, 0.0)),
+                Tuple.Create(new HsvColor(1.0, 359.0, 1.0, 1.0), new HslColor(1.0, 359.0, 1.0, 0.5)),
+
+                Tuple.Create(new HsvColor(1.0, 128.0, 0.0, 0.0), new HslColor(1.0, 128.0, 0.0, 0.0)),
+                Tuple.Create(new HsvColor(1.0, 128.0, 0.0, 1.0), new HslColor(1.0, 128.0, 0.0, 1.0)),
+                Tuple.Create(new HsvColor(1.0, 128.0, 1.0, 1.0), new HslColor(1.0, 128.0, 1.0, 0.5)),
+
+                Tuple.Create(new HsvColor(0.23, 0.5, 1.0, 1.0), new HslColor(0.23, 0.5, 1.0, 0.5)),
+            };
+
+            foreach (var dataPoint in data)
+            {
+                var convertedHsl = dataPoint.Item1.ToHsl();
+                var convertedHsv = dataPoint.Item2.ToHsv();
+
+                Assert.Equal(convertedHsv, dataPoint.Item1);
+                Assert.Equal(convertedHsl, dataPoint.Item2);
+            }
+        }
     }
 }