From e1e6789f7824c6a21f4d8c7288cf2cffe672ee00 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:05:29 -0400 Subject: [PATCH 01/59] Add ColorPicker base enums --- .../ColorPicker/ColorPickerHsvChannel.cs | 33 +++++++++++++ .../ColorSpectrum/IncrementAmount.cs | 23 ++++++++++ .../ColorSpectrum/IncrementDirection.cs | 23 ++++++++++ .../ColorPicker/ColorSpectrumChannels.cs | 46 +++++++++++++++++++ .../ColorPicker/ColorSpectrumShape.cs | 26 +++++++++++ 5 files changed, 151 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs b/src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs new file mode 100644 index 0000000000..9e6e7cb89e --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorPickerHsvChannel.cs @@ -0,0 +1,33 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +namespace Avalonia.Controls +{ + /// + /// Defines a specific HSV color model channel. + /// + public enum ColorPickerHsvChannel + { + /// + /// The Hue channel. + /// + Hue, + + /// + /// The Saturation channel. + /// + Saturation, + + /// + /// The Value channel. + /// + Value, + + /// + /// The Alpha channel. + /// + Alpha + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs new file mode 100644 index 0000000000..fd749cd7dc --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementAmount.cs @@ -0,0 +1,23 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +namespace Avalonia.Controls.Primitives +{ + /// + /// Defines a relative amount that a color channel should be incremented. + /// + internal enum IncrementAmount + { + /// + /// A smaller change in value. + /// + Small, + + /// + /// A larger change in value. + /// + Large, + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs new file mode 100644 index 0000000000..8002a44fc9 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/IncrementDirection.cs @@ -0,0 +1,23 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +namespace Avalonia.Controls.Primitives +{ + /// + /// Defines the direction a color channel should be incremented. + /// + internal enum IncrementDirection + { + /// + /// Decreasing in value towards zero. + /// + Lower, + + /// + /// Increasing in value towards positive infinity. + /// + Higher, + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs new file mode 100644 index 0000000000..2b3f711634 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs @@ -0,0 +1,46 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// Defines the two HSV color channels displayed by a . + /// Order of the color channels is important. + /// + public enum ColorSpectrumChannels + { + /// + /// The Hue and Value channels. + /// + HueValue, + + /// + /// The Value and Hue channels. + /// + ValueHue, + + /// + /// The Hue and Saturation channels. + /// + HueSaturation, + + /// + /// The Saturation and Hue channels. + /// + SaturationHue, + + /// + /// The Saturation and Value channels. + /// + SaturationValue, + + /// + /// The Value and Saturation channels. + /// + ValueSaturation, + }; +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs new file mode 100644 index 0000000000..0319d4a6c8 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrumShape.cs @@ -0,0 +1,26 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls +{ + /// + /// Defines the shape of a . + /// + public enum ColorSpectrumShape + { + /// + /// The spectrum is in the shape of a rectangular or square box. + /// Note that more colors are visible to the user in Box shape. + /// + Box, + + /// + /// The spectrum is in the shape of an ellipse or circle. + /// + Ring, + }; +} From bf9b356a29655e74b3a8fd7ade8a094f34952054 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:06:27 -0400 Subject: [PATCH 02/59] Add HsvColor.FromRgb() overload that works with normalized double values --- src/Avalonia.Visuals/Media/HsvColor.cs | 42 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs index 8b2f10d088..a18f1032f2 100644 --- a/src/Avalonia.Visuals/Media/HsvColor.cs +++ b/src/Avalonia.Visuals/Media/HsvColor.cs @@ -173,7 +173,7 @@ namespace Avalonia.Media } /// - /// Converts the given HSV color to it's RGB color equivalent. + /// Converts the given HSV color to its RGB color equivalent. /// /// The color in the HSV color model. /// A new RGB equivalent to the given HSVA values. @@ -183,7 +183,7 @@ namespace Avalonia.Media } /// - /// Converts the given HSVA color channel values to it's RGB color equivalent. + /// Converts the given HSVA color channel values to its RGB color equivalent. /// /// The hue channel value in the HSV color model in the range from 0..360. /// The saturation channel value in the HSV color model in the range from 0..1. @@ -321,7 +321,7 @@ namespace Avalonia.Media } /// - /// Converts the given RGB color to it's HSV color equivalent. + /// Converts the given RGB color to its HSV color equivalent. /// /// The color in the RGB color model. /// A new equivalent to the given RGBA values. @@ -331,7 +331,7 @@ namespace Avalonia.Media } /// - /// Converts the given RGBA color channel values to it's HSV color equivalent. + /// Converts the given RGBA color channel values to its HSV color equivalent. /// /// The red channel value in the RGB color model. /// The green channel value in the RGB color model. @@ -343,18 +343,40 @@ namespace Avalonia.Media byte green, byte blue, byte alpha = 0xFF) + { + // Normalize RGBA channel values into the 0..1 range + return HsvColor.FromRgb( + (red / 255.0), + (green / 255.0), + (blue / 255.0), + (alpha / 255.0)); + } + + // TODO: Mark the below method Internal and make Internals visible to Avalonia.Controls... + + /// + /// Converts the given RGBA color channel values to its HSV color equivalent. + /// + /// + /// Warning: No bounds checks or clamping is done on the input channel values. + /// This method is for internal-use only and the caller must ensure bounds. + /// + /// The red channel value in the RGB color model within the range 0..1. + /// The green channel value in the RGB color model within the range 0..1. + /// The blue channel value in the RGB color model within the range 0..1. + /// The alpha channel value in the RGB color model within the range 0..1. + /// A new equivalent to the given RGBA values. + public static HsvColor FromRgb( + double r, + double g, + double b, + double a = 1.0) { // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT) // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp // This was used because it is the best documented and likely most optimized for performance // Alpha channel support was added - // Normalize RGBA channel values into the 0..1 range used by this algorithm - double r = red / 255.0; - double g = green / 255.0; - double b = blue / 255.0; - double a = alpha / 255.0; - double hue; double saturation; double value; From 653abb3b3187e2b6f8753ad3b97616d590aa483f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:07:23 -0400 Subject: [PATCH 03/59] Add higher performance, mutable RGB and HSV structs These are only for internal use with the ColorSpectrum --- .../ColorPicker/ColorSpectrum/Hsv.cs | 87 +++++++++++++++++ .../ColorPicker/ColorSpectrum/Rgb.cs | 94 +++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs new file mode 100644 index 0000000000..6d9f8440bf --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Hsv.cs @@ -0,0 +1,87 @@ +// Portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System.Numerics; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains and allows modification of Hue, Saturation and Value channel values. + /// + /// + /// The is a specialized struct optimized for permanence and memory: + /// + /// This is not a read-only struct like and allows editing the fields + /// Removes the alpha component unnecessary in core calculations + /// No channel value bounds checks or clamping is done. + /// + /// + internal struct Hsv + { + /// + /// The Hue channel value in the range from 0..359. + /// + public double H; + + /// + /// The Saturation channel value in the range from 0..1. + /// + public double S; + + /// + /// The Value channel value in the range from 0..1. + /// + public double V; + + /// + /// Initializes a new instance of the struct. + /// + /// The Hue channel value in the range from 0..360. + /// The Saturation channel value in the range from 0..1. + /// The Value channel value in the range from 0..1. + public Hsv(double h, double s, double v) + { + H = h; + S = s; + V = v; + } + + /// + /// Initializes a new instance of the struct. + /// + /// An existing to convert to . + public Hsv(HsvColor hsvColor) + { + H = hsvColor.H; + S = hsvColor.S; + V = hsvColor.V; + } + + /// + /// Converts this struct into a standard . + /// + /// The Alpha channel value in the range from 0..1. + /// A new representing this struct. + public HsvColor ToHsvColor(double alpha = 1.0) + { + // Clamping is done automatically in the constructor + return HsvColor.FromAhsv(alpha, H, S, V); + } + + /// + /// Returns the color model equivalent of this color. + /// + /// The equivalent color. + public Rgb ToRgb() + { + // Instantiating a Color is unfortunately necessary to use existing conversions + // Clamping is done internally in the conversion method + Color color = HsvColor.ToRgb(H, S, V); + + return new Rgb(color); + } + } +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs new file mode 100644 index 0000000000..c2358b7bee --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/Rgb.cs @@ -0,0 +1,94 @@ +// Portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains and allows modification of Red, Green and Blue channel values. + /// + /// + /// The is a specialized struct optimized for permanence and memory: + /// + /// This is not a read-only struct like and allows editing the fields + /// Removes the alpha component unnecessary in core calculations + /// Normalizes RGB channel values in the range of 0..1 to simplify calculations. + /// No channel value bounds checks or clamping is done. + /// + /// + internal struct Rgb + { + /// + /// The Red channel value in the range from 0..1. + /// + public double R; + + /// + /// The Green channel value in the range from 0..1. + /// + public double G; + + /// + /// The Blue channel value in the range from 0..1. + /// + public double B; + + /// + /// Initializes a new instance of the struct. + /// + /// The Red channel value in the range from 0..1. + /// The Green channel value in the range from 0..1. + /// The Blue channel value in the range from 0..1. + public Rgb(double r, double g, double b) + { + R = r; + G = g; + B = b; + } + + /// + /// Initializes a new instance of the struct. + /// + /// An existing to convert to . + public Rgb(Color color) + { + R = color.R / 255.0; + G = color.G / 255.0; + B = color.B / 255.0; + } + + /// + /// Converts this struct into a standard . + /// + /// The Alpha channel value in the range from 0..1. + /// A new representing this struct. + public Color ToColor(double alpha = 1.0) + { + return Color.FromArgb( + (byte)MathUtilities.Clamp(alpha * 255.0, 0x00, 0xFF), + (byte)MathUtilities.Clamp(R * 255.0, 0x00, 0xFF), + (byte)MathUtilities.Clamp(G * 255.0, 0x00, 0xFF), + (byte)MathUtilities.Clamp(B * 255.0, 0x00, 0xFF)); + } + + /// + /// Returns the color model equivalent of this color. + /// + /// The equivalent color. + public Hsv ToHsv() + { + // Instantiating an HsvColor is unfortunately necessary to use existing conversions + HsvColor hsvColor = HsvColor.FromRgb( + MathUtilities.Clamp(R, 0.0, 1.0), + MathUtilities.Clamp(G, 0.0, 1.0), + MathUtilities.Clamp(B, 0.0, 1.0)); + + return new Hsv(hsvColor); + } + } +} From 2e9d392ef91ca27bd94567dd30926a7ec9ce3321 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:07:45 -0400 Subject: [PATCH 04/59] Add new ColorChangedEventArgs --- .../ColorPicker/ColorChangedEventArgs.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs new file mode 100644 index 0000000000..93ba1a4db1 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs @@ -0,0 +1,59 @@ +// Portions of this source file are adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Holds the details of a ColorChanged event. + /// + /// + /// HSV color information is intentionally not provided. + /// Use to obtain it. + /// + public class ColorChangedEventArgs : EventArgs + { + private Color _OldColor; + private Color _NewColor; + + /// + /// Initializes a new instance of the class. + /// + public ColorChangedEventArgs() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The old/original color from before the change event. + /// The new/updated color that triggered the change event. + public ColorChangedEventArgs(Color oldColor, Color newColor) + { + _OldColor = oldColor; + _NewColor = newColor; + } + + /// + /// Gets the old/original color from before the change event. + /// + public Color OldColor + { + get => _OldColor; + internal set => _OldColor = value; + } + + /// + /// Gets the new/updated color that triggered the change event. + /// + public Color NewColor + { + get => _NewColor; + internal set => _NewColor = value; + } + } +} From 0f3409064864cffc65996d2ac8a29ce4a4d225f5 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:08:17 -0400 Subject: [PATCH 05/59] Add base ColorHelpers porting from WinUI --- .../ColorPicker/ColorSpectrum/ColorHelpers.cs | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs new file mode 100644 index 0000000000..3d380392aa --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorHelpers.cs @@ -0,0 +1,388 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace Avalonia.Controls.Primitives +{ + internal static class ColorHelpers + { + public const int CheckerSize = 4; + + public static bool ToDisplayNameExists + { + get => false; + } + + public static string ToDisplayName(Color color) + { + return string.Empty; + } + + public static Hsv IncrementColorChannel( + Hsv originalHsv, + ColorPickerHsvChannel channel, + IncrementDirection direction, + IncrementAmount amount, + bool shouldWrap, + double minBound, + double maxBound) + { + Hsv newHsv = originalHsv; + + if (amount == IncrementAmount.Small || !ToDisplayNameExists) + { + // In order to avoid working with small values that can incur rounding issues, + // we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1. + newHsv.S *= 100; + newHsv.V *= 100; + + // Note: *valueToIncrement replaced with ref local variable for C#, must be initialized + ref double valueToIncrement = ref newHsv.H; + double incrementAmount = 0.0; + + // If we're adding a small increment, then we'll just add or subtract 1. + // If we're adding a large increment, then we want to snap to the next + // or previous major value - for hue, this is every increment of 30; + // for saturation and value, this is every increment of 10. + switch (channel) + { + case ColorPickerHsvChannel.Hue: + valueToIncrement = ref newHsv.H; + incrementAmount = amount == IncrementAmount.Small ? 1 : 30; + break; + + case ColorPickerHsvChannel.Saturation: + valueToIncrement = ref newHsv.S; + incrementAmount = amount == IncrementAmount.Small ? 1 : 10; + break; + + case ColorPickerHsvChannel.Value: + valueToIncrement = ref newHsv.V; + incrementAmount = amount == IncrementAmount.Small ? 1 : 10; + break; + + default: + throw new InvalidOperationException("Invalid ColorPickerHsvChannel."); + } + + double previousValue = valueToIncrement; + + valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount); + + // If the value has reached outside the bounds, we were previous at the boundary, and we should wrap, + // then we'll place the selection on the other side of the spectrum. + // Otherwise, we'll place it on the boundary that was exceeded. + if (valueToIncrement < minBound) + { + valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound; + } + + if (valueToIncrement > maxBound) + { + valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound; + } + + // We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range. + newHsv.S /= 100; + newHsv.V /= 100; + } + else + { + // While working with named colors, we're going to need to be working in actual HSV units, + // so we'll divide the min bound and max bound by 100 in the case of saturation or value, + // since we'll have received units between 0-100 and we need them within 0-1. + if (channel == ColorPickerHsvChannel.Saturation || + channel == ColorPickerHsvChannel.Value) + { + minBound /= 100; + maxBound /= 100; + } + + newHsv = FindNextNamedColor(originalHsv, channel, direction, shouldWrap, minBound, maxBound); + } + + return newHsv; + } + + public static Hsv FindNextNamedColor( + Hsv originalHsv, + ColorPickerHsvChannel channel, + IncrementDirection direction, + bool shouldWrap, + double minBound, + double maxBound) + { + // There's no easy way to directly get the next named color, so what we'll do + // is just iterate in the direction that we want to find it until we find a color + // in that direction that has a color name different than our current color name. + // Once we find a new color name, then we'll iterate across that color name until + // we find its bounds on the other side, and then select the color that is exactly + // in the middle of that color's bounds. + Hsv newHsv = originalHsv; + + string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); + string newColorName = originalColorName; + + // Note: *newValue replaced with ref local variable for C#, must be initialized + double originalValue = 0.0; + ref double newValue = ref newHsv.H; + double incrementAmount = 0.0; + + switch (channel) + { + case ColorPickerHsvChannel.Hue: + originalValue = originalHsv.H; + newValue = ref newHsv.H; + incrementAmount = 1; + break; + + case ColorPickerHsvChannel.Saturation: + originalValue = originalHsv.S; + newValue = ref newHsv.S; + incrementAmount = 0.01; + break; + + case ColorPickerHsvChannel.Value: + originalValue = originalHsv.V; + newValue = ref newHsv.V; + incrementAmount = 0.01; + break; + + default: + throw new InvalidOperationException("Invalid ColorPickerHsvChannel."); + } + + bool shouldFindMidPoint = true; + + while (newColorName == originalColorName) + { + double previousValue = newValue; + newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; + + bool justWrapped = false; + + // If we've hit a boundary, then either we should wrap or we shouldn't. + // If we should, then we'll perform that wrapping if we were previously up against + // the boundary that we've now hit. Otherwise, we'll stop at that boundary. + if (newValue > maxBound) + { + if (shouldWrap) + { + newValue = minBound; + justWrapped = true; + } + else + { + newValue = maxBound; + shouldFindMidPoint = false; + newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + break; + } + } + else if (newValue < minBound) + { + if (shouldWrap) + { + newValue = maxBound; + justWrapped = true; + } + else + { + newValue = minBound; + shouldFindMidPoint = false; + newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + break; + } + } + + if (!justWrapped && + previousValue != originalValue && + Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue)) + { + // If we've wrapped all the way back to the start and have failed to find a new color name, + // then we'll just quit - there isn't a new color name that we're going to find. + shouldFindMidPoint = false; + break; + } + + newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + } + + if (shouldFindMidPoint) + { + Hsv startHsv = newHsv; + Hsv currentHsv = startHsv; + double startEndOffset = 0; + string currentColorName = newColorName; + + // Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized + ref double startValue = ref startHsv.H; + ref double currentValue = ref currentHsv.H; + double wrapIncrement = 0; + + switch (channel) + { + case ColorPickerHsvChannel.Hue: + startValue = ref startHsv.H; + currentValue = ref currentHsv.H; + wrapIncrement = 360.0; + break; + + case ColorPickerHsvChannel.Saturation: + startValue = ref startHsv.S; + currentValue = ref currentHsv.S; + wrapIncrement = 1.0; + break; + + case ColorPickerHsvChannel.Value: + startValue = ref startHsv.V; + currentValue = ref currentHsv.V; + wrapIncrement = 1.0; + break; + + default: + throw new InvalidOperationException("Invalid ColorPickerHsvChannel."); + } + + while (newColorName == currentColorName) + { + currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; + + // If we've hit a boundary, then either we should wrap or we shouldn't. + // If we should, then we'll perform that wrapping if we were previously up against + // the boundary that we've now hit. Otherwise, we'll stop at that boundary. + if (currentValue > maxBound) + { + if (shouldWrap) + { + currentValue = minBound; + startEndOffset = maxBound - minBound; + } + else + { + currentValue = maxBound; + break; + } + } + else if (currentValue < minBound) + { + if (shouldWrap) + { + currentValue = maxBound; + startEndOffset = minBound - maxBound; + } + else + { + currentValue = minBound; + break; + } + } + + currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); + } + + newValue = (startValue + currentValue + startEndOffset) / 2; + + // Dividing by 2 may have gotten us halfway through a single step, so we'll + // remove that half-step if it exists. + double leftoverValue = Math.Abs(newValue); + + while (leftoverValue > incrementAmount) + { + leftoverValue -= incrementAmount; + } + + newValue -= leftoverValue; + + while (newValue < minBound) + { + newValue += wrapIncrement; + } + + while (newValue > maxBound) + { + newValue -= wrapIncrement; + } + } + + return newHsv; + } + + public static double IncrementAlphaChannel( + double originalAlpha, + IncrementDirection direction, + IncrementAmount amount, + bool shouldWrap, + double minBound, + double maxBound) + { + // In order to avoid working with small values that can incur rounding issues, + // we'll multiple alpha by 100 to put it in the range of 0-100 instead of 0-1. + originalAlpha *= 100; + + const double smallIncrementAmount = 1; + const double largeIncrementAmount = 10; + + if (amount == IncrementAmount.Small) + { + originalAlpha += (direction == IncrementDirection.Lower ? -1 : 1) * smallIncrementAmount; + } + else + { + if (direction == IncrementDirection.Lower) + { + originalAlpha = Math.Ceiling((originalAlpha - largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; + } + else + { + originalAlpha = Math.Floor((originalAlpha + largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; + } + } + + // If the value has reached outside the bounds and we should wrap, then we'll place the selection + // on the other side of the spectrum. Otherwise, we'll place it on the boundary that was exceeded. + if (originalAlpha < minBound) + { + originalAlpha = shouldWrap ? maxBound : minBound; + } + + if (originalAlpha > maxBound) + { + originalAlpha = shouldWrap ? minBound : maxBound; + } + + // We multiplied alpha by 100 previously, so now we want to put it back in the 0-1 range. + return originalAlpha / 100; + } + + public static WriteableBitmap CreateBitmapFromPixelData( + int pixelWidth, + int pixelHeight, + List bgraPixelData) + { + Vector dpi = new Vector(96, 96); // Standard may need to change on some devices + + WriteableBitmap bitmap = new WriteableBitmap( + new PixelSize(pixelWidth, pixelHeight), + dpi, + PixelFormat.Bgra8888, + AlphaFormat.Premul); + + // Warning: This is highly questionable + using (var frameBuffer = bitmap.Lock()) + { + Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + } + + return bitmap; + } + } +} From fb63032d40bea28ef5703a05814c230d6025a093 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:08:42 -0400 Subject: [PATCH 06/59] Add ColorSpectrum porting from WinUI --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 205 +++ .../ColorSpectrum/ColorSpectrum.cs | 1615 +++++++++++++++++ 2 files changed, 1820 insertions(+) create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs create mode 100644 src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs new file mode 100644 index 0000000000..e95c63da33 --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -0,0 +1,205 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorSpectrum + { + /// + /// Gets or sets the currently selected color in the RGB color model. + /// + /// + /// For control authors use instead to avoid loss + /// of precision and color drifting. + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); + + /// + /// Gets or sets the two HSV color channels displayed by the spectrum. + /// + /// + /// Internally, the uses the HSV color model. + /// + public ColorSpectrumChannels Channels + { + get => GetValue(ChannelsProperty); + set => SetValue(ChannelsProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ChannelsProperty = + AvaloniaProperty.Register( + nameof(Channels), + ColorSpectrumChannels.HueSaturation); + + /// + /// Gets or sets the currently selected color in the HSV color model. + /// + /// + /// This should be used in all cases instead of the property. + /// Internally, the uses the HSV color model and using + /// this property will avoid loss of precision and color drifting. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register(nameof(HsvColor), new HsvColor(1, 0, 0, 1)); + + /// + /// Gets or sets the maximum value of the Hue channel in the range from 0..359. + /// This property must be greater than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MaxHue + { + get => GetValue(MaxHueProperty); + set => SetValue(MaxHueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register(nameof(MaxHue), 359); + + /// + /// Gets or sets the maximum value of the Saturation channel in the range from 0..100. + /// This property must be greater than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MaxSaturation + { + get => GetValue(MaxSaturationProperty); + set => SetValue(MaxSaturationProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register(nameof(MaxSaturation), 100); + + /// + /// Gets or sets the maximum value of the Value channel in the range from 0..100. + /// This property must be greater than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MaxValue + { + get => GetValue(MaxValueProperty); + set => SetValue(MaxValueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register(nameof(MaxValue), 100); + + /// + /// Gets or sets the minimum value of the Hue channel in the range from 0..359. + /// This property must be less than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MinHue + { + get => GetValue(MinHueProperty); + set => SetValue(MinHueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register(nameof(MinHue), 0); + + /// + /// Gets or sets the minimum value of the Saturation channel in the range from 0..100. + /// This property must be less than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MinSaturation + { + get => GetValue(MinSaturationProperty); + set => SetValue(MinSaturationProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register(nameof(MinSaturation), 0); + + /// + /// Gets or sets the minimum value of the Value channel in the range from 0..100. + /// This property must be less than . + /// + /// + /// Internally, the uses the HSV color model. + /// + public int MinValue + { + get => GetValue(MinValueProperty); + set => SetValue(MinValueProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register(nameof(MinValue), 0); + + /// + /// Gets or sets the displayed shape of the spectrum. + /// + public ColorSpectrumShape Shape + { + get => GetValue(ShapeProperty); + set => SetValue(ShapeProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShapeProperty = + AvaloniaProperty.Register( + nameof(Shape), + ColorSpectrumShape.Box); + } +} diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs new file mode 100644 index 0000000000..e4096bcfda --- /dev/null +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -0,0 +1,1615 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// A two dimensional spectrum for color selection. + /// + public partial class ColorSpectrum : TemplatedControl + { + /// + /// Event for when the selected color changes within the spectrum. + /// + public event EventHandler? ColorChanged; + + private bool _updatingColor = false; + private bool _updatingHsvColor = false; + private bool _isPointerOver = false; + private bool _isPointerPressed = false; + private bool _shouldShowLargeSelection = false; + private List _hsvValues = new List(); + + private IDisposable? _layoutRootDisposable; + + // XAML template parts + private Grid? _layoutRoot; + private Grid? _sizingGrid; + + private Rectangle? _spectrumRectangle; + private Ellipse? _spectrumEllipse; + private Rectangle? _spectrumOverlayRectangle; + private Ellipse? _spectrumOverlayEllipse; + + private Canvas? _inputTarget; + private Panel? _selectionEllipsePanel; + + private ToolTip? _colorNameToolTip; + + // Put the spectrum images in a bitmap, which is then given to an ImageBrush. + private WriteableBitmap? _hueRedBitmap; + private WriteableBitmap? _hueYellowBitmap; + private WriteableBitmap? _hueGreenBitmap; + private WriteableBitmap? _hueCyanBitmap; + private WriteableBitmap? _hueBlueBitmap; + private WriteableBitmap? _huePurpleBitmap; + + private WriteableBitmap? _saturationMinimumBitmap; + private WriteableBitmap? _saturationMaximumBitmap; + + private WriteableBitmap? _valueBitmap; + + // Fields used by UpdateEllipse() to ensure that it's using the data + // associated with the last call to CreateBitmapsAndColorMap(), + // in order to function properly while the asynchronous bitmap creation + // is in progress. + private ColorSpectrumShape _shapeFromLastBitmapCreation = ColorSpectrumShape.Box; + private ColorSpectrumChannels _componentsFromLastBitmapCreation = ColorSpectrumChannels.HueSaturation; + private double _imageWidthFromLastBitmapCreation = 0.0; + private double _imageHeightFromLastBitmapCreation = 0.0; + private int _minHueFromLastBitmapCreation = 0; + private int _maxHueFromLastBitmapCreation = 0; + private int _minSaturationFromLastBitmapCreation = 0; + private int _maxSaturationFromLastBitmapCreation = 0; + private int _minValueFromLastBitmapCreation = 0; + private int _maxValueFromLastBitmapCreation = 0; + + private Color _oldColor = Color.FromArgb(255, 255, 255, 255); + private HsvColor _oldHsvColor = HsvColor.FromAhsv(0.0f, 0.0f, 1.0f, 1.0f); + + /// + /// Initializes a new instance of the class. + /// + public ColorSpectrum() + { + _shapeFromLastBitmapCreation = Shape; + _componentsFromLastBitmapCreation = Channels; + _imageWidthFromLastBitmapCreation = 0; + _imageHeightFromLastBitmapCreation = 0; + _minHueFromLastBitmapCreation = MinHue; + _maxHueFromLastBitmapCreation = MaxHue; + _minSaturationFromLastBitmapCreation = MinSaturation; + _maxSaturationFromLastBitmapCreation = MaxSaturation; + _minValueFromLastBitmapCreation = MinValue; + _maxValueFromLastBitmapCreation = MaxValue; + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + UnregisterEvents(); + + _layoutRoot = e.NameScope.Find("LayoutRoot"); + _sizingGrid = e.NameScope.Find("SizingGrid"); + _spectrumRectangle = e.NameScope.Find("SpectrumRectangle"); + _spectrumEllipse = e.NameScope.Find("SpectrumEllipse"); + _spectrumOverlayRectangle = e.NameScope.Find("SpectrumOverlayRectangle"); + _spectrumOverlayEllipse = e.NameScope.Find("SpectrumOverlayEllipse"); + _inputTarget = e.NameScope.Find("InputTarget"); + _selectionEllipsePanel = e.NameScope.Find("SelectionEllipsePanel"); + _colorNameToolTip = e.NameScope.Find("ColorNameToolTip"); + + if (_layoutRoot != null) + { + _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => OnLayoutRootSizeChanged()); + } + + if (_inputTarget != null) + { + _inputTarget.PointerEnter += OnInputTargetPointerEnter; + _inputTarget.PointerLeave += OnInputTargetPointerLeave; + _inputTarget.PointerPressed += OnInputTargetPointerPressed; + _inputTarget.PointerMoved += OnInputTargetPointerMoved; + _inputTarget.PointerReleased += OnInputTargetPointerReleased; + } + + if (ColorHelpers.ToDisplayNameExists) + { + if (_colorNameToolTip != null) + { + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + } + } + + if (_selectionEllipsePanel != null) + { + // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 + //m_selectionEllipsePanel.RegisterPropertyChangedCallback(FrameworkElement.FlowDirectionProperty, OnSelectionEllipseFlowDirectionChanged); + } + + // If we haven't yet created our bitmaps, do so now. + if (_hsvValues.Count == 0) + { + CreateBitmapsAndColorMap(); + } + + UpdateEllipse(); + UpdateVisualState(useTransitions: false); + } + + /// + /// Explicitly unregisters all events connected in OnApplyTemplate(). + /// + private void UnregisterEvents() + { + _layoutRootDisposable?.Dispose(); + _layoutRootDisposable = null; + + if (_inputTarget != null) + { + _inputTarget.PointerEnter -= OnInputTargetPointerEnter; + _inputTarget.PointerLeave -= OnInputTargetPointerLeave; + _inputTarget.PointerPressed -= OnInputTargetPointerPressed; + _inputTarget.PointerMoved -= OnInputTargetPointerMoved; + _inputTarget.PointerReleased -= OnInputTargetPointerReleased; + } + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + var key = e.Key; + + if (key != Key.Left && + key != Key.Right && + key != Key.Up && + key != Key.Down) + { + base.OnKeyDown(e); + return; + } + + bool isControlDown = e.KeyModifiers.HasFlag(KeyModifiers.Control); + + ColorPickerHsvChannel incrementChannel = ColorPickerHsvChannel.Hue; + + if (key == Key.Left || + key == Key.Right) + { + switch (Channels) + { + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.HueValue: + incrementChannel = ColorPickerHsvChannel.Hue; + break; + + case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumChannels.SaturationValue: + incrementChannel = ColorPickerHsvChannel.Saturation; + break; + + case ColorSpectrumChannels.ValueHue: + case ColorSpectrumChannels.ValueSaturation: + incrementChannel = ColorPickerHsvChannel.Value; + break; + } + } + else if (key == Key.Up || + key == Key.Down) + { + switch (Channels) + { + case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumChannels.ValueHue: + incrementChannel = ColorPickerHsvChannel.Hue; + break; + + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.ValueSaturation: + incrementChannel = ColorPickerHsvChannel.Saturation; + break; + + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.SaturationValue: + incrementChannel = ColorPickerHsvChannel.Value; + break; + } + } + + double minBound = 0.0; + double maxBound = 0.0; + + switch (incrementChannel) + { + case ColorPickerHsvChannel.Hue: + minBound = MinHue; + maxBound = MaxHue; + break; + + case ColorPickerHsvChannel.Saturation: + minBound = MinSaturation; + maxBound = MaxSaturation; + break; + + case ColorPickerHsvChannel.Value: + minBound = MinValue; + maxBound = MaxValue; + break; + } + + // The order of saturation and value in the spectrum is reversed - the max value is at the bottom while the min value is at the top - + // so we want left and up to be lower for hue, but higher for saturation and value. + // This will ensure that the icon always moves in the direction of the key press. + IncrementDirection direction = + (incrementChannel == ColorPickerHsvChannel.Hue && (key == Key.Left || key == Key.Up)) || + (incrementChannel != ColorPickerHsvChannel.Hue && (key == Key.Right || key == Key.Down)) ? + IncrementDirection.Lower : + IncrementDirection.Higher; + + IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; + + HsvColor hsvColor = HsvColor; + UpdateColor(ColorHelpers.IncrementColorChannel( + new Hsv(hsvColor), + incrementChannel, + direction, + amount, + shouldWrap: true, + minBound, + maxBound)); + + e.Handled = true; + + return; + } + + /// + protected override void OnGotFocus(GotFocusEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + if (ColorHelpers.ToDisplayNameExists) + { + //colorNameToolTip.IsOpen = true; + } + } + + UpdateVisualState(useTransitions: true); + } + + /// + protected override void OnLostFocus(RoutedEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + if (ColorHelpers.ToDisplayNameExists) + { + //colorNameToolTip.IsOpen = false; + } + } + + UpdateVisualState(useTransitions: true); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == ColorProperty) + { + OnColorChanged(change); + } + else if (change.Property == HsvColorProperty) + { + OnHsvColorChanged(change); + } + else if ( + change.Property == MinHueProperty || + change.Property == MaxHueProperty) + { + OnMinMaxHueChanged(); + } + else if ( + change.Property == MinSaturationProperty || + change.Property == MaxSaturationProperty) + { + OnMinMaxSaturationChanged(); + } + else if ( + change.Property == MinValueProperty || + change.Property == MaxValueProperty) + { + OnMinMaxValueChanged(); + } + else if (change.Property == ShapeProperty) + { + OnShapeChanged(); + } + else if (change.Property == ChannelsProperty) + { + OnComponentsChanged(); + } + + base.OnPropertyChanged(change); + } + + private void OnColorChanged(AvaloniaPropertyChangedEventArgs change) + { + // If we're in the process of internally updating the color, + // then we don't want to respond to the Color property changing. + if (!_updatingColor) + { + Color color = Color; + + _updatingHsvColor = true; + Hsv newHsv = (new Rgb(color)).ToHsv(); + HsvColor = newHsv.ToHsvColor(color.A / 255.0); + _updatingHsvColor = false; + + UpdateEllipse(); + UpdateBitmapSources(); + } + + _oldColor = change.OldValue.GetValueOrDefault(); + } + + private void OnHsvColorChanged(AvaloniaPropertyChangedEventArgs change) + { + // If we're in the process of internally updating the HSV color, + // then we don't want to respond to the HsvColor property changing. + if (!_updatingHsvColor) + { + SetColor(); + } + + _oldHsvColor = change.OldValue.GetValueOrDefault(); + } + + private void SetColor() + { + HsvColor hsvColor = HsvColor; + + _updatingColor = true; + Rgb newRgb = (new Hsv(hsvColor)).ToRgb(); + + Color = newRgb.ToColor(hsvColor.A); + + _updatingColor = false; + + UpdateEllipse(); + UpdateBitmapSources(); + RaiseColorChanged(); + } + + public void RaiseColorChanged() + { + Color newColor = Color; + + if (_oldColor.A != newColor.A || + _oldColor.R != newColor.R || + _oldColor.G != newColor.G || + _oldColor.B != newColor.B) + { + var colorChangedEventArgs = new ColorChangedEventArgs(); + + colorChangedEventArgs.OldColor = _oldColor; + colorChangedEventArgs.NewColor = newColor; + + ColorChanged?.Invoke(this, colorChangedEventArgs); + + if (ColorHelpers.ToDisplayNameExists) + { + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); + } + } + } + } + + protected void OnMinMaxHueChanged() + { + int minHue = MinHue; + int maxHue = MaxHue; + + if (minHue < 0 || minHue > 359) + { + throw new ArgumentException("MinHue must be between 0 and 359."); + } + else if (maxHue < 0 || maxHue > 359) + { + throw new ArgumentException("MaxHue must be between 0 and 359."); + } + + ColorSpectrumChannels channels = Channels; + + // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.SaturationValue && + channels != ColorSpectrumChannels.ValueSaturation) + { + CreateBitmapsAndColorMap(); + } + } + + protected void OnMinMaxSaturationChanged() + { + int minSaturation = MinSaturation; + int maxSaturation = MaxSaturation; + + if (minSaturation < 0 || minSaturation > 100) + { + throw new ArgumentException("MinSaturation must be between 0 and 100."); + } + else if (maxSaturation < 0 || maxSaturation > 100) + { + throw new ArgumentException("MaxSaturation must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueValue && + channels != ColorSpectrumChannels.ValueHue) + { + CreateBitmapsAndColorMap(); + } + } + + private void OnMinMaxValueChanged() + { + int minValue = MinValue; + int maxValue = MaxValue; + + if (minValue < 0 || minValue > 100) + { + throw new ArgumentException("MinValue must be between 0 and 100."); + } + else if (maxValue < 0 || maxValue > 100) + { + throw new ArgumentException("MaxValue must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueSaturation && + channels != ColorSpectrumChannels.SaturationHue) + { + CreateBitmapsAndColorMap(); + } + } + + private void OnShapeChanged() + { + CreateBitmapsAndColorMap(); + } + + private void OnComponentsChanged() + { + CreateBitmapsAndColorMap(); + } + + private void UpdateVisualState(bool useTransitions) + { + //if (m_isPointerPressed) + //{ + // VisualStateManager.GoToState(this, m_shouldShowLargeSelection ? "PressedLarge" : "Pressed", useTransitions); + //} + //else if (m_isPointerOver) + //{ + // VisualStateManager.GoToState(this, "PointerOver", useTransitions); + //} + //else + //{ + // VisualStateManager.GoToState(this, "Normal", useTransitions); + //} + + //VisualStateManager.GoToState(this, m_shapeFromLastBitmapCreation == ColorSpectrumShape.Box ? "BoxSelected" : "RingSelected", useTransitions); + //VisualStateManager.GoToState(this, SelectionEllipseShouldBeLight() ? "SelectionEllipseLight" : "SelectionEllipseDark", useTransitions); + + //if (IsEnabled && FocusState != FocusState.Unfocused) + //{ + // if (FocusState == FocusState.Pointer) + // { + // VisualStateManager.GoToState(this, "PointerFocused", useTransitions); + // } + // else + // { + // VisualStateManager.GoToState(this, "Focused", useTransitions); + // } + //} + //else + //{ + // VisualStateManager.GoToState(this, "Unfocused", useTransitions); + //} + } + + private void UpdateColor(Hsv newHsv) + { + _updatingColor = true; + _updatingHsvColor = true; + + Rgb newRgb = newHsv.ToRgb(); + double alpha = HsvColor.A; + + Color = newRgb.ToColor(alpha); + HsvColor = newHsv.ToHsvColor(alpha); + + UpdateEllipse(); + UpdateVisualState(useTransitions: true); + + _updatingHsvColor = false; + _updatingColor = false; + + RaiseColorChanged(); + } + + private void UpdateColorFromPoint(PointerPoint point) + { + // If we haven't initialized our HSV value array yet, then we should just ignore any user input - + // we don't yet know what to do with it. + if (_hsvValues.Count == 0) + { + return; + } + + double xPosition = point.Position.X; + double yPosition = point.Position.Y; + double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; + double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2)); + + var shape = Shape; + + // If the point is outside the circle, we should bring it back into the circle. + if (distanceFromRadius > radius && shape == ColorSpectrumShape.Ring) + { + xPosition = (radius / distanceFromRadius) * (xPosition - radius) + radius; + yPosition = (radius / distanceFromRadius) * (yPosition - radius) + radius; + } + + // Now we need to find the index into the array of HSL values at each point in the spectrum m_image. + int x = (int)Math.Round(xPosition); + int y = (int)Math.Round(yPosition); + int width = (int)Math.Round(_imageWidthFromLastBitmapCreation); + + if (x < 0) + { + x = 0; + } + else if (x >= _imageWidthFromLastBitmapCreation) + { + x = (int)Math.Round(_imageWidthFromLastBitmapCreation) - 1; + } + + if (y < 0) + { + y = 0; + } + else if (y >= _imageHeightFromLastBitmapCreation) + { + y = (int)Math.Round(_imageHeightFromLastBitmapCreation) - 1; + } + + // The gradient image contains two dimensions of HSL information, but not the third. + // We should keep the third where it already was. + // Note: This can sometimes cause a crash -- possibly due to differences in c# rounding. Therefore, index is now clamped. + Hsv hsvAtPoint = _hsvValues[MathUtilities.Clamp((y * width + x), 0, _hsvValues.Count - 1)]; + + var channels = Channels; + var hsvColor = HsvColor; + + switch (channels) + { + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.ValueHue: + hsvAtPoint.S = hsvColor.S; + break; + + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.SaturationHue: + hsvAtPoint.V = hsvColor.V; + break; + + case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumChannels.SaturationValue: + hsvAtPoint.H = hsvColor.H; + break; + } + + UpdateColor(hsvAtPoint); + } + + private void UpdateEllipse() + { + var selectionEllipsePanel = _selectionEllipsePanel; + + if (selectionEllipsePanel == null) + { + return; + } + + // If we don't have an image size yet, we shouldn't be showing the ellipse. + if (_imageWidthFromLastBitmapCreation == 0 || + _imageHeightFromLastBitmapCreation == 0) + { + selectionEllipsePanel.IsVisible = false; + return; + } + else + { + selectionEllipsePanel.IsVisible = true; + } + + double xPosition; + double yPosition; + + Hsv hsvColor = new Hsv(HsvColor); + + hsvColor.H = MathUtilities.Clamp(hsvColor.H, (double)_minHueFromLastBitmapCreation, (double)_maxHueFromLastBitmapCreation); + hsvColor.S = MathUtilities.Clamp(hsvColor.S, _minSaturationFromLastBitmapCreation / 100.0, _maxSaturationFromLastBitmapCreation / 100.0); + hsvColor.V = MathUtilities.Clamp(hsvColor.V, _minValueFromLastBitmapCreation / 100.0, _maxValueFromLastBitmapCreation / 100.0); + + if (_shapeFromLastBitmapCreation == ColorSpectrumShape.Box) + { + double xPercent = 0; + double yPercent = 0; + + double hPercent = (hsvColor.H - _minHueFromLastBitmapCreation) / (_maxHueFromLastBitmapCreation - _minHueFromLastBitmapCreation); + double sPercent = (hsvColor.S * 100.0 - _minSaturationFromLastBitmapCreation) / (_maxSaturationFromLastBitmapCreation - _minSaturationFromLastBitmapCreation); + double vPercent = (hsvColor.V * 100.0 - _minValueFromLastBitmapCreation) / (_maxValueFromLastBitmapCreation - _minValueFromLastBitmapCreation); + + // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, + // we inverted the direction of that axis in order to put more hue on the outside of the ring, + // so we need to do similarly here when positioning the ellipse. + if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) + { + sPercent = 1 - sPercent; + } + else + { + vPercent = 1 - vPercent; + } + + switch (_componentsFromLastBitmapCreation) + { + case ColorSpectrumChannels.HueValue: + xPercent = hPercent; + yPercent = vPercent; + break; + + case ColorSpectrumChannels.HueSaturation: + xPercent = hPercent; + yPercent = sPercent; + break; + + case ColorSpectrumChannels.ValueHue: + xPercent = vPercent; + yPercent = hPercent; + break; + + case ColorSpectrumChannels.ValueSaturation: + xPercent = vPercent; + yPercent = sPercent; + break; + + case ColorSpectrumChannels.SaturationHue: + xPercent = sPercent; + yPercent = hPercent; + break; + + case ColorSpectrumChannels.SaturationValue: + xPercent = sPercent; + yPercent = vPercent; + break; + } + + xPosition = _imageWidthFromLastBitmapCreation * xPercent; + yPosition = _imageHeightFromLastBitmapCreation * yPercent; + } + else + { + double thetaValue = 0; + double rValue = 0; + + double hThetaValue = + _maxHueFromLastBitmapCreation != _minHueFromLastBitmapCreation ? + 360 * (hsvColor.H - _minHueFromLastBitmapCreation) / (_maxHueFromLastBitmapCreation - _minHueFromLastBitmapCreation) : + 0; + double sThetaValue = + _maxSaturationFromLastBitmapCreation != _minSaturationFromLastBitmapCreation ? + 360 * (hsvColor.S * 100.0 - _minSaturationFromLastBitmapCreation) / (_maxSaturationFromLastBitmapCreation - _minSaturationFromLastBitmapCreation) : + 0; + double vThetaValue = + _maxValueFromLastBitmapCreation != _minValueFromLastBitmapCreation ? + 360 * (hsvColor.V * 100.0 - _minValueFromLastBitmapCreation) / (_maxValueFromLastBitmapCreation - _minValueFromLastBitmapCreation) : + 0; + double hRValue = _maxHueFromLastBitmapCreation != _minHueFromLastBitmapCreation ? + (hsvColor.H - _minHueFromLastBitmapCreation) / (_maxHueFromLastBitmapCreation - _minHueFromLastBitmapCreation) - 1 : + 0; + double sRValue = _maxSaturationFromLastBitmapCreation != _minSaturationFromLastBitmapCreation ? + (hsvColor.S * 100.0 - _minSaturationFromLastBitmapCreation) / (_maxSaturationFromLastBitmapCreation - _minSaturationFromLastBitmapCreation) - 1 : + 0; + double vRValue = _maxValueFromLastBitmapCreation != _minValueFromLastBitmapCreation ? + (hsvColor.V * 100.0 - _minValueFromLastBitmapCreation) / (_maxValueFromLastBitmapCreation - _minValueFromLastBitmapCreation) - 1 : + 0; + + // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, + // we inverted the direction of that axis in order to put more hue on the outside of the ring, + // so we need to do similarly here when positioning the ellipse. + if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumChannels.ValueHue) + { + sThetaValue = 360 - sThetaValue; + sRValue = -sRValue - 1; + } + else + { + vThetaValue = 360 - vThetaValue; + vRValue = -vRValue - 1; + } + + switch (_componentsFromLastBitmapCreation) + { + case ColorSpectrumChannels.HueValue: + thetaValue = hThetaValue; + rValue = vRValue; + break; + + case ColorSpectrumChannels.HueSaturation: + thetaValue = hThetaValue; + rValue = sRValue; + break; + + case ColorSpectrumChannels.ValueHue: + thetaValue = vThetaValue; + rValue = hRValue; + break; + + case ColorSpectrumChannels.ValueSaturation: + thetaValue = vThetaValue; + rValue = sRValue; + break; + + case ColorSpectrumChannels.SaturationHue: + thetaValue = sThetaValue; + rValue = hRValue; + break; + + case ColorSpectrumChannels.SaturationValue: + thetaValue = sThetaValue; + rValue = vRValue; + break; + } + + double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; + + xPosition = (Math.Cos((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; + yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; + } + + Canvas.SetLeft(selectionEllipsePanel, xPosition - (selectionEllipsePanel.Width / 2)); + Canvas.SetTop(selectionEllipsePanel, yPosition - (selectionEllipsePanel.Height / 2)); + + // We only want to bother with the color name tool tip if we can provide color names. + if (ColorHelpers.ToDisplayNameExists) + { + if (_colorNameToolTip is ToolTip colorNameToolTip) + { + // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, + // so toggling IsEnabled induces it to do that without incurring any visual glitches. + colorNameToolTip.IsEnabled = false; + colorNameToolTip.IsEnabled = true; + } + } + + UpdateVisualState(useTransitions: true); + } + + private void OnLayoutRootSizeChanged() + { + CreateBitmapsAndColorMap(); + } + + private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) + { + _isPointerOver = true; + UpdateVisualState(useTransitions: true); + args.Handled = true; + } + + private void OnInputTargetPointerLeave(object? sender, PointerEventArgs args) + { + _isPointerOver = false; + UpdateVisualState(useTransitions: true); + args.Handled = true; + } + + private void OnInputTargetPointerPressed(object? sender, PointerPressedEventArgs args) + { + var inputTarget = _inputTarget; + + Focus(); + + _isPointerPressed = true; + _shouldShowLargeSelection = + // TODO: After Pen PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7412 + // args.Pointer.Type == PointerType.Pen || + args.Pointer.Type == PointerType.Touch; + + args.Pointer.Capture(inputTarget); + UpdateColorFromPoint(args.GetCurrentPoint(inputTarget)); + UpdateVisualState(useTransitions: true); + UpdateEllipse(); + + args.Handled = true; + } + + private void OnInputTargetPointerMoved(object? sender, PointerEventArgs args) + { + if (!_isPointerPressed) + { + return; + } + + UpdateColorFromPoint(args.GetCurrentPoint(_inputTarget)); + args.Handled = true; + } + + private void OnInputTargetPointerReleased(object? sender, PointerReleasedEventArgs args) + { + _isPointerPressed = false; + _shouldShowLargeSelection = false; + + args.Pointer.Capture(null); + UpdateVisualState(useTransitions: true); + UpdateEllipse(); + + args.Handled = true; + } + + // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 + //private void OnSelectionEllipseFlowDirectionChanged(DependencyObject o, DependencyProperty p) + //{ + // UpdateEllipse(); + //} + + private async void CreateBitmapsAndColorMap() + { + if (_layoutRoot == null || + _sizingGrid == null || + _inputTarget == null || + _spectrumRectangle == null || + _spectrumEllipse == null || + _spectrumOverlayRectangle == null || + _spectrumOverlayEllipse == null + /*|| SharedHelpers.IsInDesignMode*/) + { + return; + } + + var layoutRoot = _layoutRoot; + var sizingGrid = _sizingGrid; + var inputTarget = _inputTarget; + var spectrumRectangle = _spectrumRectangle; + var spectrumEllipse = _spectrumEllipse; + var spectrumOverlayRectangle = _spectrumOverlayRectangle; + var spectrumOverlayEllipse = _spectrumOverlayEllipse; + + // We want ColorSpectrum to always be a square, so we'll take the smaller of the dimensions + // and size the sizing grid to that. + double minDimension = Math.Min(layoutRoot.Bounds.Width, layoutRoot.Bounds.Height); + + if (minDimension == 0) + { + return; + } + + sizingGrid.Width = minDimension; + sizingGrid.Height = minDimension; + + if (sizingGrid.Clip is RectangleGeometry clip) + { + clip.Rect = new Rect(0, 0, minDimension, minDimension); + } + + inputTarget.Width = minDimension; + inputTarget.Height = minDimension; + spectrumRectangle.Width = minDimension; + spectrumRectangle.Height = minDimension; + spectrumEllipse.Width = minDimension; + spectrumEllipse.Height = minDimension; + spectrumOverlayRectangle.Width = minDimension; + spectrumOverlayRectangle.Height = minDimension; + spectrumOverlayEllipse.Width = minDimension; + spectrumOverlayEllipse.Height = minDimension; + + HsvColor hsvColor = HsvColor; + int minHue = MinHue; + int maxHue = MaxHue; + int minSaturation = MinSaturation; + int maxSaturation = MaxSaturation; + int minValue = MinValue; + int maxValue = MaxValue; + ColorSpectrumShape shape = Shape; + ColorSpectrumChannels channels = Channels; + + // If min >= max, then by convention, min is the only number that a property can have. + if (minHue >= maxHue) + { + maxHue = minHue; + } + + if (minSaturation >= maxSaturation) + { + maxSaturation = minSaturation; + } + + if (minValue >= maxValue) + { + maxValue = minValue; + } + + Hsv hsv = new Hsv(hsvColor); + + // The middle 4 are only needed and used in the case of hue as the third dimension. + // Saturation and luminosity need only a min and max. + List bgraMinPixelData = new List(); + List bgraMiddle1PixelData = new List(); + List bgraMiddle2PixelData = new List(); + List bgraMiddle3PixelData = new List(); + List bgraMiddle4PixelData = new List(); + List bgraMaxPixelData = new List(); + List newHsvValues = new List(); + + var pixelCount = (int)(Math.Round(minDimension) * Math.Round(minDimension)); + var pixelDataSize = pixelCount * 4; + bgraMinPixelData.Capacity = pixelDataSize; + + // We'll only save pixel data for the middle bitmaps if our third dimension is hue. + if (channels == ColorSpectrumChannels.ValueSaturation || + channels == ColorSpectrumChannels.SaturationValue) + { + bgraMiddle1PixelData.Capacity = pixelDataSize; + bgraMiddle2PixelData.Capacity = pixelDataSize; + bgraMiddle3PixelData.Capacity = pixelDataSize; + bgraMiddle4PixelData.Capacity = pixelDataSize; + } + + bgraMaxPixelData.Capacity = pixelDataSize; + newHsvValues.Capacity = pixelCount; + + int minDimensionInt = (int)Math.Round(minDimension); + + await Task.Run(() => + { + // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes, + // the ColorSpectrum will visually change to accommodate that value. For example, if the ColorSpectrum handles hue and luminosity, + // and the saturation externally goes from 1.0 to 0.5, then the ColorSpectrum will visually change to look more washed out + // to represent that third dimension's new value. + // Internally, however, we don't want to regenerate the ColorSpectrum bitmap every single time this happens, since that's very expensive. + // In order to make it so that we don't have to, we implement an optimization where, rather than having only one bitmap, + // we instead have multiple that we blend together using opacity to create the effect that we want. + // In the case where the third dimension is saturation or luminosity, we only need two: one bitmap at the minimum value + // of the third dimension, and one bitmap at the maximum. Then we set the second's opacity at whatever the value of + // the third dimension is - e.g., a saturation of 0.5 implies an opacity of 50%. + // In the case where the third dimension is hue, we need six: one bitmap corresponding to red, yellow, green, cyan, blue, and purple. + // We'll then blend between whichever colors our hue exists between - e.g., an orange color would use red and yellow with an opacity of 50%. + // This optimization does incur slightly more startup time initially since we have to generate multiple bitmaps at once instead of only one, + // but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap. + if (shape == ColorSpectrumShape.Box) + { + for (int x = minDimensionInt - 1; x >= 0; --x) + { + for (int y = minDimensionInt - 1; y >= 0; --y) + { + FillPixelForBox( + x, y, hsv, minDimensionInt, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, + newHsvValues); + } + } + } + else + { + for (int y = 0; y < minDimensionInt; ++y) + { + for (int x = 0; x < minDimensionInt; ++x) + { + FillPixelForRing( + x, y, minDimensionInt / 2.0, hsv, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, + newHsvValues); + } + } + } + }); + + Dispatcher.UIThread.Post(() => + { + int pixelWidth = (int)Math.Round(minDimension); + int pixelHeight = (int)Math.Round(minDimension); + + ColorSpectrumChannels channels2 = Channels; + + WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData); + WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData); + + switch (channels2) + { + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.ValueHue: + _saturationMinimumBitmap = minBitmap; + _saturationMaximumBitmap = maxBitmap; + break; + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.SaturationHue: + _valueBitmap = maxBitmap; + break; + case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumChannels.SaturationValue: + _hueRedBitmap = minBitmap; + _hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData); + _hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData); + _hueCyanBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle3PixelData); + _hueBlueBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle4PixelData); + _huePurpleBitmap = maxBitmap; + break; + } + + _shapeFromLastBitmapCreation = Shape; + _componentsFromLastBitmapCreation = Channels; + _imageWidthFromLastBitmapCreation = minDimension; + _imageHeightFromLastBitmapCreation = minDimension; + _minHueFromLastBitmapCreation = MinHue; + _maxHueFromLastBitmapCreation = MaxHue; + _minSaturationFromLastBitmapCreation = MinSaturation; + _maxSaturationFromLastBitmapCreation = MaxSaturation; + _minValueFromLastBitmapCreation = MinValue; + _maxValueFromLastBitmapCreation = MaxValue; + + _hsvValues = newHsvValues; + + UpdateBitmapSources(); + UpdateEllipse(); + }); + } + + private void FillPixelForBox( + double x, + double y, + Hsv baseHsv, + double minDimension, + ColorSpectrumChannels channels, + double minHue, + double maxHue, + double minSaturation, + double maxSaturation, + double minValue, + double maxValue, + List bgraMinPixelData, + List bgraMiddle1PixelData, + List bgraMiddle2PixelData, + List bgraMiddle3PixelData, + List bgraMiddle4PixelData, + List bgraMaxPixelData, + List newHsvValues) + { + double hMin = minHue; + double hMax = maxHue; + double sMin = minSaturation / 100.0; + double sMax = maxSaturation / 100.0; + double vMin = minValue / 100.0; + double vMax = maxValue / 100.0; + + Hsv hsvMin = baseHsv; + Hsv hsvMiddle1 = baseHsv; + Hsv hsvMiddle2 = baseHsv; + Hsv hsvMiddle3 = baseHsv; + Hsv hsvMiddle4 = baseHsv; + Hsv hsvMax = baseHsv; + + double xPercent = (minDimension - 1 - x) / (minDimension - 1); + double yPercent = (minDimension - 1 - y) / (minDimension - 1); + + switch (channels) + { + case ColorSpectrumChannels.HueValue: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.HueSaturation: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.ValueHue: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.ValueSaturation: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + + case ColorSpectrumChannels.SaturationHue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.SaturationValue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + } + + // If saturation is an axis in the spectrum with hue, or value is an axis, then we want + // that axis to go from maximum at the top to minimum at the bottom, + // or maximum at the outside to minimum at the inside in the case of the ring configuration, + // so we'll invert the number before assigning the HSL value to the array. + // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue + // in the case of the ring configuration. + if (channels == ColorSpectrumChannels.HueSaturation || + channels == ColorSpectrumChannels.SaturationHue) + { + hsvMin.S = sMax - hsvMin.S + sMin; + hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; + hsvMiddle2.S = sMax - hsvMiddle2.S + sMin; + hsvMiddle3.S = sMax - hsvMiddle3.S + sMin; + hsvMiddle4.S = sMax - hsvMiddle4.S + sMin; + hsvMax.S = sMax - hsvMax.S + sMin; + } + else + { + hsvMin.V = vMax - hsvMin.V + vMin; + hsvMiddle1.V = vMax - hsvMiddle1.V + vMin; + hsvMiddle2.V = vMax - hsvMiddle2.V + vMin; + hsvMiddle3.V = vMax - hsvMiddle3.V + vMin; + hsvMiddle4.V = vMax - hsvMiddle4.V + vMin; + hsvMax.V = vMax - hsvMax.V + vMin; + } + + newHsvValues.Add(hsvMin); + + Rgb rgbMin = hsvMin.ToRgb(); + bgraMinPixelData.Add((byte)Math.Round(rgbMin.B * 255.0)); // b + bgraMinPixelData.Add((byte)Math.Round(rgbMin.G * 255.0)); // g + bgraMinPixelData.Add((byte)Math.Round(rgbMin.R * 255.0)); // r + bgraMinPixelData.Add(255); // a - ignored + + // We'll only save pixel data for the middle bitmaps if our third dimension is hue. + if (channels == ColorSpectrumChannels.ValueSaturation || + channels == ColorSpectrumChannels.SaturationValue) + { + Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255.0)); // b + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.G * 255.0)); // g + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.R * 255.0)); // r + bgraMiddle1PixelData.Add(255); // a - ignored + + Rgb rgbMiddle2 = hsvMiddle2.ToRgb(); + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.B * 255.0)); // b + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.G * 255.0)); // g + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.R * 255.0)); // r + bgraMiddle2PixelData.Add(255); // a - ignored + + Rgb rgbMiddle3 = hsvMiddle3.ToRgb(); + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.B * 255.0)); // b + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.G * 255.0)); // g + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.R * 255.0)); // r + bgraMiddle3PixelData.Add(255); // a - ignored + + Rgb rgbMiddle4 = hsvMiddle4.ToRgb(); + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.B * 255.0)); // b + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.G * 255.0)); // g + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.R * 255.0)); // r + bgraMiddle4PixelData.Add(255); // a - ignored + } + + Rgb rgbMax = hsvMax.ToRgb(); + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.B * 255.0)); // b + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.G * 255.0)); // g + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.R * 255.0)); // r + bgraMaxPixelData.Add(255); // a - ignored + } + + private void FillPixelForRing( + double x, + double y, + double radius, + Hsv baseHsv, + ColorSpectrumChannels channels, + double minHue, + double maxHue, + double minSaturation, + double maxSaturation, + double minValue, + double maxValue, + List bgraMinPixelData, + List bgraMiddle1PixelData, + List bgraMiddle2PixelData, + List bgraMiddle3PixelData, + List bgraMiddle4PixelData, + List bgraMaxPixelData, + List newHsvValues) + { + double hMin = minHue; + double hMax = maxHue; + double sMin = minSaturation / 100.0; + double sMax = maxSaturation / 100.0; + double vMin = minValue / 100.0; + double vMax = maxValue / 100.0; + + double distanceFromRadius = Math.Sqrt(Math.Pow(x - radius, 2) + Math.Pow(y - radius, 2)); + + double xToUse = x; + double yToUse = y; + + // If we're outside the ring, then we want the pixel to appear as blank. + // However, to avoid issues with rounding errors, we'll act as though this point + // is on the edge of the ring for the purposes of returning an HSL value. + // That way, hit testing on the edges will always return the correct value. + if (distanceFromRadius > radius) + { + xToUse = (radius / distanceFromRadius) * (x - radius) + radius; + yToUse = (radius / distanceFromRadius) * (y - radius) + radius; + distanceFromRadius = radius; + } + + Hsv hsvMin = baseHsv; + Hsv hsvMiddle1 = baseHsv; + Hsv hsvMiddle2 = baseHsv; + Hsv hsvMiddle3 = baseHsv; + Hsv hsvMiddle4 = baseHsv; + Hsv hsvMax = baseHsv; + + double r = 1 - distanceFromRadius / radius; + + double theta = Math.Atan2((radius - yToUse), (radius - xToUse)) * 180.0 / Math.PI; + theta += 180.0; + theta = Math.Floor(theta); + + while (theta > 360) + { + theta -= 360; + } + + double thetaPercent = theta / 360; + + switch (channels) + { + case ColorSpectrumChannels.HueValue: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.HueSaturation: + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.ValueHue: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); + hsvMin.S = 0; + hsvMax.S = 1; + break; + + case ColorSpectrumChannels.ValueSaturation: + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + + case ColorSpectrumChannels.SaturationHue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); + hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); + hsvMin.V = 0; + hsvMax.V = 1; + break; + + case ColorSpectrumChannels.SaturationValue: + hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); + hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); + hsvMin.H = 0; + hsvMiddle1.H = 60; + hsvMiddle2.H = 120; + hsvMiddle3.H = 180; + hsvMiddle4.H = 240; + hsvMax.H = 300; + break; + } + + // If saturation is an axis in the spectrum with hue, or value is an axis, then we want + // that axis to go from maximum at the top to minimum at the bottom, + // or maximum at the outside to minimum at the inside in the case of the ring configuration, + // so we'll invert the number before assigning the HSL value to the array. + // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue + // in the case of the ring configuration. + if (channels == ColorSpectrumChannels.HueSaturation || + channels == ColorSpectrumChannels.SaturationHue) + { + hsvMin.S = sMax - hsvMin.S + sMin; + hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; + hsvMiddle2.S = sMax - hsvMiddle2.S + sMin; + hsvMiddle3.S = sMax - hsvMiddle3.S + sMin; + hsvMiddle4.S = sMax - hsvMiddle4.S + sMin; + hsvMax.S = sMax - hsvMax.S + sMin; + } + else + { + hsvMin.V = vMax - hsvMin.V + vMin; + hsvMiddle1.V = vMax - hsvMiddle1.V + vMin; + hsvMiddle2.V = vMax - hsvMiddle2.V + vMin; + hsvMiddle3.V = vMax - hsvMiddle3.V + vMin; + hsvMiddle4.V = vMax - hsvMiddle4.V + vMin; + hsvMax.V = vMax - hsvMax.V + vMin; + } + + newHsvValues.Add(hsvMin); + + Rgb rgbMin = hsvMin.ToRgb(); + bgraMinPixelData.Add((byte)Math.Round(rgbMin.B * 255)); // b + bgraMinPixelData.Add((byte)Math.Round(rgbMin.G * 255)); // g + bgraMinPixelData.Add((byte)Math.Round(rgbMin.R * 255)); // r + bgraMinPixelData.Add(255); // a + + // We'll only save pixel data for the middle bitmaps if our third dimension is hue. + if (channels == ColorSpectrumChannels.ValueSaturation || + channels == ColorSpectrumChannels.SaturationValue) + { + Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255)); // b + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.G * 255)); // g + bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.R * 255)); // r + bgraMiddle1PixelData.Add(255); // a + + Rgb rgbMiddle2 = hsvMiddle2.ToRgb(); + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.B * 255)); // b + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.G * 255)); // g + bgraMiddle2PixelData.Add((byte)Math.Round(rgbMiddle2.R * 255)); // r + bgraMiddle2PixelData.Add(255); // a + + Rgb rgbMiddle3 = hsvMiddle3.ToRgb(); + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.B * 255)); // b + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.G * 255)); // g + bgraMiddle3PixelData.Add((byte)Math.Round(rgbMiddle3.R * 255)); // r + bgraMiddle3PixelData.Add(255); // a + + Rgb rgbMiddle4 = hsvMiddle4.ToRgb(); + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.B * 255)); // b + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.G * 255)); // g + bgraMiddle4PixelData.Add((byte)Math.Round(rgbMiddle4.R * 255)); // r + bgraMiddle4PixelData.Add(255); // a + } + + Rgb rgbMax = hsvMax.ToRgb(); + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.B * 255)); // b + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.G * 255)); // g + bgraMaxPixelData.Add((byte)Math.Round(rgbMax.R * 255)); // r + bgraMaxPixelData.Add(255); // a + } + + private void UpdateBitmapSources() + { + var spectrumOverlayRectangle = _spectrumOverlayRectangle; + var spectrumOverlayEllipse = _spectrumOverlayEllipse; + + var spectrumRectangle = _spectrumRectangle; + var spectrumEllipse = _spectrumEllipse; + + if (spectrumOverlayRectangle == null || + spectrumOverlayEllipse == null || + spectrumRectangle == null || + spectrumEllipse == null) + { + return; + } + + HsvColor hsvColor = HsvColor; + ColorSpectrumChannels channels = Channels; + + // We'll set the base image and the overlay image based on which component is our third dimension. + // If it's saturation or luminosity, then the base image is that dimension at its minimum value, + // while the overlay image is that dimension at its maximum value. + // If it's hue, then we'll figure out where in the color wheel we are, and then use the two + // colors on either side of our position as our base image and overlay image. + // For example, if our hue is orange, then the base image would be red and the overlay image yellow. + switch (channels) + { + case ColorSpectrumChannels.HueValue: + case ColorSpectrumChannels.ValueHue: + { + if (_saturationMinimumBitmap == null || + _saturationMaximumBitmap == null) + { + return; + } + + ImageBrush spectrumBrush; + ImageBrush spectrumOverlayBrush; + + spectrumBrush = new ImageBrush(_saturationMinimumBitmap); + spectrumOverlayBrush = new ImageBrush(_saturationMaximumBitmap); + spectrumOverlayRectangle.Opacity = hsvColor.S; + spectrumOverlayEllipse.Opacity = hsvColor.S; + spectrumRectangle.Fill = spectrumBrush; + spectrumEllipse.Fill = spectrumBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + } + break; + + case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumChannels.SaturationHue: + { + if (_valueBitmap == null) + { + return; + } + + ImageBrush spectrumBrush; + ImageBrush spectrumOverlayBrush; + + spectrumBrush = new ImageBrush(_valueBitmap); + spectrumOverlayBrush = new ImageBrush(_valueBitmap); + spectrumOverlayRectangle.Opacity = 1.0; + spectrumOverlayEllipse.Opacity = 1.0; + spectrumRectangle.Fill = spectrumBrush; + spectrumEllipse.Fill = spectrumBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + } + break; + + case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumChannels.SaturationValue: + { + if (_hueRedBitmap == null || + _hueYellowBitmap == null || + _hueGreenBitmap == null || + _hueCyanBitmap == null || + _hueBlueBitmap == null || + _huePurpleBitmap == null) + { + return; + } + + ImageBrush spectrumBrush; + ImageBrush spectrumOverlayBrush; + + double sextant = hsvColor.H / 60.0; + + if (sextant < 1) + { + spectrumBrush = new ImageBrush(_hueRedBitmap); + spectrumOverlayBrush = new ImageBrush(_hueYellowBitmap); + } + else if (sextant >= 1 && sextant < 2) + { + spectrumBrush = new ImageBrush(_hueYellowBitmap); + spectrumOverlayBrush = new ImageBrush(_hueGreenBitmap); + } + else if (sextant >= 2 && sextant < 3) + { + spectrumBrush = new ImageBrush(_hueGreenBitmap); + spectrumOverlayBrush = new ImageBrush(_hueCyanBitmap); + } + else if (sextant >= 3 && sextant < 4) + { + spectrumBrush = new ImageBrush(_hueCyanBitmap); + spectrumOverlayBrush = new ImageBrush(_hueBlueBitmap); + } + else if (sextant >= 4 && sextant < 5) + { + spectrumBrush = new ImageBrush(_hueBlueBitmap); + spectrumOverlayBrush = new ImageBrush(_huePurpleBitmap); + } + else + { + spectrumBrush = new ImageBrush(_huePurpleBitmap); + spectrumOverlayBrush = new ImageBrush(_hueRedBitmap); + } + + spectrumOverlayRectangle.Opacity = sextant - (int)sextant; + spectrumOverlayEllipse.Opacity = sextant - (int)sextant; + spectrumRectangle.Fill = spectrumBrush; + spectrumEllipse.Fill = spectrumBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + spectrumOverlayRectangle.Fill = spectrumOverlayBrush; + } + break; + } + } + + private bool SelectionEllipseShouldBeLight() + { + // The selection ellipse should be light if and only if the chosen color + // contrasts more with black than it does with white. + // To find how much something contrasts with white, we use the equation + // for relative luminance, which is given by + // + // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg + // + // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise } + // + // If L is closer to 1, then the color is closer to white; if it is closer to 0, + // then the color is closer to black. This is based on the fact that the human + // eye perceives green to be much brighter than red, which in turn is perceived to be + // brighter than blue. + // + // If the third dimension is value, then we won't be updating the spectrum's displayed colors, + // so in that case we should use a value of 1 when considering the backdrop + // for the selection ellipse. + Color displayedColor; + + if (Channels == ColorSpectrumChannels.HueSaturation || + Channels == ColorSpectrumChannels.SaturationHue) + { + HsvColor hsvColor = HsvColor; + Rgb color = (new Hsv(hsvColor.H, hsvColor.S, 1.0)).ToRgb(); + displayedColor = color.ToColor(hsvColor.A); + } + else + { + displayedColor = Color; + } + + double rg = displayedColor.R <= 10 ? displayedColor.R / 3294.0 : Math.Pow(displayedColor.R / 269.0 + 0.0513, 2.4); + double gg = displayedColor.G <= 10 ? displayedColor.G / 3294.0 : Math.Pow(displayedColor.G / 269.0 + 0.0513, 2.4); + double bg = displayedColor.B <= 10 ? displayedColor.B / 3294.0 : Math.Pow(displayedColor.B / 269.0 + 0.0513, 2.4); + + return ((0.2126 * rg + 0.7152 * gg + 0.0722 * bg) <= 0.5); + } + } +} From d12ee97e78f9ec1f1a15d0d61906d70317d62f94 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 20 Mar 2022 23:09:03 -0400 Subject: [PATCH 07/59] Fix missing base.OnApplyTemplate() in SplitButton --- src/Avalonia.Controls/SplitButton/SplitButton.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index f1d07b2679..c21b70645b 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -227,6 +227,8 @@ namespace Avalonia.Controls /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + UnregisterEvents(); UnregisterFlyoutEvents(Flyout); From 2edd7e4157ae3929fb3341f766785664395f4044 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 00:49:13 -0400 Subject: [PATCH 08/59] Add initial ColorSpectrum Fluent style/template --- .../ColorSpectrum/ColorSpectrum.cs | 41 ++-- .../Controls/ColorSpectrum.xaml | 183 ++++++++++++++++++ .../Controls/FluentControls.xaml | 5 +- 3 files changed, 208 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index e4096bcfda..47b113b0de 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -104,15 +104,15 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); - _layoutRoot = e.NameScope.Find("LayoutRoot"); - _sizingGrid = e.NameScope.Find("SizingGrid"); - _spectrumRectangle = e.NameScope.Find("SpectrumRectangle"); - _spectrumEllipse = e.NameScope.Find("SpectrumEllipse"); - _spectrumOverlayRectangle = e.NameScope.Find("SpectrumOverlayRectangle"); - _spectrumOverlayEllipse = e.NameScope.Find("SpectrumOverlayEllipse"); - _inputTarget = e.NameScope.Find("InputTarget"); - _selectionEllipsePanel = e.NameScope.Find("SelectionEllipsePanel"); - _colorNameToolTip = e.NameScope.Find("ColorNameToolTip"); + _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); + _sizingGrid = e.NameScope.Find("PART_SizingGrid"); + _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); + _spectrumEllipse = e.NameScope.Find("PART_SpectrumEllipse"); + _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); + _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); + _inputTarget = e.NameScope.Find("PART_InputTarget"); + _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); + _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); if (_layoutRoot != null) { @@ -149,7 +149,7 @@ namespace Avalonia.Controls.Primitives } UpdateEllipse(); - UpdateVisualState(useTransitions: false); + UpdatePseudoClasses(); } /// @@ -290,7 +290,7 @@ namespace Avalonia.Controls.Primitives } } - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); } /// @@ -305,7 +305,7 @@ namespace Avalonia.Controls.Primitives } } - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); } /// @@ -508,7 +508,10 @@ namespace Avalonia.Controls.Primitives CreateBitmapsAndColorMap(); } - private void UpdateVisualState(bool useTransitions) + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + private void UpdatePseudoClasses() { //if (m_isPointerPressed) //{ @@ -555,7 +558,7 @@ namespace Avalonia.Controls.Primitives HsvColor = newHsv.ToHsvColor(alpha); UpdateEllipse(); - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); _updatingHsvColor = false; _updatingColor = false; @@ -822,7 +825,7 @@ namespace Avalonia.Controls.Primitives } } - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); } private void OnLayoutRootSizeChanged() @@ -833,14 +836,14 @@ namespace Avalonia.Controls.Primitives private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) { _isPointerOver = true; - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); args.Handled = true; } private void OnInputTargetPointerLeave(object? sender, PointerEventArgs args) { _isPointerOver = false; - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); args.Handled = true; } @@ -858,7 +861,7 @@ namespace Avalonia.Controls.Primitives args.Pointer.Capture(inputTarget); UpdateColorFromPoint(args.GetCurrentPoint(inputTarget)); - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); UpdateEllipse(); args.Handled = true; @@ -881,7 +884,7 @@ namespace Avalonia.Controls.Primitives _shouldShowLargeSelection = false; args.Pointer.Capture(null); - UpdateVisualState(useTransitions: true); + UpdatePseudoClasses(); UpdateEllipse(); args.Handled = true; diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml new file mode 100644 index 0000000000..c4ca09d439 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 16a6cc9c14..921826bab1 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -9,6 +9,7 @@ + @@ -59,8 +60,8 @@ - - + + From edbd3de9a550673b6c20cbccc57a9cbfbb9225be Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 00:52:35 -0400 Subject: [PATCH 09/59] Add initial ColorPickerPage to ControlCatalog --- samples/ControlCatalog/MainView.xaml | 3 +++ .../ControlCatalog/Pages/ColorPickerPage.xaml | 23 +++++++++++++++++++ .../Pages/ColorPickerPage.xaml.cs | 19 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 samples/ControlCatalog/Pages/ColorPickerPage.xaml create mode 100644 samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index facce2aa82..b6f118a721 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -43,6 +43,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml new file mode 100644 index 0000000000..31faf5ff09 --- /dev/null +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs new file mode 100644 index 0000000000..6e017e381f --- /dev/null +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public partial class ColorPickerPage : UserControl + { + public ColorPickerPage() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} From 5ac00ef54d981a5e5dc23aca5ca0153c29001ee7 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 10:46:15 -0400 Subject: [PATCH 10/59] Add new EnumValueEqualsConverter --- .../Converters/EnumValueEqualsConverter.cs | 54 +++++++++++++++++++ .../Converters/MarginMultiplierConverter.cs | 1 - 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs diff --git a/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs new file mode 100644 index 0000000000..1a33a82ca4 --- /dev/null +++ b/src/Avalonia.Controls/Converters/EnumValueEqualsConverter.cs @@ -0,0 +1,54 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converter that checks if an enum value is equal to the given parameter enum value. + /// + public class EnumValueEqualsConverter : IValueConverter + { + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + // Note: Unlike string comparisons, null/empty is not supported + // Both 'value' and 'parameter' must exist and if both are missing they are not considered equal + if (value != null && + parameter != null) + { + Type type = value.GetType(); + + if (type.IsEnum) + { + var valueStr = value?.ToString(); + var paramStr = parameter?.ToString(); + + if (string.Equals(valueStr, paramStr, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + /* + // TODO: When .net Standard 2.0 is no longer supported the code can be changed to below + // This is a little more type safe + if (type.IsEnum && + Enum.TryParse(type, value?.ToString(), true, out object? valueEnum) && + Enum.TryParse(type, parameter?.ToString(), true, out object? paramEnum)) + { + return valueEnum == paramEnum; + } + */ + } + + return false; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs index b0c30ea11f..7931b63d8e 100644 --- a/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs +++ b/src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs @@ -35,7 +35,6 @@ namespace Avalonia.Controls.Converters Bottom ? Indent * thicknessDepth.Bottom : 0); } return new Thickness(0); - } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) From e828d42952825975b9ab48e03b7c2d5cbace17b9 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 10:49:23 -0400 Subject: [PATCH 11/59] Implement Box/Ring shape states in the template --- .../ColorSpectrum/ColorSpectrum.cs | 1 - .../Controls/ColorSpectrum.xaml | 49 +++++-------------- 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 47b113b0de..ef07e56f10 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -526,7 +526,6 @@ namespace Avalonia.Controls.Primitives // VisualStateManager.GoToState(this, "Normal", useTransitions); //} - //VisualStateManager.GoToState(this, m_shapeFromLastBitmapCreation == ColorSpectrumShape.Box ? "BoxSelected" : "RingSelected", useTransitions); //VisualStateManager.GoToState(this, SelectionEllipseShouldBeLight() ? "SelectionEllipseLight" : "SelectionEllipseDark", useTransitions); //if (IsEnabled && FocusState != FocusState.Unfocused) diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index c4ca09d439..0d6ed59c68 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -1,11 +1,13 @@  + x:CompileBindings="True" + xmlns:converters="using:Avalonia.Controls.Converters"> + + + + + + + + @@ -112,11 +110,19 @@ + + + + + + + - - From 7fb48a8a2578d65c5757cdd4108fe96b2c09d8a0 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 13:41:20 -0400 Subject: [PATCH 22/59] Sync with WinUI at 09267531b807707138addc9bef3c8e9fa340615a --- .../ColorSpectrum/ColorSpectrum.cs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 775a85dfec..3b8d781275 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -191,6 +191,8 @@ namespace Avalonia.Controls.Primitives HsvChannel incrementChannel = HsvChannel.Hue; + bool isSaturationValue = false; + if (key == Key.Left || key == Key.Right) { @@ -201,8 +203,10 @@ namespace Avalonia.Controls.Primitives incrementChannel = HsvChannel.Hue; break; - case ColorSpectrumChannels.SaturationHue: case ColorSpectrumChannels.SaturationValue: + isSaturationValue = true; + goto case ColorSpectrumChannels.SaturationHue; + case ColorSpectrumChannels.SaturationHue: incrementChannel = HsvChannel.Saturation; break; @@ -227,8 +231,10 @@ namespace Avalonia.Controls.Primitives incrementChannel = HsvChannel.Saturation; break; - case ColorSpectrumChannels.HueValue: case ColorSpectrumChannels.SaturationValue: + isSaturationValue = true; + goto case ColorSpectrumChannels.HueValue; + case ColorSpectrumChannels.HueValue: incrementChannel = HsvChannel.Value; break; } @@ -264,6 +270,23 @@ namespace Avalonia.Controls.Primitives IncrementDirection.Lower : IncrementDirection.Higher; + // Image is flipped in RightToLeft, so we need to invert direction in that case. + // The combination saturation and value is also flipped, so we need to invert in that case too. + // If both are false, we don't need to invert. + // If both are true, we would invert twice, so not invert at all. + if ((FlowDirection == FlowDirection.RightToLeft) != isSaturationValue && + (key == Key.Left || key == Key.Right)) + { + if (direction == IncrementDirection.Higher) + { + direction = IncrementDirection.Lower; + } + else + { + direction = IncrementDirection.Higher; + } + } + IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; HsvColor hsvColor = HsvColor; @@ -404,10 +427,18 @@ namespace Avalonia.Controls.Primitives { Color newColor = Color; - if (_oldColor.A != newColor.A || + bool colorChanged = + _oldColor.A != newColor.A || _oldColor.R != newColor.R || _oldColor.G != newColor.G || - _oldColor.B != newColor.B) + _oldColor.B != newColor.B; + + bool areBothColorsBlack = + (_oldColor.R == newColor.R && newColor.R == 0) || + (_oldColor.G == newColor.G && newColor.G == 0) || + (_oldColor.B == newColor.B && newColor.B == 0); + + if (colorChanged || areBothColorsBlack) { var colorChangedEventArgs = new ColorChangedEventArgs(); @@ -745,7 +776,7 @@ namespace Avalonia.Controls.Primitives // we inverted the direction of that axis in order to put more hue on the outside of the ring, // so we need to do similarly here when positioning the ellipse. if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || - _componentsFromLastBitmapCreation == ColorSpectrumChannels.ValueHue) + _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) { sThetaValue = 360 - sThetaValue; sRValue = -sRValue - 1; From baec9c52fc03d9d41a26d5cbe48b050831809166 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 18:31:52 -0400 Subject: [PATCH 23/59] Fix ToolTip handling --- .../ColorSpectrum/ColorSpectrum.cs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 3b8d781275..42e45c61eb 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -308,12 +308,10 @@ namespace Avalonia.Controls.Primitives protected override void OnGotFocus(GotFocusEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null && + ColorHelpers.ToDisplayNameExists) { - if (ColorHelpers.ToDisplayNameExists) - { - //colorNameToolTip.IsOpen = true; - } + ToolTip.SetIsOpen(_colorNameToolTip, true); } UpdatePseudoClasses(); @@ -323,12 +321,10 @@ namespace Avalonia.Controls.Primitives protected override void OnLostFocus(RoutedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null && + ColorHelpers.ToDisplayNameExists) { - if (ColorHelpers.ToDisplayNameExists) - { - //colorNameToolTip.IsOpen = false; - } + ToolTip.SetIsOpen(_colorNameToolTip, false); } UpdatePseudoClasses(); @@ -449,9 +445,9 @@ namespace Avalonia.Controls.Primitives if (ColorHelpers.ToDisplayNameExists) { - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null) { - colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); } } } @@ -832,12 +828,12 @@ namespace Avalonia.Controls.Primitives // We only want to bother with the color name tool tip if we can provide color names. if (ColorHelpers.ToDisplayNameExists) { - if (_colorNameToolTip is ToolTip colorNameToolTip) + if (_colorNameToolTip != null) { // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, // so toggling IsEnabled induces it to do that without incurring any visual glitches. - colorNameToolTip.IsEnabled = false; - colorNameToolTip.IsEnabled = true; + _colorNameToolTip.IsEnabled = false; + _colorNameToolTip.IsEnabled = true; } } From 9f132d4ac0d7980cd98a0a2b9cc971ed0b376a04 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 21 Mar 2022 18:44:17 -0400 Subject: [PATCH 24/59] Simplify by removing unnecessary single-use methods --- .../ColorSpectrum/ColorSpectrum.cs | 238 ++++++++---------- 1 file changed, 99 insertions(+), 139 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 42e45c61eb..892fa76fc9 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -119,7 +119,10 @@ namespace Avalonia.Controls.Primitives if (_layoutRoot != null) { - _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => OnLayoutRootSizeChanged()); + _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => + { + CreateBitmapsAndColorMap(); + }); } if (_inputTarget != null) @@ -335,74 +338,121 @@ namespace Avalonia.Controls.Primitives { if (change.Property == ColorProperty) { - OnColorChanged(change); + // If we're in the process of internally updating the color, + // then we don't want to respond to the Color property changing. + if (!_updatingColor) + { + Color color = Color; + + _updatingHsvColor = true; + Hsv newHsv = (new Rgb(color)).ToHsv(); + HsvColor = newHsv.ToHsvColor(color.A / 255.0); + _updatingHsvColor = false; + + UpdateEllipse(); + UpdateBitmapSources(); + } + + _oldColor = change.OldValue.GetValueOrDefault(); } else if (change.Property == HsvColorProperty) { - OnHsvColorChanged(change); + // If we're in the process of internally updating the HSV color, + // then we don't want to respond to the HsvColor property changing. + if (!_updatingHsvColor) + { + SetColor(); + } + + _oldHsvColor = change.OldValue.GetValueOrDefault(); } - else if ( - change.Property == MinHueProperty || - change.Property == MaxHueProperty) + else if (change.Property == MinHueProperty || + change.Property == MaxHueProperty) { - OnMinMaxHueChanged(); + int minHue = MinHue; + int maxHue = MaxHue; + + if (minHue < 0 || minHue > 359) + { + throw new ArgumentException("MinHue must be between 0 and 359."); + } + else if (maxHue < 0 || maxHue > 359) + { + throw new ArgumentException("MaxHue must be between 0 and 359."); + } + + ColorSpectrumChannels channels = Channels; + + // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.SaturationValue && + channels != ColorSpectrumChannels.ValueSaturation) + { + CreateBitmapsAndColorMap(); + } } - else if ( - change.Property == MinSaturationProperty || - change.Property == MaxSaturationProperty) + else if (change.Property == MinSaturationProperty || + change.Property == MaxSaturationProperty) { - OnMinMaxSaturationChanged(); + int minSaturation = MinSaturation; + int maxSaturation = MaxSaturation; + + if (minSaturation < 0 || minSaturation > 100) + { + throw new ArgumentException("MinSaturation must be between 0 and 100."); + } + else if (maxSaturation < 0 || maxSaturation > 100) + { + throw new ArgumentException("MaxSaturation must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueValue && + channels != ColorSpectrumChannels.ValueHue) + { + CreateBitmapsAndColorMap(); + } } - else if ( - change.Property == MinValueProperty || - change.Property == MaxValueProperty) + else if (change.Property == MinValueProperty || + change.Property == MaxValueProperty) { - OnMinMaxValueChanged(); + int minValue = MinValue; + int maxValue = MaxValue; + + if (minValue < 0 || minValue > 100) + { + throw new ArgumentException("MinValue must be between 0 and 100."); + } + else if (maxValue < 0 || maxValue > 100) + { + throw new ArgumentException("MaxValue must be between 0 and 100."); + } + + ColorSpectrumChannels channels = Channels; + + // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it + // if the maximum or minimum value has changed. + if (channels != ColorSpectrumChannels.HueSaturation && + channels != ColorSpectrumChannels.SaturationHue) + { + CreateBitmapsAndColorMap(); + } } else if (change.Property == ShapeProperty) { - OnShapeChanged(); + CreateBitmapsAndColorMap(); } else if (change.Property == ChannelsProperty) { - OnComponentsChanged(); + CreateBitmapsAndColorMap(); } base.OnPropertyChanged(change); } - private void OnColorChanged(AvaloniaPropertyChangedEventArgs change) - { - // If we're in the process of internally updating the color, - // then we don't want to respond to the Color property changing. - if (!_updatingColor) - { - Color color = Color; - - _updatingHsvColor = true; - Hsv newHsv = (new Rgb(color)).ToHsv(); - HsvColor = newHsv.ToHsvColor(color.A / 255.0); - _updatingHsvColor = false; - - UpdateEllipse(); - UpdateBitmapSources(); - } - - _oldColor = change.OldValue.GetValueOrDefault(); - } - - private void OnHsvColorChanged(AvaloniaPropertyChangedEventArgs change) - { - // If we're in the process of internally updating the HSV color, - // then we don't want to respond to the HsvColor property changing. - if (!_updatingHsvColor) - { - SetColor(); - } - - _oldHsvColor = change.OldValue.GetValueOrDefault(); - } - private void SetColor() { HsvColor hsvColor = HsvColor; @@ -453,91 +503,6 @@ namespace Avalonia.Controls.Primitives } } - protected void OnMinMaxHueChanged() - { - int minHue = MinHue; - int maxHue = MaxHue; - - if (minHue < 0 || minHue > 359) - { - throw new ArgumentException("MinHue must be between 0 and 359."); - } - else if (maxHue < 0 || maxHue > 359) - { - throw new ArgumentException("MaxHue must be between 0 and 359."); - } - - ColorSpectrumChannels channels = Channels; - - // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it - // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.SaturationValue && - channels != ColorSpectrumChannels.ValueSaturation) - { - CreateBitmapsAndColorMap(); - } - } - - protected void OnMinMaxSaturationChanged() - { - int minSaturation = MinSaturation; - int maxSaturation = MaxSaturation; - - if (minSaturation < 0 || minSaturation > 100) - { - throw new ArgumentException("MinSaturation must be between 0 and 100."); - } - else if (maxSaturation < 0 || maxSaturation > 100) - { - throw new ArgumentException("MaxSaturation must be between 0 and 100."); - } - - ColorSpectrumChannels channels = Channels; - - // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it - // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueValue && - channels != ColorSpectrumChannels.ValueHue) - { - CreateBitmapsAndColorMap(); - } - } - - private void OnMinMaxValueChanged() - { - int minValue = MinValue; - int maxValue = MaxValue; - - if (minValue < 0 || minValue > 100) - { - throw new ArgumentException("MinValue must be between 0 and 100."); - } - else if (maxValue < 0 || maxValue > 100) - { - throw new ArgumentException("MaxValue must be between 0 and 100."); - } - - ColorSpectrumChannels channels = Channels; - - // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it - // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueSaturation && - channels != ColorSpectrumChannels.SaturationHue) - { - CreateBitmapsAndColorMap(); - } - } - - private void OnShapeChanged() - { - CreateBitmapsAndColorMap(); - } - - private void OnComponentsChanged() - { - CreateBitmapsAndColorMap(); - } - /// /// Updates the visual state of the control by applying latest PseudoClasses. /// @@ -840,11 +805,6 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); } - private void OnLayoutRootSizeChanged() - { - CreateBitmapsAndColorMap(); - } - private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) { _isPointerOver = true; From d4dbd92ca882f9e76dd1729cfe2b242eef7e745a Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 22 Mar 2022 18:09:20 -0400 Subject: [PATCH 25/59] Support FlowDirection and other simplifications --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 4 +++- .../ColorSpectrum/ColorSpectrum.cs | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index e95c63da33..71fc887bee 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -69,7 +69,9 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register(nameof(HsvColor), new HsvColor(1, 0, 0, 1)); + AvaloniaProperty.Register( + nameof(HsvColor), + new HsvColor(1, 0, 0, 1)); /// /// Gets or sets the maximum value of the Hue channel in the range from 0..359. diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 892fa76fc9..a9497b98f1 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -40,6 +40,7 @@ namespace Avalonia.Controls.Primitives private List _hsvValues = new List(); private IDisposable? _layoutRootDisposable; + private IDisposable? _selectionEllipsePanelDisposable; // XAML template parts private Grid? _layoutRoot; @@ -134,18 +135,18 @@ namespace Avalonia.Controls.Primitives _inputTarget.PointerReleased += OnInputTargetPointerReleased; } - if (ColorHelpers.ToDisplayNameExists) + if (ColorHelpers.ToDisplayNameExists && + _colorNameToolTip != null) { - if (_colorNameToolTip != null) - { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); - } + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); } if (_selectionEllipsePanel != null) { - // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 - //m_selectionEllipsePanel.RegisterPropertyChangedCallback(FrameworkElement.FlowDirectionProperty, OnSelectionEllipseFlowDirectionChanged); + _selectionEllipsePanelDisposable = _selectionEllipsePanel.GetObservable(FlowDirectionProperty).Subscribe(_ => + { + UpdateEllipse(); + }); } // If we haven't yet created our bitmaps, do so now. @@ -166,6 +167,9 @@ namespace Avalonia.Controls.Primitives _layoutRootDisposable?.Dispose(); _layoutRootDisposable = null; + _selectionEllipsePanelDisposable?.Dispose(); + _selectionEllipsePanelDisposable = null; + if (_inputTarget != null) { _inputTarget.PointerEnter -= OnInputTargetPointerEnter; @@ -862,12 +866,6 @@ namespace Avalonia.Controls.Primitives args.Handled = true; } - // TODO: After FlowDirection PR is merged: https://github.com/AvaloniaUI/Avalonia/pull/7810 - //private void OnSelectionEllipseFlowDirectionChanged(DependencyObject o, DependencyProperty p) - //{ - // UpdateEllipse(); - //} - private async void CreateBitmapsAndColorMap() { if (_layoutRoot == null || From 41d2b0dd1cd92e7b1528f5410560d5b6b5c031db Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 22 Mar 2022 21:37:33 -0400 Subject: [PATCH 26/59] Remove ContrastBrushConverter for now (part of ColorSlider) --- .../Converters/ContrastBrushConverter.cs | 88 ------------------- 1 file changed, 88 deletions(-) delete mode 100644 src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs diff --git a/src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs b/src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs deleted file mode 100644 index 79c38ea53f..0000000000 --- a/src/Avalonia.Controls/ColorPicker/Converters/ContrastBrushConverter.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Globalization; -using Avalonia; -using Avalonia.Controls.Primitives; -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace FinancialManager.UWP -{ - /// - /// Gets a color, either black or white, depending on the perceived brightness of the supplied color. - /// - public class ContrastBrushConverter : IValueConverter - { - /// - /// Gets or sets the alpha channel threshold below which a default color is used instead of black/white. - /// - public byte AlphaThreshold { get; set; } = 128; - - /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - Color comparisonColor; - Color? defaultColor = null; - - // Get the changing color to compare against - if (value is Color valueColor) - { - comparisonColor = valueColor; - } - else if (value is HsvColor valueHsvColor) - { - comparisonColor = valueHsvColor.ToRgb(); - } - else if (value is SolidColorBrush valueBrush) - { - comparisonColor = valueBrush.Color; - } - else - { - // Invalid color value provided - return AvaloniaProperty.UnsetValue; - } - - // Get the default color when transparency is high - if (parameter is Color parameterColor) - { - defaultColor = parameterColor; - } - else if (parameter is HsvColor parameterHsvColor) - { - defaultColor = parameterHsvColor.ToRgb(); - } - else if (parameter is SolidColorBrush parameterBrush) - { - defaultColor = parameterBrush.Color; - } - - if (comparisonColor.A < AlphaThreshold && - defaultColor.HasValue) - { - // If the transparency is less than the threshold just use the default brush - // This can commonly be something like the TextControlForeground brush - return new SolidColorBrush(defaultColor.Value); - } - else - { - // Chose a white/black brush based on contrast to the base color - if (ColorHelpers.GetRelativeLuminance(comparisonColor) > 0.5) - { - // Bright color, use a dark for contrast - return new SolidColorBrush(Colors.Black); - } - else - { - // Dark color, use a light for contrast - return new SolidColorBrush(Colors.White); - } - } - } - - /// - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } -} From d3551ce99fa850c840f8730fafcfab256632db01 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 11:59:33 -0400 Subject: [PATCH 27/59] Use the new TemplatePartAttribute --- .../ColorSpectrum/ColorSpectrum.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index a9497b98f1..9d04d1385b 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -20,6 +20,15 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// + [TemplatePart(Name = "PART_ColorNameToolTip", Type = typeof(ToolTip))] + [TemplatePart(Name = "PART_InputTarget", Type = typeof(Canvas))] + [TemplatePart(Name = "PART_LayoutRoot", Type = typeof(Grid))] + [TemplatePart(Name = "PART_SelectionEllipsePanel", Type = typeof(Panel))] + [TemplatePart(Name = "PART_SizingGrid", Type = typeof(Grid))] + [TemplatePart(Name = "PART_SpectrumEllipse", Type = typeof(Ellipse))] + [TemplatePart(Name = "PART_SpectrumRectangle", Type = typeof(Rectangle))] + [TemplatePart(Name = "PART_SpectrumOverlayEllipse", Type = typeof(Ellipse))] + [TemplatePart(Name = "PART_SpectrumOverlayRectangle", Type = typeof(Rectangle))] [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] public partial class ColorSpectrum : TemplatedControl { @@ -108,15 +117,15 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); + _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); + _inputTarget = e.NameScope.Find("PART_InputTarget"); _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); + _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); _sizingGrid = e.NameScope.Find("PART_SizingGrid"); - _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); _spectrumEllipse = e.NameScope.Find("PART_SpectrumEllipse"); - _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); + _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); - _inputTarget = e.NameScope.Find("PART_InputTarget"); - _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); - _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); + _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); if (_layoutRoot != null) { From 32f8b63764fcbfeda894415e706b3686fe07997b Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:01:17 -0400 Subject: [PATCH 28/59] Remove extra variables leftover from C# conversion --- .../ColorSpectrum/ColorSpectrum.cs | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 9d04d1385b..abd84cc452 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -889,41 +889,33 @@ namespace Avalonia.Controls.Primitives return; } - var layoutRoot = _layoutRoot; - var sizingGrid = _sizingGrid; - var inputTarget = _inputTarget; - var spectrumRectangle = _spectrumRectangle; - var spectrumEllipse = _spectrumEllipse; - var spectrumOverlayRectangle = _spectrumOverlayRectangle; - var spectrumOverlayEllipse = _spectrumOverlayEllipse; - // We want ColorSpectrum to always be a square, so we'll take the smaller of the dimensions // and size the sizing grid to that. - double minDimension = Math.Min(layoutRoot.Bounds.Width, layoutRoot.Bounds.Height); + double minDimension = Math.Min(_layoutRoot.Bounds.Width, _layoutRoot.Bounds.Height); if (minDimension == 0) { return; } - sizingGrid.Width = minDimension; - sizingGrid.Height = minDimension; + _sizingGrid.Width = minDimension; + _sizingGrid.Height = minDimension; - if (sizingGrid.Clip is RectangleGeometry clip) + if (_sizingGrid.Clip is RectangleGeometry clip) { clip.Rect = new Rect(0, 0, minDimension, minDimension); } - inputTarget.Width = minDimension; - inputTarget.Height = minDimension; - spectrumRectangle.Width = minDimension; - spectrumRectangle.Height = minDimension; - spectrumEllipse.Width = minDimension; - spectrumEllipse.Height = minDimension; - spectrumOverlayRectangle.Width = minDimension; - spectrumOverlayRectangle.Height = minDimension; - spectrumOverlayEllipse.Width = minDimension; - spectrumOverlayEllipse.Height = minDimension; + _inputTarget.Width = minDimension; + _inputTarget.Height = minDimension; + _spectrumRectangle.Width = minDimension; + _spectrumRectangle.Height = minDimension; + _spectrumEllipse.Width = minDimension; + _spectrumEllipse.Height = minDimension; + _spectrumOverlayRectangle.Width = minDimension; + _spectrumOverlayRectangle.Height = minDimension; + _spectrumOverlayEllipse.Width = minDimension; + _spectrumOverlayEllipse.Height = minDimension; HsvColor hsvColor = HsvColor; int minHue = MinHue; From a47d9b6487dad0303eec8728f30bc14c25d911c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:14:09 -0400 Subject: [PATCH 29/59] Improve comments ColorSpectrumChannels clarifying axis mappings --- .../ColorPicker/ColorSpectrumChannels.cs | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs index 2b3f711634..a31586d175 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrumChannels.cs @@ -9,38 +9,65 @@ namespace Avalonia.Controls { /// /// Defines the two HSV color channels displayed by a . - /// Order of the color channels is important. /// + /// + /// Order of the color channels is important and correspond with an X/Y axis in Box + /// shape or a degree/radius in Ring shape. + /// public enum ColorSpectrumChannels { /// /// The Hue and Value channels. /// + /// + /// In Box shape, Hue is mapped to the X-axis and Value is mapped to the Y-axis. + /// In Ring shape, Hue is mapped to degrees and Value is mapped to radius. + /// HueValue, /// /// The Value and Hue channels. /// + /// + /// In Box shape, Value is mapped to the X-axis and Hue is mapped to the Y-axis. + /// In Ring shape, Value is mapped to degrees and Hue is mapped to radius. + /// ValueHue, /// /// The Hue and Saturation channels. /// + /// + /// In Box shape, Hue is mapped to the X-axis and Saturation is mapped to the Y-axis. + /// In Ring shape, Hue is mapped to degrees and Saturation is mapped to radius. + /// HueSaturation, /// /// The Saturation and Hue channels. /// + /// + /// In Box shape, Saturation is mapped to the X-axis and Hue is mapped to the Y-axis. + /// In Ring shape, Saturation is mapped to degrees and Hue is mapped to radius. + /// SaturationHue, /// /// The Saturation and Value channels. /// + /// + /// In Box shape, Saturation is mapped to the X-axis and Value is mapped to the Y-axis. + /// In Ring shape, Saturation is mapped to degrees and Value is mapped to radius. + /// SaturationValue, /// /// The Value and Saturation channels. /// + /// + /// In Box shape, Value is mapped to the X-axis and Saturation is mapped to the Y-axis. + /// In Ring shape, Value is mapped to degrees and Saturation is mapped to radius. + /// ValueSaturation, }; } From 5904057bd200fb8b933eb8ad516d3db0c25532f6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:41:59 -0400 Subject: [PATCH 30/59] Separate border style setters for easier customization --- .../Controls/ColorSpectrum.xaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index 6a958809a0..8657a072a4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -73,8 +73,6 @@ --> + + + + From 88370ce91bbbe1d1b786c578a22a3286f6329851 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 27 Mar 2022 12:49:10 -0400 Subject: [PATCH 32/59] Add Default theme for ColorSpectrum --- .../Controls/ColorSpectrum.xaml | 138 ++++++++++++++++++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + 2 files changed, 139 insertions(+) create mode 100644 src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml new file mode 100644 index 0000000000..128f7038eb --- /dev/null +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 846d45b839..eea5af5932 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -11,6 +11,7 @@ + From 5a76c63c478ec21a6b2f8b960f6e6e7623904a56 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 11:36:37 -0400 Subject: [PATCH 33/59] Simplify TemplatePartAttribute usage --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index abd84cc452..62d3b4ce88 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -20,15 +20,15 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// - [TemplatePart(Name = "PART_ColorNameToolTip", Type = typeof(ToolTip))] - [TemplatePart(Name = "PART_InputTarget", Type = typeof(Canvas))] - [TemplatePart(Name = "PART_LayoutRoot", Type = typeof(Grid))] - [TemplatePart(Name = "PART_SelectionEllipsePanel", Type = typeof(Panel))] - [TemplatePart(Name = "PART_SizingGrid", Type = typeof(Grid))] - [TemplatePart(Name = "PART_SpectrumEllipse", Type = typeof(Ellipse))] - [TemplatePart(Name = "PART_SpectrumRectangle", Type = typeof(Rectangle))] - [TemplatePart(Name = "PART_SpectrumOverlayEllipse", Type = typeof(Ellipse))] - [TemplatePart(Name = "PART_SpectrumOverlayRectangle", Type = typeof(Rectangle))] + [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] + [TemplatePart("PART_InputTarget", typeof(Canvas))] + [TemplatePart("PART_LayoutRoot", typeof(Grid))] + [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] + [TemplatePart("PART_SizingGrid", typeof(Grid))] + [TemplatePart("PART_SpectrumEllipse", typeof(Ellipse))] + [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] + [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] + [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))] [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] public partial class ColorSpectrum : TemplatedControl { From 4fe7fb71e67264d8635c2fa5383138b2eab7ff12 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 11:44:32 -0400 Subject: [PATCH 34/59] Simplify ColorChangedEventArgs --- .../ColorPicker/ColorChangedEventArgs.cs | 26 +++---------------- .../ColorSpectrum/ColorSpectrum.cs | 6 +---- 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs index 93ba1a4db1..b1d15d6b17 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorChangedEventArgs.cs @@ -17,16 +17,6 @@ namespace Avalonia.Controls /// public class ColorChangedEventArgs : EventArgs { - private Color _OldColor; - private Color _NewColor; - - /// - /// Initializes a new instance of the class. - /// - public ColorChangedEventArgs() - { - } - /// /// Initializes a new instance of the class. /// @@ -34,26 +24,18 @@ namespace Avalonia.Controls /// The new/updated color that triggered the change event. public ColorChangedEventArgs(Color oldColor, Color newColor) { - _OldColor = oldColor; - _NewColor = newColor; + OldColor = oldColor; + NewColor = newColor; } /// /// Gets the old/original color from before the change event. /// - public Color OldColor - { - get => _OldColor; - internal set => _OldColor = value; - } + public Color OldColor { get; private set; } /// /// Gets the new/updated color that triggered the change event. /// - public Color NewColor - { - get => _NewColor; - internal set => _NewColor = value; - } + public Color NewColor { get; private set; } } } diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 62d3b4ce88..11e1437f81 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -499,11 +499,7 @@ namespace Avalonia.Controls.Primitives if (colorChanged || areBothColorsBlack) { - var colorChangedEventArgs = new ColorChangedEventArgs(); - - colorChangedEventArgs.OldColor = _oldColor; - colorChangedEventArgs.NewColor = newColor; - + var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); ColorChanged?.Invoke(this, colorChangedEventArgs); if (ColorHelpers.ToDisplayNameExists) From 073b3e4d34706a1198fc7fea3f9d02dc747dcb50 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:05:14 -0400 Subject: [PATCH 35/59] Slightly rework events using OnAttached/DetachedToVisualTree --- .../ColorSpectrum/ColorSpectrum.cs | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 11e1437f81..dfead58c6a 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -115,7 +115,7 @@ namespace Avalonia.Controls.Primitives { base.OnApplyTemplate(e); - UnregisterEvents(); + UnregisterEvents(); // Failsafe _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); @@ -127,14 +127,6 @@ namespace Avalonia.Controls.Primitives _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); _spectrumOverlayRectangle = e.NameScope.Find("PART_SpectrumOverlayRectangle"); - if (_layoutRoot != null) - { - _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => - { - CreateBitmapsAndColorMap(); - }); - } - if (_inputTarget != null) { _inputTarget.PointerEnter += OnInputTargetPointerEnter; @@ -144,10 +136,12 @@ namespace Avalonia.Controls.Primitives _inputTarget.PointerReleased += OnInputTargetPointerReleased; } - if (ColorHelpers.ToDisplayNameExists && - _colorNameToolTip != null) + if (_layoutRoot != null) { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => + { + CreateBitmapsAndColorMap(); + }); } if (_selectionEllipsePanel != null) @@ -158,6 +152,12 @@ namespace Avalonia.Controls.Primitives }); } + if (ColorHelpers.ToDisplayNameExists && + _colorNameToolTip != null) + { + _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + } + // If we haven't yet created our bitmaps, do so now. if (_hsvValues.Count == 0) { @@ -168,6 +168,22 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + // OnAttachedToVisualTree is called after OnApplyTemplate so events cannot be connected here + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + UnregisterEvents(); + } + /// /// Explicitly unregisters all events connected in OnApplyTemplate(). /// From 7e3cf42c00f6dfe6ea1a3a16af4f2906224cbff6 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:08:05 -0400 Subject: [PATCH 36/59] Rename event handlers that are not overridable methods --- .../ColorSpectrum/ColorSpectrum.cs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index dfead58c6a..c63bcb225b 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -129,11 +129,11 @@ namespace Avalonia.Controls.Primitives if (_inputTarget != null) { - _inputTarget.PointerEnter += OnInputTargetPointerEnter; - _inputTarget.PointerLeave += OnInputTargetPointerLeave; - _inputTarget.PointerPressed += OnInputTargetPointerPressed; - _inputTarget.PointerMoved += OnInputTargetPointerMoved; - _inputTarget.PointerReleased += OnInputTargetPointerReleased; + _inputTarget.PointerEnter += InputTarget_PointerEnter; + _inputTarget.PointerLeave += InputTarget_PointerLeave; + _inputTarget.PointerPressed += InputTarget_PointerPressed; + _inputTarget.PointerMoved += InputTarget_PointerMoved; + _inputTarget.PointerReleased += InputTarget_PointerReleased; } if (_layoutRoot != null) @@ -197,11 +197,11 @@ namespace Avalonia.Controls.Primitives if (_inputTarget != null) { - _inputTarget.PointerEnter -= OnInputTargetPointerEnter; - _inputTarget.PointerLeave -= OnInputTargetPointerLeave; - _inputTarget.PointerPressed -= OnInputTargetPointerPressed; - _inputTarget.PointerMoved -= OnInputTargetPointerMoved; - _inputTarget.PointerReleased -= OnInputTargetPointerReleased; + _inputTarget.PointerEnter -= InputTarget_PointerEnter; + _inputTarget.PointerLeave -= InputTarget_PointerLeave; + _inputTarget.PointerPressed -= InputTarget_PointerPressed; + _inputTarget.PointerMoved -= InputTarget_PointerMoved; + _inputTarget.PointerReleased -= InputTarget_PointerReleased; } } @@ -830,21 +830,24 @@ namespace Avalonia.Controls.Primitives UpdatePseudoClasses(); } - private void OnInputTargetPointerEnter(object? sender, PointerEventArgs args) + /// + private void InputTarget_PointerEnter(object? sender, PointerEventArgs args) { _isPointerOver = true; UpdatePseudoClasses(); args.Handled = true; } - private void OnInputTargetPointerLeave(object? sender, PointerEventArgs args) + /// + private void InputTarget_PointerLeave(object? sender, PointerEventArgs args) { _isPointerOver = false; UpdatePseudoClasses(); args.Handled = true; } - private void OnInputTargetPointerPressed(object? sender, PointerPressedEventArgs args) + /// + private void InputTarget_PointerPressed(object? sender, PointerPressedEventArgs args) { var inputTarget = _inputTarget; @@ -864,7 +867,8 @@ namespace Avalonia.Controls.Primitives args.Handled = true; } - private void OnInputTargetPointerMoved(object? sender, PointerEventArgs args) + /// + private void InputTarget_PointerMoved(object? sender, PointerEventArgs args) { if (!_isPointerPressed) { @@ -875,7 +879,8 @@ namespace Avalonia.Controls.Primitives args.Handled = true; } - private void OnInputTargetPointerReleased(object? sender, PointerReleasedEventArgs args) + /// + private void InputTarget_PointerReleased(object? sender, PointerReleasedEventArgs args) { _isPointerPressed = false; _shouldShowLargeSelection = false; From 89b6e4e9d05f95d6ef73f30a777387e72f4d3ff2 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:10:39 -0400 Subject: [PATCH 37/59] If we can't connect in OnAttachedToVisualTree we also can't disconnect --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index c63bcb225b..2ff264baf9 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -180,8 +180,6 @@ namespace Avalonia.Controls.Primitives protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - - UnregisterEvents(); } /// From 088d63b9128e153ef91c7cfb4015718eb0f309ed Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:17:15 -0400 Subject: [PATCH 38/59] Remove Clip geometry which seems to be unnecessary --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 6 ------ src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml | 6 ++---- src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml | 6 ++---- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 2ff264baf9..45b95ce981 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -915,12 +915,6 @@ namespace Avalonia.Controls.Primitives _sizingGrid.Width = minDimension; _sizingGrid.Height = minDimension; - - if (_sizingGrid.Clip is RectangleGeometry clip) - { - clip.Rect = new Rect(0, 0, minDimension, minDimension); - } - _inputTarget.Width = minDimension; _inputTarget.Height = minDimension; _spectrumRectangle.Width = minDimension; diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 128f7038eb..c5ea317ce3 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -20,10 +20,8 @@ - - - + VerticalAlignment="Center" + ClipToBounds="True"> - - - + VerticalAlignment="Center" + ClipToBounds="True"> Date: Mon, 28 Mar 2022 12:39:38 -0400 Subject: [PATCH 39/59] Switch from Grid to Panel which is more lightweight --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 18 +++++++++--------- .../Controls/ColorSpectrum.xaml | 16 +++++++++------- .../Controls/ColorSpectrum.xaml | 16 +++++++++------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 45b95ce981..1b04607ef0 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -22,9 +22,9 @@ namespace Avalonia.Controls.Primitives /// [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] [TemplatePart("PART_InputTarget", typeof(Canvas))] - [TemplatePart("PART_LayoutRoot", typeof(Grid))] + [TemplatePart("PART_LayoutRoot", typeof(Panel))] [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] - [TemplatePart("PART_SizingGrid", typeof(Grid))] + [TemplatePart("PART_SizingPanel", typeof(Panel))] [TemplatePart("PART_SpectrumEllipse", typeof(Ellipse))] [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] @@ -52,8 +52,8 @@ namespace Avalonia.Controls.Primitives private IDisposable? _selectionEllipsePanelDisposable; // XAML template parts - private Grid? _layoutRoot; - private Grid? _sizingGrid; + private Panel? _layoutRoot; + private Panel? _sizingPanel; private Rectangle? _spectrumRectangle; private Ellipse? _spectrumEllipse; private Rectangle? _spectrumOverlayRectangle; @@ -119,9 +119,9 @@ namespace Avalonia.Controls.Primitives _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); - _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); + _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); - _sizingGrid = e.NameScope.Find("PART_SizingGrid"); + _sizingPanel = e.NameScope.Find("PART_SizingPanel"); _spectrumEllipse = e.NameScope.Find("PART_SpectrumEllipse"); _spectrumRectangle = e.NameScope.Find("PART_SpectrumRectangle"); _spectrumOverlayEllipse = e.NameScope.Find("PART_SpectrumOverlayEllipse"); @@ -893,7 +893,7 @@ namespace Avalonia.Controls.Primitives private async void CreateBitmapsAndColorMap() { if (_layoutRoot == null || - _sizingGrid == null || + _sizingPanel == null || _inputTarget == null || _spectrumRectangle == null || _spectrumEllipse == null || @@ -913,8 +913,8 @@ namespace Avalonia.Controls.Primitives return; } - _sizingGrid.Width = minDimension; - _sizingGrid.Height = minDimension; + _sizingPanel.Width = minDimension; + _sizingPanel.Height = minDimension; _inputTarget.Width = minDimension; _inputTarget.Height = minDimension; _spectrumRectangle.Width = minDimension; diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index c5ea317ce3..7fa703ed18 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -17,11 +17,13 @@ - - + + - - + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index 0b68aa3744..b138a4ad63 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -17,11 +17,13 @@ - - + + - - + + From e607f8cce8ccced935eded7450751c4973ace6ea Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:55:30 -0400 Subject: [PATCH 40/59] Watch for Bounds and FlowDirection property changes on the control itself --- .../ColorSpectrum/ColorSpectrum.cs | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 1b04607ef0..1b68c686fd 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -48,9 +48,6 @@ namespace Avalonia.Controls.Primitives private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); - private IDisposable? _layoutRootDisposable; - private IDisposable? _selectionEllipsePanelDisposable; - // XAML template parts private Panel? _layoutRoot; private Panel? _sizingPanel; @@ -136,22 +133,6 @@ namespace Avalonia.Controls.Primitives _inputTarget.PointerReleased += InputTarget_PointerReleased; } - if (_layoutRoot != null) - { - _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => - { - CreateBitmapsAndColorMap(); - }); - } - - if (_selectionEllipsePanel != null) - { - _selectionEllipsePanelDisposable = _selectionEllipsePanel.GetObservable(FlowDirectionProperty).Subscribe(_ => - { - UpdateEllipse(); - }); - } - if (ColorHelpers.ToDisplayNameExists && _colorNameToolTip != null) { @@ -187,12 +168,6 @@ namespace Avalonia.Controls.Primitives /// private void UnregisterEvents() { - _layoutRootDisposable?.Dispose(); - _layoutRootDisposable = null; - - _selectionEllipsePanelDisposable?.Dispose(); - _selectionEllipsePanelDisposable = null; - if (_inputTarget != null) { _inputTarget.PointerEnter -= InputTarget_PointerEnter; @@ -363,7 +338,11 @@ namespace Avalonia.Controls.Primitives /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (change.Property == ColorProperty) + if (change.Property == BoundsProperty) + { + CreateBitmapsAndColorMap(); + } + else if (change.Property == ColorProperty) { // If we're in the process of internally updating the color, // then we don't want to respond to the Color property changing. @@ -382,6 +361,10 @@ namespace Avalonia.Controls.Primitives _oldColor = change.OldValue.GetValueOrDefault(); } + else if (change.Property == FlowDirectionProperty) + { + UpdateEllipse(); + } else if (change.Property == HsvColorProperty) { // If we're in the process of internally updating the HSV color, From 680d3084aa5aaf068326137f54075f8259a4f8ee Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 28 Mar 2022 12:58:01 -0400 Subject: [PATCH 41/59] Rename border controls in template to match convention --- src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml | 8 ++++---- src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 7fa703ed18..1d127ec115 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -72,14 +72,14 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index b138a4ad63..c8267b57d9 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -72,14 +72,14 @@ - - - From 9ccc563cfb45a1de2b7495d0871376b8f975f7dd Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 29 Mar 2022 20:13:14 -0400 Subject: [PATCH 42/59] Revert "Watch for Bounds and FlowDirection property changes on the control itself" This reverts commit e607f8cce8ccced935eded7450751c4973ace6ea. --- .../ColorSpectrum/ColorSpectrum.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 1b68c686fd..1b04607ef0 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -48,6 +48,9 @@ namespace Avalonia.Controls.Primitives private bool _shouldShowLargeSelection = false; private List _hsvValues = new List(); + private IDisposable? _layoutRootDisposable; + private IDisposable? _selectionEllipsePanelDisposable; + // XAML template parts private Panel? _layoutRoot; private Panel? _sizingPanel; @@ -133,6 +136,22 @@ namespace Avalonia.Controls.Primitives _inputTarget.PointerReleased += InputTarget_PointerReleased; } + if (_layoutRoot != null) + { + _layoutRootDisposable = _layoutRoot.GetObservable(BoundsProperty).Subscribe(_ => + { + CreateBitmapsAndColorMap(); + }); + } + + if (_selectionEllipsePanel != null) + { + _selectionEllipsePanelDisposable = _selectionEllipsePanel.GetObservable(FlowDirectionProperty).Subscribe(_ => + { + UpdateEllipse(); + }); + } + if (ColorHelpers.ToDisplayNameExists && _colorNameToolTip != null) { @@ -168,6 +187,12 @@ namespace Avalonia.Controls.Primitives /// private void UnregisterEvents() { + _layoutRootDisposable?.Dispose(); + _layoutRootDisposable = null; + + _selectionEllipsePanelDisposable?.Dispose(); + _selectionEllipsePanelDisposable = null; + if (_inputTarget != null) { _inputTarget.PointerEnter -= InputTarget_PointerEnter; @@ -338,11 +363,7 @@ namespace Avalonia.Controls.Primitives /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (change.Property == BoundsProperty) - { - CreateBitmapsAndColorMap(); - } - else if (change.Property == ColorProperty) + if (change.Property == ColorProperty) { // If we're in the process of internally updating the color, // then we don't want to respond to the Color property changing. @@ -361,10 +382,6 @@ namespace Avalonia.Controls.Primitives _oldColor = change.OldValue.GetValueOrDefault(); } - else if (change.Property == FlowDirectionProperty) - { - UpdateEllipse(); - } else if (change.Property == HsvColorProperty) { // If we're in the process of internally updating the HSV color, From f46ed4d922faa02eaa8d658fcb88e467290d3005 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 29 Mar 2022 20:21:07 -0400 Subject: [PATCH 43/59] Switch last Grid entirely to Panel --- .../ColorPicker/ColorSpectrum/ColorSpectrum.cs | 14 ++++++-------- .../Controls/ColorSpectrum.xaml | 8 ++++---- .../Controls/ColorSpectrum.xaml | 8 ++++---- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs index 1b04607ef0..6779c54a1e 100644 --- a/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls/ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -643,9 +643,7 @@ namespace Avalonia.Controls.Primitives private void UpdateEllipse() { - var selectionEllipsePanel = _selectionEllipsePanel; - - if (selectionEllipsePanel == null) + if (_selectionEllipsePanel == null) { return; } @@ -654,12 +652,12 @@ namespace Avalonia.Controls.Primitives if (_imageWidthFromLastBitmapCreation == 0 || _imageHeightFromLastBitmapCreation == 0) { - selectionEllipsePanel.IsVisible = false; + _selectionEllipsePanel.IsVisible = false; return; } else { - selectionEllipsePanel.IsVisible = true; + _selectionEllipsePanel.IsVisible = true; } double xPosition; @@ -810,8 +808,8 @@ namespace Avalonia.Controls.Primitives yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; } - Canvas.SetLeft(selectionEllipsePanel, xPosition - (selectionEllipsePanel.Width / 2)); - Canvas.SetTop(selectionEllipsePanel, yPosition - (selectionEllipsePanel.Height / 2)); + Canvas.SetLeft(_selectionEllipsePanel, xPosition - (_selectionEllipsePanel.Width / 2)); + Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. if (ColorHelpers.ToDisplayNameExists) @@ -905,7 +903,7 @@ namespace Avalonia.Controls.Primitives } // We want ColorSpectrum to always be a square, so we'll take the smaller of the dimensions - // and size the sizing grid to that. + // and size the sizing panel to that. double minDimension = Math.Min(_layoutRoot.Bounds.Width, _layoutRoot.Bounds.Height); if (minDimension == 0) diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 1d127ec115..7065ba30f6 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -52,7 +52,7 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + - + - - diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml index c8267b57d9..4208d9665f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml @@ -52,7 +52,7 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + - + - - From 1a8435c04db7a8c7ae14b8949da87cad925dcf22 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 4 Apr 2022 21:35:46 -0400 Subject: [PATCH 44/59] Add CornerRadiusToDoubleConverter --- ...iusFilterKind.cs => CornerRadiusCorner.cs} | 18 +++++--- .../Converters/CornerRadiusFilterConverter.cs | 17 +++---- .../CornerRadiusToDoubleConverter.cs | 46 +++++++++++++++++++ 3 files changed, 66 insertions(+), 15 deletions(-) rename src/Avalonia.Controls/Converters/{CornerRadiusFilterKind.cs => CornerRadiusCorner.cs} (57%) create mode 100644 src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs b/src/Avalonia.Controls/Converters/CornerRadiusCorner.cs similarity index 57% rename from src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs rename to src/Avalonia.Controls/Converters/CornerRadiusCorner.cs index 6a9d0596be..205dd12f48 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterKind.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusCorner.cs @@ -3,29 +3,33 @@ namespace Avalonia.Controls.Converters { /// - /// Defines constants that specify the filter type for a instance. + /// Defines constants that specify the corner of a . /// [Flags] - public enum CornerRadiusFilterKinds + public enum CornerRadiusCorner { /// - /// No filter applied. + /// No corner. /// None, + /// - /// Filters TopLeft value. + /// The TopLeft corner. /// TopLeft = 1, + /// - /// Filters TopRight value. + /// The TopRight corner. /// TopRight = 2, + /// - /// Filters BottomLeft value. + /// The BottomLeft corner. /// BottomLeft = 4, + /// - /// Filters BottomRight value. + /// The BottomRight corner. /// BottomRight = 8 } diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs index 643c30178e..b298fbf2e1 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs @@ -7,17 +7,18 @@ namespace Avalonia.Controls.Converters { /// /// Converts an existing CornerRadius struct to a new CornerRadius struct, - /// with filters applied to extract only the specified fields, leaving the others set to 0. + /// with filters applied to extract only the specified corners, leaving the others set to 0. /// public class CornerRadiusFilterConverter : IValueConverter { /// - /// Gets or sets the type of the filter applied to the . + /// Gets or sets the corners to filter by. + /// Only the specified corners will be included in the converted . /// - public CornerRadiusFilterKinds Filter { get; set; } + public CornerRadiusCorner Filter { get; set; } /// - /// Gets or sets the scale multiplier applied to the . + /// Gets or sets the scale multiplier applied uniformly to each corner. /// public double Scale { get; set; } = 1; @@ -29,10 +30,10 @@ namespace Avalonia.Controls.Converters } return new CornerRadius( - Filter.HasAllFlags(CornerRadiusFilterKinds.TopLeft) ? radius.TopLeft * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.TopRight) ? radius.TopRight * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.BottomRight) ? radius.BottomRight * Scale : 0, - Filter.HasAllFlags(CornerRadiusFilterKinds.BottomLeft) ? radius.BottomLeft * Scale : 0); + Filter.HasAllFlags(CornerRadiusCorner.TopLeft) ? radius.TopLeft * Scale : 0, + Filter.HasAllFlags(CornerRadiusCorner.TopRight) ? radius.TopRight * Scale : 0, + Filter.HasAllFlags(CornerRadiusCorner.BottomRight) ? radius.BottomRight * Scale : 0, + Filter.HasAllFlags(CornerRadiusCorner.BottomLeft) ? radius.BottomLeft * Scale : 0); } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) diff --git a/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs new file mode 100644 index 0000000000..2d549504ff --- /dev/null +++ b/src/Avalonia.Controls/Converters/CornerRadiusToDoubleConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts one corner of a to its double value. + /// + public class CornerRadiusToDoubleConverter : IValueConverter + { + /// + /// Gets or sets the specific corner of the to convert to double. + /// + public CornerRadiusCorner Corner { get; set; } + + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (!(value is CornerRadius cornerRadius)) + { + return AvaloniaProperty.UnsetValue; + } + + switch (Corner) + { + case CornerRadiusCorner.TopLeft: + return cornerRadius.TopLeft; + case CornerRadiusCorner.TopRight: + return cornerRadius.TopRight; + case CornerRadiusCorner.BottomRight: + return cornerRadius.BottomRight; + case CornerRadiusCorner.BottomLeft: + return cornerRadius.BottomLeft; + default: + return 0.0; + } + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} From 30f713746a399883d59446b3a2d5307aaaf014b6 Mon Sep 17 00:00:00 2001 From: robloo Date: Mon, 4 Apr 2022 21:39:11 -0400 Subject: [PATCH 45/59] Support CornerRadius in ColorSpectrum --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 6 ++++++ .../Controls/ColorSpectrum.xaml | 20 ++++++++++--------- .../Controls/ColorSpectrum.xaml | 20 ++++++++++--------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index eee041e5c5..ec34193f8c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -19,5 +19,11 @@ Shape="Ring" Height="256" Width="256" /> + diff --git a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml index 7065ba30f6..338ea69dac 100644 --- a/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Themes.Default/Controls/ColorSpectrum.xaml @@ -11,6 +11,8 @@ + + - + diff --git a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml similarity index 97% rename from src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 5de6daea11..545702ea84 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,14 +1,8 @@ - - - - - - - @@ -136,5 +130,5 @@ - + diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 682f395e5b..468b723f5b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -11,7 +11,6 @@ - diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index b7d35ad5cc..5b217e4764 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -11,7 +11,6 @@ - From cf654da43f1d4a8dae5f1e37347c724221eb4ecc Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 22:38:49 -0400 Subject: [PATCH 49/59] Add separate Avalonia.Controls.ColorPicker project --- Avalonia.sln | 26 +++++++++++++++++++ samples/ControlCatalog/App.xaml.cs | 13 +++++++++- samples/ControlCatalog/ControlCatalog.csproj | 1 + samples/ControlCatalog/MainView.xaml.cs | 4 +++ samples/Sandbox/Sandbox.csproj | 1 + src/Avalonia.Base/Properties/AssemblyInfo.cs | 1 + .../Avalonia.Controls.ColorPicker.csproj | 21 +++++++++++++++ .../Avalonia.Diagnostics.csproj | 1 + .../Avalonia.LeakTests.csproj | 1 + 9 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj diff --git a/Avalonia.sln b/Avalonia.sln index 1e2a3c6027..ea30514c3e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -169,6 +169,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{1ECC012A-8837-4AE2-9BDA-3E2857898727}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" @@ -1963,6 +1965,30 @@ Global {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.Build.0 = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 866fb8632a..6539cdaee6 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -18,6 +18,16 @@ namespace ControlCatalog DataContext = new ApplicationViewModel(); } + public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + { + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml") + }; + + public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) + { + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml") + }; + public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml") @@ -69,7 +79,8 @@ namespace ControlCatalog public override void Initialize() { Styles.Insert(0, Fluent); - Styles.Insert(1, DataGridFluent); + Styles.Insert(1, ColorPickerFluent); + Styles.Insert(2, DataGridFluent); AvaloniaXamlLoader.Load(this); } diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index c0e24357ca..7cbd8a3f9c 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -23,6 +23,7 @@ + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index e8ea39abbb..f2d89a1325 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -50,6 +50,7 @@ namespace ControlCatalog } Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; + Application.Current.Styles.Add(App.ColorPickerFluent); } else if (theme == CatalogTheme.FluentDark) { @@ -60,18 +61,21 @@ namespace ControlCatalog } Application.Current.Styles[0] = App.Fluent; Application.Current.Styles[1] = App.DataGridFluent; + Application.Current.Styles.Add(App.ColorPickerFluent); } else if (theme == CatalogTheme.DefaultLight) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light; Application.Current.Styles[0] = App.DefaultLight; Application.Current.Styles[1] = App.DataGridDefault; + Application.Current.Styles.Add(App.ColorPickerDefault); } else if (theme == CatalogTheme.DefaultDark) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark; Application.Current.Styles[0] = App.DefaultDark; Application.Current.Styles[1] = App.DataGridDefault; + Application.Current.Styles.Add(App.ColorPickerDefault); } } }; diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj index 8f2812e048..f3c38cd96e 100644 --- a/samples/Sandbox/Sandbox.csproj +++ b/samples/Sandbox/Sandbox.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index a0560924e7..2c40c768f5 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -19,6 +19,7 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Controls.ColorPicker, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj new file mode 100644 index 0000000000..21218fc771 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -0,0 +1,21 @@ + + + net6.0;netstandard2.0 + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index 2fb7c07b6f..a83e8be475 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 0c663e1a8f..a308e1c3ed 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -9,6 +9,7 @@ + From 83402cb285712d5d9a5d3563f9aa21fc87bae2cc Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 22:50:28 -0400 Subject: [PATCH 50/59] Make Color.ToHsv() internal again --- src/Avalonia.Base/Media/Color.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index db18ec7c6e..cb90404f6d 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -653,9 +653,6 @@ namespace Avalonia.Media (byteToDouble * alpha)); } - // TODO: Mark the below .ToHsv(double...) method internal and make internals visible to Avalonia.Controls - // It is needed for the ColorPicker which works in normalized values directly - /// /// Converts the given RGBA color component values to their HSV color equivalent. /// @@ -668,7 +665,7 @@ namespace Avalonia.Media /// The Blue component in the RGB color model within the range 0..1. /// The Alpha component in the RGB color model within the range 0..1. /// A new equivalent to the given RGBA values. - public static HsvColor ToHsv( + internal static HsvColor ToHsv( double r, double g, double b, From 9f1833030dec741137058631dcfc1e67f63a5e23 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 23:29:46 -0400 Subject: [PATCH 51/59] Use namespaces in XAML --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 36 +++++++++---------- .../Themes/Default.xaml | 27 +++++++------- .../Themes/Fluent.xaml | 27 +++++++------- 3 files changed, 46 insertions(+), 44 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index ec34193f8c..08f56be8e3 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -2,28 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" + xmlns:cpp="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls.ColorPicker" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 832daf8853..8d9b49b9b1 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,7 +1,8 @@ + xmlns:converters="using:Avalonia.Controls.Converters" + xmlns:primitive="using:Avalonia.Controls.Primitives"> @@ -9,7 +10,7 @@ - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 545702ea84..9c3a74b1fa 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,7 +1,8 @@ + xmlns:converters="using:Avalonia.Controls.Converters" + xmlns:primitive="using:Avalonia.Controls.Primitives"> @@ -9,7 +10,7 @@ - - - - - - - - - - From f13398506d96b964fb96f834063053bdaef11f23 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 23:30:22 -0400 Subject: [PATCH 52/59] Fix Avalonia.Controls.ColorPicker.csproj - Disable ApiDiff tool - Enable nullable reference types --- .../Avalonia.Controls.ColorPicker.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 21218fc771..03950fb168 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -3,6 +3,9 @@ net6.0;netstandard2.0 + + + @@ -16,6 +19,7 @@ - + + From 8da297b30073c05142ad3e31fb7710553db6e824 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 16 Apr 2022 23:36:09 -0400 Subject: [PATCH 53/59] Correct ColorPicker style loading in ControlCatalog --- samples/ControlCatalog/MainView.xaml.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index f2d89a1325..0326946c08 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -49,8 +49,8 @@ namespace ControlCatalog App.Fluent.Mode = FluentThemeMode.Light; } Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; - Application.Current.Styles.Add(App.ColorPickerFluent); + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; } else if (theme == CatalogTheme.FluentDark) { @@ -60,22 +60,22 @@ namespace ControlCatalog App.Fluent.Mode = FluentThemeMode.Dark; } Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.DataGridFluent; - Application.Current.Styles.Add(App.ColorPickerFluent); + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; } else if (theme == CatalogTheme.DefaultLight) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light; Application.Current.Styles[0] = App.DefaultLight; - Application.Current.Styles[1] = App.DataGridDefault; - Application.Current.Styles.Add(App.ColorPickerDefault); + Application.Current.Styles[1] = App.ColorPickerDefault; + Application.Current.Styles[2] = App.DataGridDefault; } else if (theme == CatalogTheme.DefaultDark) { App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark; Application.Current.Styles[0] = App.DefaultDark; - Application.Current.Styles[1] = App.DataGridDefault; - Application.Current.Styles.Add(App.ColorPickerDefault); + Application.Current.Styles[1] = App.ColorPickerDefault; + Application.Current.Styles[2] = App.DataGridDefault; } } }; From a389f7b0d8aa82db0d04401887f72f0bca3c2e3a Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:02:18 -0400 Subject: [PATCH 54/59] Fix ColorPicker project references --- src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj | 2 +- tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index a83e8be475..adddf3f57b 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -13,7 +13,7 @@ - + diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index a308e1c3ed..a00b24bdd7 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -9,7 +9,7 @@ - + From 60e0d12543aac9b8948f8657f0f64292cc219b50 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:04:56 -0400 Subject: [PATCH 55/59] Revert "Use namespaces in XAML" This reverts commit 9f1833030dec741137058631dcfc1e67f63a5e23. --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 36 +++++++++---------- .../Themes/Default.xaml | 27 +++++++------- .../Themes/Fluent.xaml | 27 +++++++------- 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index 08f56be8e3..ec34193f8c 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -2,28 +2,28 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:cpp="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls.ColorPicker" + xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - - + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 8d9b49b9b1..832daf8853 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,8 +1,7 @@ + xmlns:converters="using:Avalonia.Controls.Converters"> @@ -10,7 +9,7 @@ - - - - - - - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 9c3a74b1fa..545702ea84 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,8 +1,7 @@ + xmlns:converters="using:Avalonia.Controls.Converters"> @@ -10,7 +9,7 @@ - - - - - - - - - - From 8453b4a79ac85fd338f23f1941005ac0ad788ae1 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:11:23 -0400 Subject: [PATCH 56/59] Add AssemblyInfo for ColorPicker.csproj --- .../Properties/AssemblyInfo.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs diff --git a/src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs b/src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..0135541349 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +using System.Runtime.CompilerServices; +using Avalonia.Metadata; + +[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] + +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] From a66e15e8a2a3f2b8d2f9d0ae14eaaf8509436260 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:11:41 -0400 Subject: [PATCH 57/59] Add back PackageId for ColorPicker.csproj --- .../Avalonia.Controls.ColorPicker.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 03950fb168..0952c899d4 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -1,7 +1,7 @@  net6.0;netstandard2.0 - + Avalonia.Controls.ColorPicker From 397fd5068fc26e7093e0316d1cfb0beb28aa1543 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 17 Apr 2022 22:31:23 -0400 Subject: [PATCH 58/59] Rename 'channel' to 'component' --- .../ColorSpectrum/ColorHelpers.cs | 44 ++-- .../ColorSpectrum/ColorSpectrum.Properties.cs | 30 +-- .../ColorSpectrum/ColorSpectrum.cs | 231 +++++++++--------- .../ColorSpectrum/Hsv.cs | 18 +- .../ColorSpectrum/IncrementAmount.cs | 2 +- .../ColorSpectrum/IncrementDirection.cs | 2 +- .../ColorSpectrum/Rgb.cs | 20 +- ...Channels.cs => ColorSpectrumComponents.cs} | 18 +- .../HsvChannel.cs | 33 --- .../HsvComponent.cs | 47 ++++ 10 files changed, 229 insertions(+), 216 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrumChannels.cs => ColorSpectrumComponents.cs} (81%) delete mode 100644 src/Avalonia.Controls.ColorPicker/HsvChannel.cs create mode 100644 src/Avalonia.Controls.ColorPicker/HsvComponent.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs index cd2decf0a7..b912d39aba 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs @@ -26,9 +26,9 @@ namespace Avalonia.Controls.Primitives return string.Empty; } - public static Hsv IncrementColorChannel( + public static Hsv IncrementColorComponent( Hsv originalHsv, - HsvChannel channel, + HsvComponent component, IncrementDirection direction, IncrementAmount amount, bool shouldWrap, @@ -52,25 +52,25 @@ namespace Avalonia.Controls.Primitives // If we're adding a large increment, then we want to snap to the next // or previous major value - for hue, this is every increment of 30; // for saturation and value, this is every increment of 10. - switch (channel) + switch (component) { - case HsvChannel.Hue: + case HsvComponent.Hue: valueToIncrement = ref newHsv.H; incrementAmount = amount == IncrementAmount.Small ? 1 : 30; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: valueToIncrement = ref newHsv.S; incrementAmount = amount == IncrementAmount.Small ? 1 : 10; break; - case HsvChannel.Value: + case HsvComponent.Value: valueToIncrement = ref newHsv.V; incrementAmount = amount == IncrementAmount.Small ? 1 : 10; break; default: - throw new InvalidOperationException("Invalid HsvChannel."); + throw new InvalidOperationException("Invalid HsvComponent."); } double previousValue = valueToIncrement; @@ -99,14 +99,14 @@ namespace Avalonia.Controls.Primitives // While working with named colors, we're going to need to be working in actual HSV units, // so we'll divide the min bound and max bound by 100 in the case of saturation or value, // since we'll have received units between 0-100 and we need them within 0-1. - if (channel == HsvChannel.Saturation || - channel == HsvChannel.Value) + if (component == HsvComponent.Saturation || + component == HsvComponent.Value) { minBound /= 100; maxBound /= 100; } - newHsv = FindNextNamedColor(originalHsv, channel, direction, shouldWrap, minBound, maxBound); + newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound); } return newHsv; @@ -114,7 +114,7 @@ namespace Avalonia.Controls.Primitives public static Hsv FindNextNamedColor( Hsv originalHsv, - HsvChannel channel, + HsvComponent component, IncrementDirection direction, bool shouldWrap, double minBound, @@ -136,28 +136,28 @@ namespace Avalonia.Controls.Primitives ref double newValue = ref newHsv.H; double incrementAmount = 0.0; - switch (channel) + switch (component) { - case HsvChannel.Hue: + case HsvComponent.Hue: originalValue = originalHsv.H; newValue = ref newHsv.H; incrementAmount = 1; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: originalValue = originalHsv.S; newValue = ref newHsv.S; incrementAmount = 0.01; break; - case HsvChannel.Value: + case HsvComponent.Value: originalValue = originalHsv.V; newValue = ref newHsv.V; incrementAmount = 0.01; break; default: - throw new InvalidOperationException("Invalid HsvChannel."); + throw new InvalidOperationException("Invalid HsvComponent."); } bool shouldFindMidPoint = true; @@ -228,28 +228,28 @@ namespace Avalonia.Controls.Primitives ref double currentValue = ref currentHsv.H; double wrapIncrement = 0; - switch (channel) + switch (component) { - case HsvChannel.Hue: + case HsvComponent.Hue: startValue = ref startHsv.H; currentValue = ref currentHsv.H; wrapIncrement = 360.0; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: startValue = ref startHsv.S; currentValue = ref currentHsv.S; wrapIncrement = 1.0; break; - case HsvChannel.Value: + case HsvComponent.Value: startValue = ref startHsv.V; currentValue = ref currentHsv.V; wrapIncrement = 1.0; break; default: - throw new InvalidOperationException("Invalid HsvChannel."); + throw new InvalidOperationException("Invalid HsvComponent."); } while (newColorName == currentColorName) @@ -316,7 +316,7 @@ namespace Avalonia.Controls.Primitives return newHsv; } - public static double IncrementAlphaChannel( + public static double IncrementAlphaComponent( double originalAlpha, IncrementDirection direction, IncrementAmount amount, diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 71fc887bee..824bf9ab05 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -32,24 +32,24 @@ namespace Avalonia.Controls.Primitives Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); /// - /// Gets or sets the two HSV color channels displayed by the spectrum. + /// Gets or sets the two HSV color components displayed by the spectrum. /// /// /// Internally, the uses the HSV color model. /// - public ColorSpectrumChannels Channels + public ColorSpectrumComponents Components { - get => GetValue(ChannelsProperty); - set => SetValue(ChannelsProperty, value); + get => GetValue(ComponentsProperty); + set => SetValue(ComponentsProperty, value); } /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ChannelsProperty = - AvaloniaProperty.Register( - nameof(Channels), - ColorSpectrumChannels.HueSaturation); + public static readonly StyledProperty ComponentsProperty = + AvaloniaProperty.Register( + nameof(Components), + ColorSpectrumComponents.HueSaturation); /// /// Gets or sets the currently selected color in the HSV color model. @@ -74,7 +74,7 @@ namespace Avalonia.Controls.Primitives new HsvColor(1, 0, 0, 1)); /// - /// Gets or sets the maximum value of the Hue channel in the range from 0..359. + /// Gets or sets the maximum value of the Hue component in the range from 0..359. /// This property must be greater than . /// /// @@ -93,7 +93,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MaxHue), 359); /// - /// Gets or sets the maximum value of the Saturation channel in the range from 0..100. + /// Gets or sets the maximum value of the Saturation component in the range from 0..100. /// This property must be greater than . /// /// @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MaxSaturation), 100); /// - /// Gets or sets the maximum value of the Value channel in the range from 0..100. + /// Gets or sets the maximum value of the Value component in the range from 0..100. /// This property must be greater than . /// /// @@ -131,7 +131,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MaxValue), 100); /// - /// Gets or sets the minimum value of the Hue channel in the range from 0..359. + /// Gets or sets the minimum value of the Hue component in the range from 0..359. /// This property must be less than . /// /// @@ -150,7 +150,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MinHue), 0); /// - /// Gets or sets the minimum value of the Saturation channel in the range from 0..100. + /// Gets or sets the minimum value of the Saturation component in the range from 0..100. /// This property must be less than . /// /// @@ -169,7 +169,7 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.Register(nameof(MinSaturation), 0); /// - /// Gets or sets the minimum value of the Value channel in the range from 0..100. + /// Gets or sets the minimum value of the Value component in the range from 0..100. /// This property must be less than . /// /// diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 6779c54a1e..aecaa88f36 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls.Primitives // in order to function properly while the asynchronous bitmap creation // is in progress. private ColorSpectrumShape _shapeFromLastBitmapCreation = ColorSpectrumShape.Box; - private ColorSpectrumChannels _componentsFromLastBitmapCreation = ColorSpectrumChannels.HueSaturation; + private ColorSpectrumComponents _componentsFromLastBitmapCreation = ColorSpectrumComponents.HueSaturation; private double _imageWidthFromLastBitmapCreation = 0.0; private double _imageHeightFromLastBitmapCreation = 0.0; private int _minHueFromLastBitmapCreation = 0; @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Primitives public ColorSpectrum() { _shapeFromLastBitmapCreation = Shape; - _componentsFromLastBitmapCreation = Channels; + _componentsFromLastBitmapCreation = Components; _imageWidthFromLastBitmapCreation = 0; _imageHeightFromLastBitmapCreation = 0; _minHueFromLastBitmapCreation = MinHue; @@ -219,53 +219,53 @@ namespace Avalonia.Controls.Primitives bool isControlDown = e.KeyModifiers.HasFlag(KeyModifiers.Control); - HsvChannel incrementChannel = HsvChannel.Hue; + HsvComponent incrementComponent = HsvComponent.Hue; bool isSaturationValue = false; if (key == Key.Left || key == Key.Right) { - switch (Channels) + switch (Components) { - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.HueValue: - incrementChannel = HsvChannel.Hue; + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.HueValue: + incrementComponent = HsvComponent.Hue; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: isSaturationValue = true; - goto case ColorSpectrumChannels.SaturationHue; - case ColorSpectrumChannels.SaturationHue: - incrementChannel = HsvChannel.Saturation; + goto case ColorSpectrumComponents.SaturationHue; + case ColorSpectrumComponents.SaturationHue: + incrementComponent = HsvComponent.Saturation; break; - case ColorSpectrumChannels.ValueHue: - case ColorSpectrumChannels.ValueSaturation: - incrementChannel = HsvChannel.Value; + case ColorSpectrumComponents.ValueHue: + case ColorSpectrumComponents.ValueSaturation: + incrementComponent = HsvComponent.Value; break; } } else if (key == Key.Up || key == Key.Down) { - switch (Channels) + switch (Components) { - case ColorSpectrumChannels.SaturationHue: - case ColorSpectrumChannels.ValueHue: - incrementChannel = HsvChannel.Hue; + case ColorSpectrumComponents.SaturationHue: + case ColorSpectrumComponents.ValueHue: + incrementComponent = HsvComponent.Hue; break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.ValueSaturation: - incrementChannel = HsvChannel.Saturation; + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.ValueSaturation: + incrementComponent = HsvComponent.Saturation; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: isSaturationValue = true; - goto case ColorSpectrumChannels.HueValue; - case ColorSpectrumChannels.HueValue: - incrementChannel = HsvChannel.Value; + goto case ColorSpectrumComponents.HueValue; + case ColorSpectrumComponents.HueValue: + incrementComponent = HsvComponent.Value; break; } } @@ -273,19 +273,19 @@ namespace Avalonia.Controls.Primitives double minBound = 0.0; double maxBound = 0.0; - switch (incrementChannel) + switch (incrementComponent) { - case HsvChannel.Hue: + case HsvComponent.Hue: minBound = MinHue; maxBound = MaxHue; break; - case HsvChannel.Saturation: + case HsvComponent.Saturation: minBound = MinSaturation; maxBound = MaxSaturation; break; - case HsvChannel.Value: + case HsvComponent.Value: minBound = MinValue; maxBound = MaxValue; break; @@ -295,8 +295,8 @@ namespace Avalonia.Controls.Primitives // so we want left and up to be lower for hue, but higher for saturation and value. // This will ensure that the icon always moves in the direction of the key press. IncrementDirection direction = - (incrementChannel == HsvChannel.Hue && (key == Key.Left || key == Key.Up)) || - (incrementChannel != HsvChannel.Hue && (key == Key.Right || key == Key.Down)) ? + (incrementComponent == HsvComponent.Hue && (key == Key.Left || key == Key.Up)) || + (incrementComponent != HsvComponent.Hue && (key == Key.Right || key == Key.Down)) ? IncrementDirection.Lower : IncrementDirection.Higher; @@ -320,9 +320,9 @@ namespace Avalonia.Controls.Primitives IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; HsvColor hsvColor = HsvColor; - UpdateColor(ColorHelpers.IncrementColorChannel( + UpdateColor(ColorHelpers.IncrementColorComponent( new Hsv(hsvColor), - incrementChannel, + incrementComponent, direction, amount, shouldWrap: true, @@ -408,12 +408,12 @@ namespace Avalonia.Controls.Primitives throw new ArgumentException("MaxHue must be between 0 and 359."); } - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If hue is one of the axes in the spectrum bitmap, then we'll need to regenerate it // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.SaturationValue && - channels != ColorSpectrumChannels.ValueSaturation) + if (components != ColorSpectrumComponents.SaturationValue && + components != ColorSpectrumComponents.ValueSaturation) { CreateBitmapsAndColorMap(); } @@ -433,12 +433,12 @@ namespace Avalonia.Controls.Primitives throw new ArgumentException("MaxSaturation must be between 0 and 100."); } - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueValue && - channels != ColorSpectrumChannels.ValueHue) + if (components != ColorSpectrumComponents.HueValue && + components != ColorSpectrumComponents.ValueHue) { CreateBitmapsAndColorMap(); } @@ -458,12 +458,12 @@ namespace Avalonia.Controls.Primitives throw new ArgumentException("MaxValue must be between 0 and 100."); } - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If value is one of the axes in the spectrum bitmap, then we'll need to regenerate it // if the maximum or minimum value has changed. - if (channels != ColorSpectrumChannels.HueSaturation && - channels != ColorSpectrumChannels.SaturationHue) + if (components != ColorSpectrumComponents.HueSaturation && + components != ColorSpectrumComponents.SaturationHue) { CreateBitmapsAndColorMap(); } @@ -472,7 +472,7 @@ namespace Avalonia.Controls.Primitives { CreateBitmapsAndColorMap(); } - else if (change.Property == ChannelsProperty) + else if (change.Property == ComponentsProperty) { CreateBitmapsAndColorMap(); } @@ -617,23 +617,22 @@ namespace Avalonia.Controls.Primitives // Note: This can sometimes cause a crash -- possibly due to differences in c# rounding. Therefore, index is now clamped. Hsv hsvAtPoint = _hsvValues[MathUtilities.Clamp((y * width + x), 0, _hsvValues.Count - 1)]; - var channels = Channels; var hsvColor = HsvColor; - switch (channels) + switch (Components) { - case ColorSpectrumChannels.HueValue: - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: hsvAtPoint.S = hsvColor.S; break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: hsvAtPoint.V = hsvColor.V; break; - case ColorSpectrumChannels.ValueSaturation: - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + case ColorSpectrumComponents.SaturationValue: hsvAtPoint.H = hsvColor.H; break; } @@ -681,8 +680,8 @@ namespace Avalonia.Controls.Primitives // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, // we inverted the direction of that axis in order to put more hue on the outside of the ring, // so we need to do similarly here when positioning the ellipse. - if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || - _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) + if (_componentsFromLastBitmapCreation == ColorSpectrumComponents.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumComponents.SaturationHue) { sPercent = 1 - sPercent; } @@ -693,32 +692,32 @@ namespace Avalonia.Controls.Primitives switch (_componentsFromLastBitmapCreation) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: xPercent = hPercent; yPercent = vPercent; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: xPercent = hPercent; yPercent = sPercent; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: xPercent = vPercent; yPercent = hPercent; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: xPercent = vPercent; yPercent = sPercent; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: xPercent = sPercent; yPercent = hPercent; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: xPercent = sPercent; yPercent = vPercent; break; @@ -757,8 +756,8 @@ namespace Avalonia.Controls.Primitives // In the case where saturation was an axis in the spectrum with hue, or value is an axis, full stop, // we inverted the direction of that axis in order to put more hue on the outside of the ring, // so we need to do similarly here when positioning the ellipse. - if (_componentsFromLastBitmapCreation == ColorSpectrumChannels.HueSaturation || - _componentsFromLastBitmapCreation == ColorSpectrumChannels.SaturationHue) + if (_componentsFromLastBitmapCreation == ColorSpectrumComponents.HueSaturation || + _componentsFromLastBitmapCreation == ColorSpectrumComponents.SaturationHue) { sThetaValue = 360 - sThetaValue; sRValue = -sRValue - 1; @@ -771,32 +770,32 @@ namespace Avalonia.Controls.Primitives switch (_componentsFromLastBitmapCreation) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: thetaValue = hThetaValue; rValue = vRValue; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: thetaValue = hThetaValue; rValue = sRValue; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: thetaValue = vThetaValue; rValue = hRValue; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: thetaValue = vThetaValue; rValue = sRValue; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: thetaValue = sThetaValue; rValue = hRValue; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: thetaValue = sThetaValue; rValue = vRValue; break; @@ -932,7 +931,7 @@ namespace Avalonia.Controls.Primitives int minValue = MinValue; int maxValue = MaxValue; ColorSpectrumShape shape = Shape; - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // If min >= max, then by convention, min is the only number that a property can have. if (minHue >= maxHue) @@ -967,8 +966,8 @@ namespace Avalonia.Controls.Primitives bgraMinPixelData.Capacity = pixelDataSize; // We'll only save pixel data for the middle bitmaps if our third dimension is hue. - if (channels == ColorSpectrumChannels.ValueSaturation || - channels == ColorSpectrumChannels.SaturationValue) + if (components == ColorSpectrumComponents.ValueSaturation || + components == ColorSpectrumComponents.SaturationValue) { bgraMiddle1PixelData.Capacity = pixelDataSize; bgraMiddle2PixelData.Capacity = pixelDataSize; @@ -1004,7 +1003,7 @@ namespace Avalonia.Controls.Primitives for (int y = minDimensionInt - 1; y >= 0; --y) { FillPixelForBox( - x, y, hsv, minDimensionInt, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, hsv, minDimensionInt, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1017,7 +1016,7 @@ namespace Avalonia.Controls.Primitives for (int x = 0; x < minDimensionInt; ++x) { FillPixelForRing( - x, y, minDimensionInt / 2.0, hsv, channels, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, minDimensionInt / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1030,24 +1029,24 @@ namespace Avalonia.Controls.Primitives int pixelWidth = (int)Math.Round(minDimension); int pixelHeight = (int)Math.Round(minDimension); - ColorSpectrumChannels channels2 = Channels; + ColorSpectrumComponents components2 = Components; WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData); WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData); - switch (channels2) + switch (components2) { - case ColorSpectrumChannels.HueValue: - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: _saturationMinimumBitmap = minBitmap; _saturationMaximumBitmap = maxBitmap; break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: _valueBitmap = maxBitmap; break; - case ColorSpectrumChannels.ValueSaturation: - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + case ColorSpectrumComponents.SaturationValue: _hueRedBitmap = minBitmap; _hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData); _hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData); @@ -1058,7 +1057,7 @@ namespace Avalonia.Controls.Primitives } _shapeFromLastBitmapCreation = Shape; - _componentsFromLastBitmapCreation = Channels; + _componentsFromLastBitmapCreation = Components; _imageWidthFromLastBitmapCreation = minDimension; _imageHeightFromLastBitmapCreation = minDimension; _minHueFromLastBitmapCreation = MinHue; @@ -1080,7 +1079,7 @@ namespace Avalonia.Controls.Primitives double y, Hsv baseHsv, double minDimension, - ColorSpectrumChannels channels, + ColorSpectrumComponents components, double minHue, double maxHue, double minSaturation, @@ -1112,30 +1111,30 @@ namespace Avalonia.Controls.Primitives double xPercent = (minDimension - 1 - x) / (minDimension - 1); double yPercent = (minDimension - 1 - y) / (minDimension - 1); - switch (channels) + switch (components) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + yPercent * (hMax - hMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + yPercent * (vMax - vMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + xPercent * (sMax - sMin); hsvMin.H = 0; @@ -1146,14 +1145,14 @@ namespace Avalonia.Controls.Primitives hsvMax.H = 300; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + xPercent * (hMax - hMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + yPercent * (sMax - sMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + xPercent * (vMax - vMin); hsvMin.H = 0; @@ -1171,8 +1170,8 @@ namespace Avalonia.Controls.Primitives // so we'll invert the number before assigning the HSL value to the array. // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue // in the case of the ring configuration. - if (channels == ColorSpectrumChannels.HueSaturation || - channels == ColorSpectrumChannels.SaturationHue) + if (components == ColorSpectrumComponents.HueSaturation || + components == ColorSpectrumComponents.SaturationHue) { hsvMin.S = sMax - hsvMin.S + sMin; hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; @@ -1200,8 +1199,8 @@ namespace Avalonia.Controls.Primitives bgraMinPixelData.Add(255); // a - ignored // We'll only save pixel data for the middle bitmaps if our third dimension is hue. - if (channels == ColorSpectrumChannels.ValueSaturation || - channels == ColorSpectrumChannels.SaturationValue) + if (components == ColorSpectrumComponents.ValueSaturation || + components == ColorSpectrumComponents.SaturationValue) { Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255.0)); // b @@ -1240,7 +1239,7 @@ namespace Avalonia.Controls.Primitives double y, double radius, Hsv baseHsv, - ColorSpectrumChannels channels, + ColorSpectrumComponents components, double minHue, double maxHue, double minSaturation, @@ -1298,30 +1297,30 @@ namespace Avalonia.Controls.Primitives double thetaPercent = theta / 360; - switch (channels) + switch (components) { - case ColorSpectrumChannels.HueValue: + case ColorSpectrumComponents.HueValue: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.HueSaturation: + case ColorSpectrumComponents.HueSaturation: hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + thetaPercent * (hMax - hMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.ValueHue: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); hsvMin.S = 0; hsvMax.S = 1; break; - case ColorSpectrumChannels.ValueSaturation: + case ColorSpectrumComponents.ValueSaturation: hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + thetaPercent * (vMax - vMin); hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + r * (sMax - sMin); hsvMin.H = 0; @@ -1332,14 +1331,14 @@ namespace Avalonia.Controls.Primitives hsvMax.H = 300; break; - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.SaturationHue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); hsvMin.H = hsvMiddle1.H = hsvMiddle2.H = hsvMiddle3.H = hsvMiddle4.H = hsvMax.H = hMin + r * (hMax - hMin); hsvMin.V = 0; hsvMax.V = 1; break; - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.SaturationValue: hsvMin.S = hsvMiddle1.S = hsvMiddle2.S = hsvMiddle3.S = hsvMiddle4.S = hsvMax.S = sMin + thetaPercent * (sMax - sMin); hsvMin.V = hsvMiddle1.V = hsvMiddle2.V = hsvMiddle3.V = hsvMiddle4.V = hsvMax.V = vMin + r * (vMax - vMin); hsvMin.H = 0; @@ -1357,8 +1356,8 @@ namespace Avalonia.Controls.Primitives // so we'll invert the number before assigning the HSL value to the array. // Otherwise, we'll have a very narrow section in the middle that actually has meaningful hue // in the case of the ring configuration. - if (channels == ColorSpectrumChannels.HueSaturation || - channels == ColorSpectrumChannels.SaturationHue) + if (components == ColorSpectrumComponents.HueSaturation || + components == ColorSpectrumComponents.SaturationHue) { hsvMin.S = sMax - hsvMin.S + sMin; hsvMiddle1.S = sMax - hsvMiddle1.S + sMin; @@ -1386,8 +1385,8 @@ namespace Avalonia.Controls.Primitives bgraMinPixelData.Add(255); // a // We'll only save pixel data for the middle bitmaps if our third dimension is hue. - if (channels == ColorSpectrumChannels.ValueSaturation || - channels == ColorSpectrumChannels.SaturationValue) + if (components == ColorSpectrumComponents.ValueSaturation || + components == ColorSpectrumComponents.SaturationValue) { Rgb rgbMiddle1 = hsvMiddle1.ToRgb(); bgraMiddle1PixelData.Add((byte)Math.Round(rgbMiddle1.B * 255)); // b @@ -1432,7 +1431,7 @@ namespace Avalonia.Controls.Primitives } HsvColor hsvColor = HsvColor; - ColorSpectrumChannels channels = Channels; + ColorSpectrumComponents components = Components; // We'll set the base image and the overlay image based on which component is our third dimension. // If it's saturation or luminosity, then the base image is that dimension at its minimum value, @@ -1440,10 +1439,10 @@ namespace Avalonia.Controls.Primitives // If it's hue, then we'll figure out where in the color wheel we are, and then use the two // colors on either side of our position as our base image and overlay image. // For example, if our hue is orange, then the base image would be red and the overlay image yellow. - switch (channels) + switch (components) { - case ColorSpectrumChannels.HueValue: - case ColorSpectrumChannels.ValueHue: + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: { if (_saturationMinimumBitmap == null || _saturationMaximumBitmap == null) @@ -1463,8 +1462,8 @@ namespace Avalonia.Controls.Primitives } break; - case ColorSpectrumChannels.HueSaturation: - case ColorSpectrumChannels.SaturationHue: + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: { if (_valueBitmap == null) { @@ -1483,8 +1482,8 @@ namespace Avalonia.Controls.Primitives } break; - case ColorSpectrumChannels.ValueSaturation: - case ColorSpectrumChannels.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + case ColorSpectrumComponents.SaturationValue: { if (_hueRedBitmap == null || _hueYellowBitmap == null || @@ -1554,13 +1553,13 @@ namespace Avalonia.Controls.Primitives // To find how much something contrasts with white, we use the equation // for relative luminance. // - // If the third channel is value, then we won't be updating the spectrum's displayed colors, + // If the third component is value, then we won't be updating the spectrum's displayed colors, // so in that case we should use a value of 1 when considering the backdrop // for the selection ellipse. Color displayedColor; - if (Channels == ColorSpectrumChannels.HueSaturation || - Channels == ColorSpectrumChannels.SaturationHue) + if (Components == ColorSpectrumComponents.HueSaturation || + Components == ColorSpectrumComponents.SaturationHue) { HsvColor hsvColor = HsvColor; Rgb color = (new Hsv(hsvColor.H, hsvColor.S, 1.0)).ToRgb(); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs index d44e6b3a9f..8a425b9581 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs @@ -8,39 +8,39 @@ using Avalonia.Media; namespace Avalonia.Controls.Primitives { /// - /// Contains and allows modification of Hue, Saturation and Value channel values. + /// Contains and allows modification of Hue, Saturation and Value components. /// /// /// The is a specialized struct optimized for permanence and memory: /// /// This is not a read-only struct like and allows editing the fields /// Removes the alpha component unnecessary in core calculations - /// No channel value bounds checks or clamping is done. + /// No component bounds checks or clamping is done. /// /// internal struct Hsv { /// - /// The Hue channel value in the range from 0..359. + /// The Hue component in the range from 0..359. /// public double H; /// - /// The Saturation channel value in the range from 0..1. + /// The Saturation component in the range from 0..1. /// public double S; /// - /// The Value channel value in the range from 0..1. + /// The Value component in the range from 0..1. /// public double V; /// /// Initializes a new instance of the struct. /// - /// The Hue channel value in the range from 0..360. - /// The Saturation channel value in the range from 0..1. - /// The Value channel value in the range from 0..1. + /// The Hue component in the range from 0..360. + /// The Saturation component in the range from 0..1. + /// The Value component in the range from 0..1. public Hsv(double h, double s, double v) { H = h; @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Primitives /// /// Converts this struct into a standard . /// - /// The Alpha channel value in the range from 0..1. + /// The Alpha component in the range from 0..1. /// A new representing this struct. public HsvColor ToHsvColor(double alpha = 1.0) { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs index fd749cd7dc..12aca593d5 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs @@ -6,7 +6,7 @@ namespace Avalonia.Controls.Primitives { /// - /// Defines a relative amount that a color channel should be incremented. + /// Defines a relative amount that a color component should be incremented. /// internal enum IncrementAmount { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs index 8002a44fc9..df9c1e3350 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs @@ -6,7 +6,7 @@ namespace Avalonia.Controls.Primitives { /// - /// Defines the direction a color channel should be incremented. + /// Defines the direction a color component should be incremented. /// internal enum IncrementDirection { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs index 616ab9e9ac..72e3821c2b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs @@ -9,40 +9,40 @@ using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { /// - /// Contains and allows modification of Red, Green and Blue channel values. + /// Contains and allows modification of Red, Green and Blue components. /// /// /// The is a specialized struct optimized for permanence and memory: /// /// This is not a read-only struct like and allows editing the fields /// Removes the alpha component unnecessary in core calculations - /// Normalizes RGB channel values in the range of 0..1 to simplify calculations. - /// No channel value bounds checks or clamping is done. + /// Normalizes RGB components in the range of 0..1 to simplify calculations. + /// No component bounds checks or clamping is done. /// /// internal struct Rgb { /// - /// The Red channel value in the range from 0..1. + /// The Red component in the range from 0..1. /// public double R; /// - /// The Green channel value in the range from 0..1. + /// The Green component in the range from 0..1. /// public double G; /// - /// The Blue channel value in the range from 0..1. + /// The Blue component in the range from 0..1. /// public double B; /// /// Initializes a new instance of the struct. /// - /// The Red channel value in the range from 0..1. - /// The Green channel value in the range from 0..1. - /// The Blue channel value in the range from 0..1. + /// The Red component in the range from 0..1. + /// The Green component in the range from 0..1. + /// The Blue component in the range from 0..1. public Rgb(double r, double g, double b) { R = r; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives /// /// Converts this struct into a standard . /// - /// The Alpha channel value in the range from 0..1. + /// The Alpha component in the range from 0..1. /// A new representing this struct. public Color ToColor(double alpha = 1.0) { diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumChannels.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs similarity index 81% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumChannels.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs index a31586d175..164089096e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrumChannels.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs @@ -8,16 +8,16 @@ using Avalonia.Controls.Primitives; namespace Avalonia.Controls { /// - /// Defines the two HSV color channels displayed by a . + /// Defines the two HSV color components displayed by a . /// /// - /// Order of the color channels is important and correspond with an X/Y axis in Box + /// Order of the color components is important and correspond with an X/Y axis in Box /// shape or a degree/radius in Ring shape. /// - public enum ColorSpectrumChannels + public enum ColorSpectrumComponents { /// - /// The Hue and Value channels. + /// The Hue and Value components. /// /// /// In Box shape, Hue is mapped to the X-axis and Value is mapped to the Y-axis. @@ -26,7 +26,7 @@ namespace Avalonia.Controls HueValue, /// - /// The Value and Hue channels. + /// The Value and Hue components. /// /// /// In Box shape, Value is mapped to the X-axis and Hue is mapped to the Y-axis. @@ -35,7 +35,7 @@ namespace Avalonia.Controls ValueHue, /// - /// The Hue and Saturation channels. + /// The Hue and Saturation components. /// /// /// In Box shape, Hue is mapped to the X-axis and Saturation is mapped to the Y-axis. @@ -44,7 +44,7 @@ namespace Avalonia.Controls HueSaturation, /// - /// The Saturation and Hue channels. + /// The Saturation and Hue components. /// /// /// In Box shape, Saturation is mapped to the X-axis and Hue is mapped to the Y-axis. @@ -53,7 +53,7 @@ namespace Avalonia.Controls SaturationHue, /// - /// The Saturation and Value channels. + /// The Saturation and Value components. /// /// /// In Box shape, Saturation is mapped to the X-axis and Value is mapped to the Y-axis. @@ -62,7 +62,7 @@ namespace Avalonia.Controls SaturationValue, /// - /// The Value and Saturation channels. + /// The Value and Saturation components. /// /// /// In Box shape, Value is mapped to the X-axis and Saturation is mapped to the Y-axis. diff --git a/src/Avalonia.Controls.ColorPicker/HsvChannel.cs b/src/Avalonia.Controls.ColorPicker/HsvChannel.cs deleted file mode 100644 index 18f96fe70b..0000000000 --- a/src/Avalonia.Controls.ColorPicker/HsvChannel.cs +++ /dev/null @@ -1,33 +0,0 @@ -// This source file is adapted from the WinUI project. -// (https://github.com/microsoft/microsoft-ui-xaml) -// -// Licensed to The Avalonia Project under the MIT License. - -namespace Avalonia.Controls -{ - /// - /// Defines a specific HSV color model channel. - /// - public enum HsvChannel - { - /// - /// The Hue channel. - /// - Hue, - - /// - /// The Saturation channel. - /// - Saturation, - - /// - /// The Value channel. - /// - Value, - - /// - /// The Alpha channel. - /// - Alpha - }; -} diff --git a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs new file mode 100644 index 0000000000..1132bd7bbb --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs @@ -0,0 +1,47 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Defines a specific component in the HSV color model. + /// + public enum HsvComponent + { + /// + /// The Hue component. + /// + /// + /// Also see: + /// + Hue, + + /// + /// The Saturation component. + /// + /// + /// Also see: + /// + Saturation, + + /// + /// The Value component. + /// + /// + /// Also see: + /// + Value, + + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha + }; +} From 2a5b2cf90f16418806170a18ed5692600784c7a5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Apr 2022 22:24:32 -0400 Subject: [PATCH 59/59] Use newer GetOldValue method --- .../ColorSpectrum/ColorSpectrum.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index aecaa88f36..fe9a2fac43 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -361,7 +361,7 @@ namespace Avalonia.Controls.Primitives } /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == ColorProperty) { @@ -380,7 +380,7 @@ namespace Avalonia.Controls.Primitives UpdateBitmapSources(); } - _oldColor = change.OldValue.GetValueOrDefault(); + _oldColor = change.GetOldValue(); } else if (change.Property == HsvColorProperty) { @@ -391,7 +391,7 @@ namespace Avalonia.Controls.Primitives SetColor(); } - _oldHsvColor = change.OldValue.GetValueOrDefault(); + _oldHsvColor = change.GetOldValue(); } else if (change.Property == MinHueProperty || change.Property == MaxHueProperty)