Browse Source

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

ColorPicker Updates
pull/11432/head
Max Katz 3 years ago
committed by GitHub
parent
commit
16bc3224f9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  2. 32
      src/Avalonia.Base/Media/Color.cs
  3. 78
      src/Avalonia.Base/Media/HslColor.cs
  4. 78
      src/Avalonia.Base/Media/HsvColor.cs
  5. 72
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  6. 88
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  7. 91
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  8. 6
      src/Avalonia.Controls.ColorPicker/ColorView/ColorView.Properties.cs
  9. 60
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  10. 2
      src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs
  11. 2
      src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs
  12. 25
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  13. 13
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  14. 25
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  15. 25
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  16. 13
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPreviewer.xaml
  17. 25
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  18. 29
      tests/Avalonia.Base.UnitTests/Media/ColorTests.cs

3
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>

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>

78
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>

78
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>

72
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);
}
}
}

88
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;

91
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
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);

60
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;
}
}
}

2
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>

2
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>

25
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>

13
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}" />

25
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>

25
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>

13
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}" />

25
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
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);
}
}
}
}

Loading…
Cancel
Save