From e51116978f5be53489210564317a85d1f360099f Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 21 Oct 2025 19:49:00 +0300 Subject: [PATCH 01/22] Added clamping on properties in HsvColor and HslColor --- .../Helpers/src/ColorHelper/HslColor.cs | 51 ++++++++++++++--- .../Helpers/src/ColorHelper/HsvColor.cs | 57 +++++++++++++++---- 2 files changed, 87 insertions(+), 21 deletions(-) diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index c69dbdeb..7622b85b 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -5,27 +5,60 @@ namespace CommunityToolkit.WinUI; /// -/// Defines a color in Hue/Saturation/Lightness (HSL) space. +/// Defines a color in Hue/Saturation/Lightness (HSL) space with alpha. /// public struct HslColor { + private double _hue; + private double _saturation; + private double _lightness; + private double _alpha; + /// - /// The Hue in 0..360 range. + /// Gets or sets the hue. /// - public double H; + /// + /// This value is clamped between 0 and 360. + /// + public double H + { + readonly get => _hue; + set => _hue = Math.Clamp(value, 0, 360); + } /// - /// The Saturation in 0..1 range. + /// Gets or sets the saturation. /// - public double S; + /// + /// This value is clamped between 0 and 1. + /// + public double S + { + readonly get => _saturation; + set => _saturation = Math.Clamp(value, 0, 1); + } /// - /// The Lightness in 0..1 range. + /// Gets or sets the lightness. /// - public double L; + /// + /// This value is clamped between 0 and 1. + /// + public double L + { + readonly get => _lightness; + set => _lightness = Math.Clamp(value, 0, 1); + } /// - /// The Alpha/opacity in 0..1 range. + /// Gets or sets the alpha/opacity. /// - public double A; + /// + /// This value is clamped between 0 and 1. + /// + public double A + { + readonly get => _alpha; + set => _alpha = Math.Clamp(value, 0, 1); + } } diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index f30f72a6..19ccbfaf 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -5,27 +5,60 @@ namespace CommunityToolkit.WinUI; /// -/// Defines a color in Hue/Saturation/Value (HSV) space. +/// Defines a color in Hue/Saturation/Value (HSV) space with alpha. /// public struct HsvColor { + private double _hue; + private double _saturation; + private double _value; + private double _alpha; + /// - /// The Hue in 0..360 range. + /// Gets or sets the hue. /// - public double H; - + /// + /// This value is clamped between 0 and 360. + /// + public double H + { + readonly get => _hue; + set => _hue = Math.Clamp(value, 0, 360); + } + /// - /// The Saturation in 0..1 range. + /// Gets or sets the saturation. /// - public double S; - + /// + /// This value is clamped between 0 and 1. + /// + public double S + { + readonly get => _saturation; + set => _saturation = Math.Clamp(value, 0, 1); + } + /// - /// The Value in 0..1 range. + /// Gets or sets the color value. /// - public double V; - + /// + /// This value is clamped between 0 and 1. + /// + public double V + { + readonly get => _value; + set => _value = Math.Clamp(value, 0, 1); + } + /// - /// The Alpha/opacity in 0..1 range. + /// Gets or sets the alpha/opacity. /// - public double A; + /// + /// This value is clamped between 0 and 1. + /// + public double A + { + readonly get => _alpha; + set => _alpha = Math.Clamp(value, 0, 1); + } } From 39cc112a462b8fdc7e258ffa6fbee92dd13c22d8 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 21 Oct 2025 20:26:21 +0300 Subject: [PATCH 02/22] Moved HSL and HSV to RGB logic into HslColor and HsvColor --- .../src/ColorHelper/ColorHelper.Obsolete.cs | 34 +++++ .../Helpers/src/ColorHelper/ColorHelper.cs | 136 ++---------------- .../Helpers/src/ColorHelper/HslColor.cs | 40 ++++++ .../Helpers/src/ColorHelper/HsvColor.cs | 40 ++++++ components/Helpers/tests/Test_ColorHelper.cs | 10 +- 5 files changed, 133 insertions(+), 127 deletions(-) create mode 100644 components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs diff --git a/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs b/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs new file mode 100644 index 00000000..350aad67 --- /dev/null +++ b/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +public static partial class ColorHelper +{ + /// + /// Creates a from the specified hue, saturation, lightness, and alpha values. + /// + /// 0..360 range hue + /// 0..1 range saturation + /// 0..1 range lightness + /// 0..1 alpha + /// The created . + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with (Color)HslColor.Create().")] + public static Color FromHsl(double hue, double saturation, double lightness, double alpha = 1.0) + => HslColor.Create(hue, saturation, lightness, alpha); + + /// + /// Creates a from the specified hue, saturation, value, and alpha values. + /// + /// 0..360 range hue + /// 0..1 range saturation + /// 0..1 range value + /// 0..1 alpha + /// The created . + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with (Color)HsvColor.Create().")] + public static Color FromHsv(double hue, double saturation, double value, double alpha = 1.0) + => HsvColor.Create(hue, saturation, value, alpha); +} diff --git a/components/Helpers/src/ColorHelper/ColorHelper.cs b/components/Helpers/src/ColorHelper/ColorHelper.cs index 284d7d88..301f2fd2 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.cs @@ -15,7 +15,7 @@ namespace CommunityToolkit.WinUI.Helpers; /// /// This class provides static helper methods for colors. /// -public static class ColorHelper +public static partial class ColorHelper { /// /// Creates a from a XAML color string. @@ -187,7 +187,7 @@ public static HslColor ToHsl(this Color color) double lightness = 0.5 * (max + min); double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); - HslColor ret; + HslColor ret = default; ret.H = 60 * h1; ret.S = saturation; ret.L = lightness; @@ -231,7 +231,7 @@ public static HsvColor ToHsv(this Color color) } double saturation = chroma == 0 ? 0 : chroma / max; - HsvColor ret; + HsvColor ret = default; ret.H = 60 * h1; ret.S = saturation; ret.V = max; @@ -239,129 +239,21 @@ public static HsvColor ToHsv(this Color color) return ret; } - /// - /// Creates a from the specified hue, saturation, lightness, and alpha values. - /// - /// 0..360 range hue - /// 0..1 range saturation - /// 0..1 range lightness - /// 0..1 alpha - /// The created . - public static Color FromHsl(double hue, double saturation, double lightness, double alpha = 1.0) + internal static Color FromHueChroma(double h1, double chroma, double x, double m, double alpha) { - if (hue < 0 || hue > 360) - { - throw new ArgumentOutOfRangeException(nameof(hue)); - } + // This code is shared between both the conversion of + // both HSL and HSV to RGB. - double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation; - double h1 = hue / 60; - double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); - double m = lightness - (0.5 * chroma); double r1, g1, b1; - - if (h1 < 1) + (r1, g1, b1) = h1 switch { - r1 = chroma; - g1 = x; - b1 = 0; - } - else if (h1 < 2) - { - r1 = x; - g1 = chroma; - b1 = 0; - } - else if (h1 < 3) - { - r1 = 0; - g1 = chroma; - b1 = x; - } - else if (h1 < 4) - { - r1 = 0; - g1 = x; - b1 = chroma; - } - else if (h1 < 5) - { - r1 = x; - g1 = 0; - b1 = chroma; - } - else - { - r1 = chroma; - g1 = 0; - b1 = x; - } - - byte r = (byte)(255 * (r1 + m)); - byte g = (byte)(255 * (g1 + m)); - byte b = (byte)(255 * (b1 + m)); - byte a = (byte)(255 * alpha); - - return Color.FromArgb(a, r, g, b); - } - - /// - /// Creates a from the specified hue, saturation, value, and alpha values. - /// - /// 0..360 range hue - /// 0..1 range saturation - /// 0..1 range value - /// 0..1 alpha - /// The created . - public static Color FromHsv(double hue, double saturation, double value, double alpha = 1.0) - { - if (hue < 0 || hue > 360) - { - throw new ArgumentOutOfRangeException(nameof(hue)); - } - - double chroma = value * saturation; - double h1 = hue / 60; - double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); - double m = value - chroma; - double r1, g1, b1; - - if (h1 < 1) - { - r1 = chroma; - g1 = x; - b1 = 0; - } - else if (h1 < 2) - { - r1 = x; - g1 = chroma; - b1 = 0; - } - else if (h1 < 3) - { - r1 = 0; - g1 = chroma; - b1 = x; - } - else if (h1 < 4) - { - r1 = 0; - g1 = x; - b1 = chroma; - } - else if (h1 < 5) - { - r1 = x; - g1 = 0; - b1 = chroma; - } - else - { - r1 = chroma; - g1 = 0; - b1 = x; - } + < 1 => (chroma, x, 0d), + < 2 => (x, chroma, 0d), + < 3 => (0d, chroma, x), + < 4 => (0d, x, chroma), + < 5 => (x, 0d, chroma), + _ => (chroma, 0d, x), + }; byte r = (byte)(255 * (r1 + m)); byte g = (byte)(255 * (g1 + m)); diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index 7622b85b..7f6e09c1 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Windows.UI; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; + namespace CommunityToolkit.WinUI; /// @@ -14,6 +17,24 @@ public struct HslColor private double _lightness; private double _alpha; + /// + /// Creates a new . + /// + /// The color's hue. + /// The color's saturation. + /// The color's lightness. + /// The alpha/opacity. + /// The new . + public static HslColor Create(double hue, double saturation, double lightness, double alpha = 1) + { + HslColor color = default; + color.H = hue; + color.S = saturation; + color.L = lightness; + color.A = alpha; + return color; + } + /// /// Gets or sets the hue. /// @@ -61,4 +82,23 @@ public double A readonly get => _alpha; set => _alpha = Math.Clamp(value, 0, 1); } + + /// + /// Converts the to a . + /// + /// The as a . + public readonly Color ToColor() + { + double chroma = (1 - Math.Abs((2 * L) - 1)) * S; + double h1 = H / 60; + double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); + double m = L - (0.5 * chroma); + + return ColorHelper.FromHueChroma(h1, chroma, x, m, A); + } + + /// + /// Cast a to a . + /// + public static implicit operator Color(HslColor hsl) => hsl.ToColor(); } diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index 19ccbfaf..92f85278 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Windows.UI; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; + namespace CommunityToolkit.WinUI; /// @@ -13,6 +16,24 @@ public struct HsvColor private double _saturation; private double _value; private double _alpha; + + /// + /// Creates a new . + /// + /// The color's hue. + /// The color's saturation. + /// The color's value. + /// The alpha/opacity. + /// The new . + public static HsvColor Create(double hue, double saturation, double value, double alpha = 1) + { + HsvColor color = default; + color.H = hue; + color.S = saturation; + color.V = value; + color.A = alpha; + return color; + } /// /// Gets or sets the hue. @@ -61,4 +82,23 @@ public double A readonly get => _alpha; set => _alpha = Math.Clamp(value, 0, 1); } + + /// + /// Converts the to a . + /// + /// The as a . + public readonly Color ToColor() + { + double chroma = V * S; + double h1 = H / 60; + double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); + double m = V - chroma; + + return ColorHelper.FromHueChroma(h1, chroma, x, m, A); + } + + /// + /// Cast a to a . + /// + public static implicit operator Color(HsvColor hsv) => hsv.ToColor(); } diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index 94821501..a667f49f 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -69,7 +69,7 @@ public void Test_ColorHelper_ToInt() [TestMethod] public void Test_ColorHelper_ToHsl() { - HslColor hslRed; + HslColor hslRed = default; hslRed.A = 1.0; // Alpha hslRed.H = 0.0; // Hue hslRed.S = 1.0; // Saturation @@ -82,7 +82,7 @@ public void Test_ColorHelper_ToHsl() [TestMethod] public void Test_ColorHelper_ToHsl_White() { - HslColor hslWhite; + HslColor hslWhite = default; hslWhite.A = 1.0; // Alpha hslWhite.H = 0.0; // Hue hslWhite.S = 0.0; // Saturation @@ -96,7 +96,7 @@ public void Test_ColorHelper_ToHsl_White() public void Test_ColorHelper_ToHsl_MaxR() { // Test when given an RGB value where R is the max value. - HslColor hslColor; + HslColor hslColor = default; hslColor.A = 1.0; // Alpha hslColor.H = 330.0; // Hue hslColor.S = 1.0; // Saturation @@ -114,7 +114,7 @@ public void Test_ColorHelper_ToHsl_MaxR() [TestMethod] public void Test_ColorHelper_ToHsv() { - HsvColor hsvColor; + HsvColor hsvColor = default; hsvColor.A = 1.0; // Alpha hsvColor.H = 100; // Hue hsvColor.S = 0.25; // Saturation @@ -136,7 +136,7 @@ public void Test_ColorHelper_ToHsv() [TestMethod] public void Test_ColorHelper_ToHsv_MaxR() { - HsvColor hsvColor; + HsvColor hsvColor = default; hsvColor.A = 1.0; // Alpha hsvColor.H = 330; // Hue hsvColor.S = 0.58823529; // Saturation From 6ca048f902ee079b772e347af6315efb65f53a46 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 21 Oct 2025 20:59:22 +0300 Subject: [PATCH 03/22] Moved HSL and HSV from RGB logic into HslColor and HsvColor --- .../Helpers/src/ColorHelper/ColorHelper.cs | 86 ++++++------------- .../Helpers/src/ColorHelper/HslColor.cs | 25 ++++++ .../Helpers/src/ColorHelper/HsvColor.cs | 31 ++++++- 3 files changed, 78 insertions(+), 64 deletions(-) diff --git a/components/Helpers/src/ColorHelper/ColorHelper.cs b/components/Helpers/src/ColorHelper/ColorHelper.cs index 301f2fd2..ec0cd983 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Globalization; + #if WINAPPSDK using Microsoft.UI; #else @@ -155,94 +156,57 @@ public static int ToInt(this Color color) /// /// The to convert. /// The converted . - public static HslColor ToHsl(this Color color) - { - const double toDouble = 1.0 / 255; - var r = toDouble * color.R; - var g = toDouble * color.G; - var b = toDouble * color.B; - var max = Math.Max(Math.Max(r, g), b); - var min = Math.Min(Math.Min(r, g), b); - var chroma = max - min; - double h1; - - if (chroma == 0) - { - h1 = 0; - } - else if (max == r) - { - // The % operator doesn't do proper modulo on negative - // numbers, so we'll add 6 before using it - h1 = (((g - b) / chroma) + 6) % 6; - } - else if (max == g) - { - h1 = 2 + ((b - r) / chroma); - } - else - { - h1 = 4 + ((r - g) / chroma); - } - - double lightness = 0.5 * (max + min); - double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); - HslColor ret = default; - ret.H = 60 * h1; - ret.S = saturation; - ret.L = lightness; - ret.A = toDouble * color.A; - return ret; - } + public static HslColor ToHsl(this Color color) => (HslColor)color; /// /// Converts a to an . /// /// The to convert. /// The converted . - public static HsvColor ToHsv(this Color color) + public static HsvColor ToHsv(this Color color) => (HsvColor)color; + + internal static (double h1, double chroma) CalculateHueAndChroma(Color color, out double min, out double max, out double alpha) { - const double toDouble = 1.0 / 255; - var r = toDouble * color.R; - var g = toDouble * color.G; - var b = toDouble * color.B; - var max = Math.Max(Math.Max(r, g), b); - var min = Math.Min(Math.Min(r, g), b); + // This code is shared between both the conversion + // to both HSL and HSV from RGB. + + var r = (double)color.R / 255; + var g = (double)color.G / 255; + var b = (double)color.B / 255; + alpha = (double)color.A / 255; + + max = Math.Max(Math.Max(r, g), b); + min = Math.Min(Math.Min(r, g), b); var chroma = max - min; - double h1; + double h1; if (chroma == 0) { + // No max h1 = 0; } else if (max == r) { - // The % operator doesn't do proper modulo on negative - // numbers, so we'll add 6 before using it - h1 = (((g - b) / chroma) + 6) % 6; - } - else if (max == g) + // Red is max + h1 = ((g - b) / chroma + 6) % 6; + } else if (max == g) { + // Green is max h1 = 2 + ((b - r) / chroma); } else { + // Blue is max h1 = 4 + ((r - g) / chroma); } - double saturation = chroma == 0 ? 0 : chroma / max; - HsvColor ret = default; - ret.H = 60 * h1; - ret.S = saturation; - ret.V = max; - ret.A = toDouble * color.A; - return ret; + return (h1, chroma); } internal static Color FromHueChroma(double h1, double chroma, double x, double m, double alpha) { - // This code is shared between both the conversion of - // both HSL and HSV to RGB. + // This code is shared between both the conversion + // from both HSL and HSV to RGB. double r1, g1, b1; (r1, g1, b1) = h1 switch diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index 7f6e09c1..98756201 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -17,6 +17,26 @@ public struct HslColor private double _lightness; private double _alpha; + /// + /// Initializes a new instance of the struct. + /// + /// The to convert to a . + private HslColor(Color color) + { + // Calculate hue, chroma, and supplementary values + (double h1, double chroma) = ColorHelper.CalculateHueAndChroma(color, out var min, out var max, out var alpha); + + // Calculate saturation and lightness + double lightness = 0.5 * (max + min); + double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); + + // Set hsl properties + H = 60 * h1; + S = saturation; + L = lightness; + A = alpha; + } + /// /// Creates a new . /// @@ -101,4 +121,9 @@ public readonly Color ToColor() /// Cast a to a . /// public static implicit operator Color(HslColor hsl) => hsl.ToColor(); + + /// + /// Cast a to + /// + public static explicit operator HslColor(Color color) => new(color); } diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index 92f85278..4204ff9e 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -17,6 +17,26 @@ public struct HsvColor private double _value; private double _alpha; + /// + /// Initializes a new instance of the struct. + /// + /// The to convert to a . + private HsvColor(Color color) + { + // Calculate hue, chroma, and supplementary values + (double h1, double chroma) = ColorHelper.CalculateHueAndChroma(color, out var _, out var max, out var alpha); + + // Calculate saturation and value + double saturation = chroma == 0 ? 0 : chroma / max; + double value = max; + + // Set hsv properties + H = 60 * h1; + S = saturation; + V = value; + A = alpha; + } + /// /// Creates a new . /// @@ -84,9 +104,9 @@ public double A } /// - /// Converts the to a . + /// Converts the to a . /// - /// The as a . + /// The as a . public readonly Color ToColor() { double chroma = V * S; @@ -98,7 +118,12 @@ public readonly Color ToColor() } /// - /// Cast a to a . + /// Cast a to a . /// public static implicit operator Color(HsvColor hsv) => hsv.ToColor(); + + /// + /// Cast a to + /// + public static explicit operator HsvColor(Color color) => new(color); } From d33117bbd1a99ce0e564bc04e5366e0744a48da6 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 21 Oct 2025 21:15:20 +0300 Subject: [PATCH 04/22] Marked final obsolete functions and split ColorHelper with ColorExtensions --- .../src/ColorHelper/ColorExtensions.cs | 39 +++++++++++++++++++ .../src/ColorHelper/ColorHelper.Obsolete.cs | 17 ++++++++ .../Helpers/src/ColorHelper/ColorHelper.cs | 38 +----------------- 3 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 components/Helpers/src/ColorHelper/ColorExtensions.cs diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs new file mode 100644 index 00000000..8bc6f05f --- /dev/null +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Windows.UI; + +namespace CommunityToolkit.WinUI.Helpers; + +/// +/// A class containing extensions methods for +/// +public static class ColorExtensions +{ + /// + /// Converts a to a premultiplied Int32 - 4 byte ARGB structure. + /// + /// The color to convert. + /// The int representation of the color. + public static int ToInt(this Color color) + { + var a = color.A + 1; + var col = (color.A << 24) | ((byte)((color.R * a) >> 8) << 16) | ((byte)((color.G * a) >> 8) << 8) | (byte)((color.B * a) >> 8); + return col; + } + + /// + /// Converts a to an . + /// + /// The to convert. + /// The converted . + public static HslColor ToHsl(this Color color) => (HslColor)color; + + /// + /// Converts a to an . + /// + /// The to convert. + /// The converted . + public static HsvColor ToHsv(this Color color) => (HsvColor)color; +} diff --git a/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs b/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs index 350aad67..77b96dd4 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.Obsolete.cs @@ -8,6 +8,15 @@ namespace CommunityToolkit.WinUI.Helpers; public static partial class ColorHelper { + /// + /// Creates a from a XAML color string. + /// Any format used in XAML should work. + /// + /// The XAML color string. + /// The created . + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with ColorHelper.ParseColor(string colorString).")] + public static Color ToColor(this string colorString) => ParseColor(colorString); + /// /// Creates a from the specified hue, saturation, lightness, and alpha values. /// @@ -31,4 +40,12 @@ public static Color FromHsl(double hue, double saturation, double lightness, dou [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with (Color)HsvColor.Create().")] public static Color FromHsv(double hue, double saturation, double value, double alpha = 1.0) => HsvColor.Create(hue, saturation, value, alpha); + + /// + /// Converts a to a hexadecimal string representation. + /// + /// The color to convert. + /// The hexadecimal string representation of the color. + [Obsolete("This method is marked deprecated, and will be removed in a future version. Please replace with .ToString().")] + public static string ToHex(this Color color) => color.ToString(); } diff --git a/components/Helpers/src/ColorHelper/ColorHelper.cs b/components/Helpers/src/ColorHelper/ColorHelper.cs index ec0cd983..db214741 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.cs @@ -24,7 +24,7 @@ public static partial class ColorHelper /// /// The XAML color string. /// The created . - public static Color ToColor(this string colorString) + public static Color ParseColor(string colorString) { if (string.IsNullOrEmpty(colorString)) { @@ -129,42 +129,6 @@ public static Color ToColor(this string colorString) static Color ThrowFormatException() => throw new FormatException("The parameter \"colorString\" is not a recognized Color format."); } - /// - /// Converts a to a hexadecimal string representation. - /// - /// The color to convert. - /// The hexadecimal string representation of the color. - public static string ToHex(this Color color) - { - return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; - } - - /// - /// Converts a to a premultiplied Int32 - 4 byte ARGB structure. - /// - /// The color to convert. - /// The int representation of the color. - public static int ToInt(this Color color) - { - var a = color.A + 1; - var col = (color.A << 24) | ((byte)((color.R * a) >> 8) << 16) | ((byte)((color.G * a) >> 8) << 8) | (byte)((color.B * a) >> 8); - return col; - } - - /// - /// Converts a to an . - /// - /// The to convert. - /// The converted . - public static HslColor ToHsl(this Color color) => (HslColor)color; - - /// - /// Converts a to an . - /// - /// The to convert. - /// The converted . - public static HsvColor ToHsv(this Color color) => (HsvColor)color; - internal static (double h1, double chroma) CalculateHueAndChroma(Color color, out double min, out double max, out double alpha) { // This code is shared between both the conversion From 88decd4008714206a186e012fe97364d869fe640 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 21 Oct 2025 21:53:59 +0300 Subject: [PATCH 05/22] Added ToString methods on HsvColor and HslColor, and explicit cast between them --- .../Helpers/src/ColorHelper/ColorHelper.cs | 86 +++++++------------ .../Helpers/src/ColorHelper/HslColor.cs | 8 ++ .../Helpers/src/ColorHelper/HsvColor.cs | 8 ++ components/Helpers/tests/Test_ColorHelper.cs | 14 +++ 4 files changed, 61 insertions(+), 55 deletions(-) diff --git a/components/Helpers/src/ColorHelper/ColorHelper.cs b/components/Helpers/src/ColorHelper/ColorHelper.cs index db214741..c56bc3cd 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.cs @@ -33,59 +33,35 @@ public static Color ParseColor(string colorString) if (colorString[0] == '#') { - switch (colorString.Length) + var cuint = Convert.ToUInt32(colorString[1..], 16); + + // 4 bytes + byte b3 = (byte)(cuint >> 24); + byte b2 = (byte)((cuint >> 16) & 0xff); + byte b1 = (byte)((cuint >> 8) & 0xff); + byte b0 = (byte)(cuint & 0xff); + + // 4 half-bytes + byte h3 = (byte)(cuint >> 12); + byte h2 = (byte)((cuint >> 8) & 0xf); + byte h1 = (byte)((cuint >> 4) & 0xf); + byte h0 = (byte)(cuint & 0xf); + h3 = (byte)(h3 << 4 | h3); + h2 = (byte)(h2 << 4 | h2); + h1 = (byte)(h1 << 4 | h1); + h0 = (byte)(h0 << 4 | h0); + + byte r, g, b, a; + (a, r, g, b) = colorString.Length switch { - case 9: - { - var cuint = Convert.ToUInt32(colorString.Substring(1), 16); - var a = (byte)(cuint >> 24); - var r = (byte)((cuint >> 16) & 0xff); - var g = (byte)((cuint >> 8) & 0xff); - var b = (byte)(cuint & 0xff); - - return Color.FromArgb(a, r, g, b); - } - - case 7: - { - var cuint = Convert.ToUInt32(colorString.Substring(1), 16); - var r = (byte)((cuint >> 16) & 0xff); - var g = (byte)((cuint >> 8) & 0xff); - var b = (byte)(cuint & 0xff); - - return Color.FromArgb(255, r, g, b); - } - - case 5: - { - var cuint = Convert.ToUInt16(colorString.Substring(1), 16); - var a = (byte)(cuint >> 12); - var r = (byte)((cuint >> 8) & 0xf); - var g = (byte)((cuint >> 4) & 0xf); - var b = (byte)(cuint & 0xf); - a = (byte)(a << 4 | a); - r = (byte)(r << 4 | r); - g = (byte)(g << 4 | g); - b = (byte)(b << 4 | b); - - return Color.FromArgb(a, r, g, b); - } - - case 4: - { - var cuint = Convert.ToUInt16(colorString.Substring(1), 16); - var r = (byte)((cuint >> 8) & 0xf); - var g = (byte)((cuint >> 4) & 0xf); - var b = (byte)(cuint & 0xf); - r = (byte)(r << 4 | r); - g = (byte)(g << 4 | g); - b = (byte)(b << 4 | b); - - return Color.FromArgb(255, r, g, b); - } - - default: return ThrowFormatException(); - } + 9 => (b3, b2, b1, b0), + 7 => ((byte)255, b2, b1, b0), + 5 => (h3, h2, h1, h0), + 4 => ((byte)255, h2, h1, h0), + _ => ThrowFormatException<(byte, byte, byte, byte)>(), + }; + + return Color.FromArgb(a, r, g, b); } if (colorString.Length > 3 && colorString[0] == 's' && colorString[1] == 'c' && colorString[2] == '#') @@ -111,7 +87,7 @@ public static Color ParseColor(string colorString) return Color.FromArgb(255, (byte)(scR * 255), (byte)(scG * 255), (byte)(scB * 255)); } - return ThrowFormatException(); + return ThrowFormatException(); } var prop = typeof(Colors).GetTypeInfo().GetDeclaredProperty(colorString); @@ -123,10 +99,10 @@ public static Color ParseColor(string colorString) #pragma warning restore CS8605 // Unboxing a possibly null value. } - return ThrowFormatException(); + return ThrowFormatException(); static void ThrowArgumentException() => throw new ArgumentException("The parameter \"colorString\" must not be null or empty."); - static Color ThrowFormatException() => throw new FormatException("The parameter \"colorString\" is not a recognized Color format."); + static T ThrowFormatException() => throw new FormatException("The parameter \"colorString\" is not a recognized Color format."); } internal static (double h1, double chroma) CalculateHueAndChroma(Color color, out double min, out double max, out double alpha) diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index 98756201..b1ada913 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -117,6 +117,9 @@ public readonly Color ToColor() return ColorHelper.FromHueChroma(h1, chroma, x, m, A); } + /// + public override readonly string ToString() => $"hsl({H:N0}, {S}, {L})"; + /// /// Cast a to a . /// @@ -126,4 +129,9 @@ public readonly Color ToColor() /// Cast a to /// public static explicit operator HslColor(Color color) => new(color); + + /// + /// Cast a to + /// + public static explicit operator HslColor(HsvColor color) => new(color); } diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index 4204ff9e..1324f29f 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -117,6 +117,9 @@ public readonly Color ToColor() return ColorHelper.FromHueChroma(h1, chroma, x, m, A); } + /// + public override readonly string ToString() => $"hsv({H:N0}, {S}, {V})"; + /// /// Cast a to a . /// @@ -126,4 +129,9 @@ public readonly Color ToColor() /// Cast a to /// public static explicit operator HsvColor(Color color) => new(color); + + /// + /// Cast a to + /// + public static explicit operator HsvColor(HslColor color) => new(color); } diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index a667f49f..97040e78 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -58,6 +58,20 @@ public void Test_ColorHelper_ToHex() Assert.AreEqual(Colors.Red.ToHex(), "#FFFF0000"); } + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_HslToString() + { + Assert.AreEqual(((HslColor)Colors.Red).ToString(), "hsl(0, 1, 0.5)"); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_HsvToString() + { + Assert.AreEqual(((HsvColor)Colors.Red).ToString(), "hsv(0, 1, 1)"); + } + [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToInt() From 0a2ce89af8f29dcdb902b50b6da87913cb169394 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 21 Oct 2025 21:58:53 +0300 Subject: [PATCH 06/22] Replaced deprecated API usage with new API usage in ColorHelper tests --- components/Helpers/tests/Test_ColorHelper.cs | 43 +++++++++++--------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index 97040e78..bacf1c40 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.WinUI.Helpers; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; namespace HelpersTests; @@ -13,49 +14,49 @@ public class Test_ColorHelper [TestMethod] public void Test_ColorHelper_ToColor_Predifined() { - Assert.AreEqual("Red".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("Red"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToColor_Hex8Digits() { - Assert.AreEqual("#FFFF0000".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("#FFFF0000"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToColor_Hex6Digits() { - Assert.AreEqual("#FF0000".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("#FF0000"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToColor_Hex4Digits() { - Assert.AreEqual("#FF00".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("#FF00"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToColor_Hex3Digits() { - Assert.AreEqual("#F00".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("#F00"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToColor_ScreenColor() { - Assert.AreEqual("sc#1.0,1.0,0,0".ToColor(), Colors.Red); + Assert.AreEqual(ColorHelper.ParseColor("sc#1.0,1.0,0,0"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToHex() { - Assert.AreEqual(Colors.Red.ToHex(), "#FFFF0000"); + Assert.AreEqual(Colors.Red.ToString(), "#FFFF0000"); } [TestCategory("Helpers")] @@ -128,11 +129,13 @@ public void Test_ColorHelper_ToHsl_MaxR() [TestMethod] public void Test_ColorHelper_ToHsv() { - HsvColor hsvColor = default; - hsvColor.A = 1.0; // Alpha - hsvColor.H = 100; // Hue - hsvColor.S = 0.25; // Saturation - hsvColor.V = 0.80; // Value + HsvColor hsvColor = new HsvColor + { + A = 1.0, // Alpha + H = 100, // Hue + S = 0.25, // Saturation + V = 0.80, // Value + }; // Use a test color with non-zero/non-max values for both RGB and HSV var color = Windows.UI.Color.FromArgb(255, 170, 204, 153).ToHsv(); @@ -150,11 +153,13 @@ public void Test_ColorHelper_ToHsv() [TestMethod] public void Test_ColorHelper_ToHsv_MaxR() { - HsvColor hsvColor = default; - hsvColor.A = 1.0; // Alpha - hsvColor.H = 330; // Hue - hsvColor.S = 0.58823529; // Saturation - hsvColor.V = 1; // Value + HsvColor hsvColor = new HsvColor + { + A = 1.0, // Alpha + H = 330, // Hue + S = 0.58823529, // Saturation + V = 1, // Value + }; // Use a test color with non-zero/non-max values for both RGB and HSV var color = Windows.UI.Color.FromArgb(255, 255, 105, 180).ToHsv(); @@ -172,13 +177,13 @@ public void Test_ColorHelper_ToHsv_MaxR() [TestMethod] public void Test_ColorHelper_FromHsl() { - Assert.AreEqual(CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsl(0.0, 1.0, 0.5), Colors.Red); + Assert.AreEqual(HslColor.Create(0.0, 1.0, 0.5), Colors.Red); } [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_FromHsv() { - Assert.AreEqual(CommunityToolkit.WinUI.Helpers.ColorHelper.FromHsv(0.0, 1.0, 1.0), Colors.Red); + Assert.AreEqual(HsvColor.Create(0.0, 1.0, 1.0), Colors.Red); } } From 2bdfcd24c2112cb57bca4347505e71604dea2207 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Wed, 22 Oct 2025 07:08:09 +0300 Subject: [PATCH 07/22] Split ParseColor into various child methods for different formats --- .../Helpers/src/ColorHelper/ColorHelper.cs | 263 +++++++++++++----- 1 file changed, 195 insertions(+), 68 deletions(-) diff --git a/components/Helpers/src/ColorHelper/ColorHelper.cs b/components/Helpers/src/ColorHelper/ColorHelper.cs index c56bc3cd..2be98252 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.cs @@ -19,90 +19,216 @@ namespace CommunityToolkit.WinUI.Helpers; public static partial class ColorHelper { /// - /// Creates a from a XAML color string. - /// Any format used in XAML should work. + /// Attempts to parse a string as a color. /// - /// The XAML color string. - /// The created . - public static Color ParseColor(string colorString) + /// + /// Any format used in XAML should work. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseColor(string colorString, out Color color) { + color = default; + if (string.IsNullOrEmpty(colorString)) + return false; + + // Try as hex + if (TryParseHexColor(colorString, out color)) + return true; + + // Try as screen color + if (TryParseScreenColor(colorString, out color)) + return true; + + if (TryParseColorName(colorString, out color)) + return true; + + return false; + } + + /// + /// Attempts to parse a string as a hex color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseHexColor(string hexString, out Color color) + { + color = default; + + // Ensure string begins with '#' + // and is long enough to not break later in the parser. + if (string.IsNullOrEmpty(hexString) || + hexString.Length < 2 || + hexString[0] != '#') + return false; + + // Convert base 16 string to uint + if(!uint.TryParse(hexString[1..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cuint)) + return false; + + // Extract 4 bytes + byte b3 = (byte)(cuint >> 24); + byte b2 = (byte)((cuint >> 16) & 0xff); + byte b1 = (byte)((cuint >> 8) & 0xff); + byte b0 = (byte)(cuint & 0xff); + + // Extract 4 half-bytes + byte h3 = (byte)(cuint >> 12); + byte h2 = (byte)((cuint >> 8) & 0xf); + byte h1 = (byte)((cuint >> 4) & 0xf); + byte h0 = (byte)(cuint & 0xf); + h3 = (byte)(h3 << 4 | h3); + h2 = (byte)(h2 << 4 | h2); + h1 = (byte)(h1 << 4 | h1); + h0 = (byte)(h0 << 4 | h0); + + // Select bytes based on string length + switch (hexString.Length) { - ThrowArgumentException(); + // #AaRrGgBb + case 9: + color = Color.FromArgb(b3, b2, b1, b0); + return true; + // #RrGgBb + case 7: + color = Color.FromArgb(255, b2, b1, b0); + return true; + // #ARGB + case 5: + color = Color.FromArgb(h3, h2, h1, h0); + return true; + // #RGB + case 4: + color = Color.FromArgb(255, h2, h1, h0); + return true; + // Invalid format + default: + return false; } + } + + /// + /// Attempts to parse a string as a screen color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseScreenColor(string screenColor, out Color color) + { + color = default; + + // Ensure the string begins with "sc#" + if (screenColor.Length < 3 || screenColor[0..3] != "sc#") + return false; - if (colorString[0] == '#') + // Get arguments + screenColor = screenColor[3..]; + var values = screenColor.Split(','); + + // Parse the arguments from string doubles into bytes + var args = new byte[values.Length]; + for (int i = 0; i < values.Length; i++) { - var cuint = Convert.ToUInt32(colorString[1..], 16); - - // 4 bytes - byte b3 = (byte)(cuint >> 24); - byte b2 = (byte)((cuint >> 16) & 0xff); - byte b1 = (byte)((cuint >> 8) & 0xff); - byte b0 = (byte)(cuint & 0xff); - - // 4 half-bytes - byte h3 = (byte)(cuint >> 12); - byte h2 = (byte)((cuint >> 8) & 0xf); - byte h1 = (byte)((cuint >> 4) & 0xf); - byte h0 = (byte)(cuint & 0xf); - h3 = (byte)(h3 << 4 | h3); - h2 = (byte)(h2 << 4 | h2); - h1 = (byte)(h1 << 4 | h1); - h0 = (byte)(h0 << 4 | h0); - - byte r, g, b, a; - (a, r, g, b) = colorString.Length switch - { - 9 => (b3, b2, b1, b0), - 7 => ((byte)255, b2, b1, b0), - 5 => (h3, h2, h1, h0), - 4 => ((byte)255, h2, h1, h0), - _ => ThrowFormatException<(byte, byte, byte, byte)>(), - }; - - return Color.FromArgb(a, r, g, b); + if (!double.TryParse(values[i], out var arg)) + return false; + + args[i] = (byte)(arg * 255); } - if (colorString.Length > 3 && colorString[0] == 's' && colorString[1] == 'c' && colorString[2] == '#') + // Assign channel data based on arguments + switch (args.Length) + { + // sc#Alpha,Red,Green,Blue + case 4: + color = Color.FromArgb(args[0], args[1], args[2], args[3]); + return true; + // sc#Red,Green,Blue + case 3: + color = Color.FromArgb(255, args[0], args[1], args[2]); + return true; + // Invalid format + default: + return false; + } + } + + /// + /// Attempts to parse a string color name. + /// + /// The color name. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseColorName(string colorName, out Color color) + { + color = default; + +#pragma warning disable CS8605 // Unboxing a possibly null value. + var prop = typeof(Colors).GetTypeInfo().GetDeclaredProperty(colorName); + if (prop != null) { - var values = colorString.Split(','); + color = (Color)prop.GetValue(null); + return true; + } +#pragma warning restore CS8605 // Unboxing a possibly null value. - if (values.Length == 4) - { - var scA = double.Parse(values[0].Substring(3), CultureInfo.InvariantCulture); - var scR = double.Parse(values[1], CultureInfo.InvariantCulture); - var scG = double.Parse(values[2], CultureInfo.InvariantCulture); - var scB = double.Parse(values[3], CultureInfo.InvariantCulture); + return false; + } - return Color.FromArgb((byte)(scA * 255), (byte)(scR * 255), (byte)(scG * 255), (byte)(scB * 255)); - } + /// + /// Creates a from a XAML color string. + /// Any format used in XAML should work. + /// + /// The XAML color string. + /// The created . + public static Color ParseColor(string colorString) + { + if (TryParseColor(colorString, out var color)) + return color; - if (values.Length == 3) - { - var scR = double.Parse(values[0].Substring(3), CultureInfo.InvariantCulture); - var scG = double.Parse(values[1], CultureInfo.InvariantCulture); - var scB = double.Parse(values[2], CultureInfo.InvariantCulture); + throw new FormatException($"The string '{colorString}' could not be parsed as a string."); + } - return Color.FromArgb(255, (byte)(scR * 255), (byte)(scG * 255), (byte)(scB * 255)); - } + /// + /// Parses a hexadecimal color string into a . + /// + /// The hex color string. + /// The resulting . + public static Color ParseHexColor(string hexColor) + { + if (TryParseHexColor(hexColor, out var color)) + return color; - return ThrowFormatException(); - } + throw new FormatException($"The string '{hexColor}' could not be parsed as a hex color."); + } - var prop = typeof(Colors).GetTypeInfo().GetDeclaredProperty(colorString); + /// + /// Parses a screen color string into a . + /// + /// The screen color string. + /// The resulting . + public static Color ParseScreenColor(string screenColor) + { + if (TryParseScreenColor(screenColor, out var color)) + return color; - if (prop != null) - { -#pragma warning disable CS8605 // Unboxing a possibly null value. - return (Color)prop.GetValue(null); -#pragma warning restore CS8605 // Unboxing a possibly null value. - } + throw new FormatException($"The string '{screenColor}' is not a valid ScreenColor string"); + } - return ThrowFormatException(); + /// + /// Parses a color by name. + /// + /// The color's name. + /// The resulting . + /// Throws if the color name is not recognized. + public static Color ParseColorName(string colorName) + { + if (TryParseColorName(colorName, out var color)) + return color; - static void ThrowArgumentException() => throw new ArgumentException("The parameter \"colorString\" must not be null or empty."); - static T ThrowFormatException() => throw new FormatException("The parameter \"colorString\" is not a recognized Color format."); + throw new FormatException($"The name '{colorName}' not a valid color"); } internal static (double h1, double chroma) CalculateHueAndChroma(Color color, out double min, out double max, out double alpha) @@ -114,7 +240,7 @@ internal static (double h1, double chroma) CalculateHueAndChroma(Color color, ou var g = (double)color.G / 255; var b = (double)color.B / 255; alpha = (double)color.A / 255; - + max = Math.Max(Math.Max(r, g), b); min = Math.Min(Math.Min(r, g), b); var chroma = max - min; @@ -129,7 +255,8 @@ internal static (double h1, double chroma) CalculateHueAndChroma(Color color, ou { // Red is max h1 = ((g - b) / chroma + 6) % 6; - } else if (max == g) + } + else if (max == g) { // Green is max h1 = 2 + ((b - r) / chroma); @@ -152,7 +279,7 @@ internal static Color FromHueChroma(double h1, double chroma, double x, double m (r1, g1, b1) = h1 switch { < 1 => (chroma, x, 0d), - < 2 => (x, chroma, 0d), + < 2 => (x, chroma, 0d), < 3 => (0d, chroma, x), < 4 => (0d, x, chroma), < 5 => (x, 0d, chroma), From 0ae3f29aadf1d6e940fd1023b0c47e57d3454ac6 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 23 Oct 2025 11:25:34 +0300 Subject: [PATCH 08/22] Renamed ColorHelper tests to match new API --- components/Helpers/tests/Test_ColorHelper.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index bacf1c40..9215f84e 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -12,42 +12,42 @@ public class Test_ColorHelper { [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Predifined() + public void Test_ColorHelper_ParseColor_Predifined() { Assert.AreEqual(ColorHelper.ParseColor("Red"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex8Digits() + public void Test_ColorHelper_ParseColor_Hex8Digits() { Assert.AreEqual(ColorHelper.ParseColor("#FFFF0000"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex6Digits() + public void Test_ColorHelper_ParseColor_Hex6Digits() { Assert.AreEqual(ColorHelper.ParseColor("#FF0000"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex4Digits() + public void Test_ColorHelper_ParseColor_Hex4Digits() { Assert.AreEqual(ColorHelper.ParseColor("#FF00"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_Hex3Digits() + public void Test_ColorHelper_ParseColor_Hex3Digits() { Assert.AreEqual(ColorHelper.ParseColor("#F00"), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_ToColor_ScreenColor() + public void Test_ColorHelper_ParseColor_ScreenColor() { Assert.AreEqual(ColorHelper.ParseColor("sc#1.0,1.0,0,0"), Colors.Red); } @@ -175,14 +175,14 @@ public void Test_ColorHelper_ToHsv_MaxR() [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_FromHsl() + public void Test_ColorHelper_CreateHsl() { Assert.AreEqual(HslColor.Create(0.0, 1.0, 0.5), Colors.Red); } [TestCategory("Helpers")] [TestMethod] - public void Test_ColorHelper_FromHsv() + public void Test_ColorHelper_CreateHsv() { Assert.AreEqual(HsvColor.Create(0.0, 1.0, 1.0), Colors.Red); } From 9e45d233be0025f1e531a21f9f2fdd4cb14cf8db Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 23 Oct 2025 15:39:36 +0300 Subject: [PATCH 09/22] Added hsl and hsv color parsing --- .../Helpers/src/ColorHelper/ColorHelper.cs | 113 +++++++++++++++++- components/Helpers/tests/Test_ColorHelper.cs | 14 +++ 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/components/Helpers/src/ColorHelper/ColorHelper.cs b/components/Helpers/src/ColorHelper/ColorHelper.cs index 2be98252..d03855e7 100644 --- a/components/Helpers/src/ColorHelper/ColorHelper.cs +++ b/components/Helpers/src/ColorHelper/ColorHelper.cs @@ -42,6 +42,8 @@ public static bool TryParseColor(string colorString, out Color color) if (TryParseScreenColor(colorString, out color)) return true; + // TODO: Should hsl and hsv be added? + if (TryParseColorName(colorString, out color)) return true; @@ -66,7 +68,7 @@ public static bool TryParseHexColor(string hexString, out Color color) return false; // Convert base 16 string to uint - if(!uint.TryParse(hexString[1..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cuint)) + if (!uint.TryParse(hexString[1..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cuint)) return false; // Extract 4 bytes @@ -110,6 +112,46 @@ public static bool TryParseHexColor(string hexString, out Color color) } } + /// + /// Attempts to parse a string as a hsl color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseHslColor(string hslColor, out HslColor color) + { + color = default; + + if (!MatchArgPattern(hslColor, "hsl", out var args)) + return false; + + if (args.Length != 3) + return false; + + color = HslColor.Create(args[0], args[1], args[2]); + return true; + } + + /// + /// Attempts to parse a string as a hsv color. + /// + /// The string to parse. + /// The resulting color. + /// Whether or not the parse succeeded. + public static bool TryParseHsvColor(string hsvColor, out HsvColor color) + { + color = default; + + if (!MatchArgPattern(hsvColor, "hsv", out var args)) + return false; + + if (args.Length != 3) + return false; + + color = HsvColor.Create(args[0], args[1], args[2]); + return true; + } + /// /// Attempts to parse a string as a screen color. /// @@ -130,7 +172,7 @@ public static bool TryParseScreenColor(string screenColor, out Color color) // Parse the arguments from string doubles into bytes var args = new byte[values.Length]; - for (int i = 0; i < values.Length; i++) + for (int i = 0; i < values.Length; i++) { if (!double.TryParse(values[i], out var arg)) return false; @@ -154,7 +196,7 @@ public static bool TryParseScreenColor(string screenColor, out Color color) return false; } } - + /// /// Attempts to parse a string color name. /// @@ -164,7 +206,7 @@ public static bool TryParseScreenColor(string screenColor, out Color color) public static bool TryParseColorName(string colorName, out Color color) { color = default; - + #pragma warning disable CS8605 // Unboxing a possibly null value. var prop = typeof(Colors).GetTypeInfo().GetDeclaredProperty(colorName); if (prop != null) @@ -203,6 +245,32 @@ public static Color ParseHexColor(string hexColor) throw new FormatException($"The string '{hexColor}' could not be parsed as a hex color."); } + + /// + /// Parses a hsl color string into a . + /// + /// The hsl color string. + /// The resulting . + public static HslColor ParseHslColor(string hslColor) + { + if (TryParseHslColor(hslColor, out var color)) + return color; + + throw new FormatException($"The string '{hslColor}' could not be parsed as a hsl color."); + } + + /// + /// Parses a hsv color string into a . + /// + /// The hsv color string. + /// The resulting . + public static HsvColor ParseHsvColor(string hsvColor) + { + if (TryParseHsvColor(hsvColor, out var color)) + return color; + + throw new FormatException($"The string '{hsvColor}' could not be parsed as a hsv color."); + } /// /// Parses a screen color string into a . @@ -293,4 +361,41 @@ internal static Color FromHueChroma(double h1, double chroma, double x, double m return Color.FromArgb(a, r, g, b); } + + /// + /// Parses a string to match the argument pattern "(args[0], args[1], ...)" + /// + private static bool MatchArgPattern(string value, string funcName, out T?[] args) + where T : IParsable + { + args = []; + + // Find opening and closing parenthesis + var argsStart = value.IndexOf('('); + var argsEnd = value.Length - 1; + if (argsStart is -1) + return false; + + // Ensure the string begins with the function name + if (value[0..argsStart] != funcName) + return false; + + // Ensure the string ends with ')' + if (value[argsEnd] != ')') + return false; + + // Split to substring between the parenthesis + value = value[(argsStart + 1)..argsEnd]; + var argStrings = value.Split(','); + + // Parse the args into an array of doubles + args = new T[argStrings.Length]; + for (var i = 0; i < argStrings.Length; i++) + { + if (!T.TryParse(argStrings[i], null, out args[i])) + return false; + } + + return true; + } } diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index 9215f84e..7b79e187 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -52,6 +52,20 @@ public void Test_ColorHelper_ParseColor_ScreenColor() Assert.AreEqual(ColorHelper.ParseColor("sc#1.0,1.0,0,0"), Colors.Red); } + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_ParseHslColor() + { + Assert.AreEqual(ColorHelper.ParseHslColor("hsl(0,1,0.5)"), Colors.Red); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_ParseHsvColor() + { + Assert.AreEqual(ColorHelper.ParseHsvColor("hsv(0,1,1)"), Colors.Red); + } + [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ToHex() From 6fbddcfb6805a190bd2976e5dc1a60fde54bf069 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Fri, 24 Oct 2025 10:10:52 +0300 Subject: [PATCH 10/22] Restored public fields with obsolete attributes to remove breaking changes --- .../Helpers/src/ColorHelper/HslColor.cs | 51 +++++++++++++------ .../Helpers/src/ColorHelper/HsvColor.cs | 51 +++++++++++++------ 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index b1ada913..26639f38 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -12,10 +12,29 @@ namespace CommunityToolkit.WinUI; /// public struct HslColor { - private double _hue; - private double _saturation; - private double _lightness; - private double _alpha; + /// + /// The hue value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Hue property")] + public double H; + + /// + /// The saturation value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Saturation property")] + public double S; + + /// + /// The lightness value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Lightness property")] + public double L; + + /// + /// The alpha/opacity value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Alpha property")] + public double A; /// /// Initializes a new instance of the struct. @@ -61,10 +80,10 @@ public static HslColor Create(double hue, double saturation, double lightness, d /// /// This value is clamped between 0 and 360. /// - public double H + public double Hue { - readonly get => _hue; - set => _hue = Math.Clamp(value, 0, 360); + readonly get => Math.Clamp(H, 0, 360); + set => H = Math.Clamp(value, 0, 360); } /// @@ -73,10 +92,10 @@ public double H /// /// This value is clamped between 0 and 1. /// - public double S + public double Saturation { - readonly get => _saturation; - set => _saturation = Math.Clamp(value, 0, 1); + readonly get => Math.Clamp(S, 0, 1); + set => S = Math.Clamp(value, 0, 1); } /// @@ -85,10 +104,10 @@ public double S /// /// This value is clamped between 0 and 1. /// - public double L + public double Lightness { - readonly get => _lightness; - set => _lightness = Math.Clamp(value, 0, 1); + readonly get => Math.Clamp(L, 0, 1); + set => L = Math.Clamp(value, 0, 1); } /// @@ -97,10 +116,10 @@ public double L /// /// This value is clamped between 0 and 1. /// - public double A + public double Alpha { - readonly get => _alpha; - set => _alpha = Math.Clamp(value, 0, 1); + readonly get => Math.Clamp(A, 0, 1); + set => A = Math.Clamp(value, 0, 1); } /// diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index 1324f29f..5821eb82 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -12,10 +12,29 @@ namespace CommunityToolkit.WinUI; /// public struct HsvColor { - private double _hue; - private double _saturation; - private double _value; - private double _alpha; + /// + /// The hue value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Hue property")] + public double H; + + /// + /// The saturation value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Saturation property")] + public double S; + + /// + /// The "value" value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Value property")] + public double V; + + /// + /// The alpha/opacity value. + /// + [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Alpha property")] + public double A; /// /// Initializes a new instance of the struct. @@ -61,10 +80,10 @@ public static HsvColor Create(double hue, double saturation, double value, doubl /// /// This value is clamped between 0 and 360. /// - public double H + public double Hue { - readonly get => _hue; - set => _hue = Math.Clamp(value, 0, 360); + readonly get => Math.Clamp(H, 0, 360); + set => H = Math.Clamp(value, 0, 360); } /// @@ -73,10 +92,10 @@ public double H /// /// This value is clamped between 0 and 1. /// - public double S + public double Saturation { - readonly get => _saturation; - set => _saturation = Math.Clamp(value, 0, 1); + readonly get => Math.Clamp(S, 0, 1); + set => S = Math.Clamp(value, 0, 1); } /// @@ -85,10 +104,10 @@ public double S /// /// This value is clamped between 0 and 1. /// - public double V + public double Value { - readonly get => _value; - set => _value = Math.Clamp(value, 0, 1); + readonly get => Math.Clamp(V, 0, 1); + set => V = Math.Clamp(value, 0, 1); } /// @@ -97,10 +116,10 @@ public double V /// /// This value is clamped between 0 and 1. /// - public double A + public double Alpha { - readonly get => _alpha; - set => _alpha = Math.Clamp(value, 0, 1); + readonly get => Math.Clamp(A, 0, 1); + set => A = Math.Clamp(value, 0, 1); } /// From 6f93d65efbd669aa91be88e2674319455b8c7741 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 18 Nov 2025 03:17:54 +0200 Subject: [PATCH 11/22] Changed HslColor and HsvColor not to use deprecated fields internally --- .../Helpers/src/ColorHelper/HslColor.cs | 32 +++++++++------- .../Helpers/src/ColorHelper/HsvColor.cs | 37 ++++++++++++------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index 26639f38..13b1fbd6 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -50,10 +50,10 @@ private HslColor(Color color) double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); // Set hsl properties - H = 60 * h1; - S = saturation; - L = lightness; - A = alpha; + Hue = 60 * h1; + Saturation = saturation; + Lightness = lightness; + Alpha = alpha; } /// @@ -67,13 +67,17 @@ private HslColor(Color color) public static HslColor Create(double hue, double saturation, double lightness, double alpha = 1) { HslColor color = default; - color.H = hue; - color.S = saturation; - color.L = lightness; - color.A = alpha; + color.Hue = hue; + color.Saturation = saturation; + color.Lightness = lightness; + color.Alpha = alpha; return color; } + // This class contains deprecated public backing fields to be removed in a future version. + // Suppress the warnings from using them in their new wrapping properties. +#pragma warning disable 0618 + /// /// Gets or sets the hue. /// @@ -122,22 +126,24 @@ public double Alpha set => A = Math.Clamp(value, 0, 1); } +#pragma warning restore 0618 + /// /// Converts the to a . /// /// The as a . public readonly Color ToColor() { - double chroma = (1 - Math.Abs((2 * L) - 1)) * S; - double h1 = H / 60; + double chroma = (1 - Math.Abs((2 * Lightness) - 1)) * Saturation; + double h1 = Hue / 60; double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); - double m = L - (0.5 * chroma); + double m = Lightness - (0.5 * chroma); - return ColorHelper.FromHueChroma(h1, chroma, x, m, A); + return ColorHelper.FromHueChroma(h1, chroma, x, m, Alpha); } /// - public override readonly string ToString() => $"hsl({H:N0}, {S}, {L})"; + public override readonly string ToString() => $"hsl({Hue:N0}, {Saturation}, {Lightness})"; /// /// Cast a to a . diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index 5821eb82..0f8bb3b8 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -7,6 +7,7 @@ namespace CommunityToolkit.WinUI; + /// /// Defines a color in Hue/Saturation/Value (HSV) space with alpha. /// @@ -50,10 +51,10 @@ private HsvColor(Color color) double value = max; // Set hsv properties - H = 60 * h1; - S = saturation; - V = value; - A = alpha; + Hue = 60 * h1; + Saturation = saturation; + Value = value; + Alpha = alpha; } /// @@ -67,13 +68,18 @@ private HsvColor(Color color) public static HsvColor Create(double hue, double saturation, double value, double alpha = 1) { HsvColor color = default; - color.H = hue; - color.S = saturation; - color.V = value; - color.A = alpha; + color.Hue = hue; + color.Saturation = saturation; + color.Value = value; + color.Alpha = alpha; return color; } - + + + // This class contains deprecated public backing fields to be removed in a future version. + // Suppress the warnings from using them in their new wrapping properties. +#pragma warning disable 0618 + /// /// Gets or sets the hue. /// @@ -122,22 +128,25 @@ public double Alpha set => A = Math.Clamp(value, 0, 1); } +#pragma warning restore 0618 + + /// /// Converts the to a . /// /// The as a . public readonly Color ToColor() { - double chroma = V * S; - double h1 = H / 60; + double chroma = Value * Saturation; + double h1 = Hue / 60; double x = chroma * (1 - Math.Abs((h1 % 2) - 1)); - double m = V - chroma; + double m = Value - chroma; - return ColorHelper.FromHueChroma(h1, chroma, x, m, A); + return ColorHelper.FromHueChroma(h1, chroma, x, m, Alpha); } /// - public override readonly string ToString() => $"hsv({H:N0}, {S}, {V})"; + public override readonly string ToString() => $"hsv({Hue:N0}, {Saturation}, {Value})"; /// /// Cast a to a . From 08b2cd75c76b5ecfb083e28b67d087b8d35796ca Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 18 Nov 2025 03:18:31 +0200 Subject: [PATCH 12/22] Added static Color extensions with #if NET10 clause --- .../src/ColorHelper/ColorExtensions.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs index 8bc6f05f..b435981d 100644 --- a/components/Helpers/src/ColorHelper/ColorExtensions.cs +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -36,4 +36,39 @@ public static int ToInt(this Color color) /// The to convert. /// The converted . public static HsvColor ToHsv(this Color color) => (HsvColor)color; + +#if NET10_0_OR_GREATER + + extension(Color color) + { + /// + /// Gets a from alpha, hue, saturation, and lightness channel info. + /// + /// + /// This returns a in order to avoid unneccesary conversions. + /// However, the will be implicitly cast to a if needed. + /// + /// The color's hue. + /// The color's saturation. + /// The color's lightness. + /// The color's alpha value. + /// The color as a . + public static HslColor FromAhsl(double a, double h, double s, double l) => HslColor.Create(h, s, l, a); + + /// + /// Gets a from alpha, hue, saturation, and value channel info. + /// + /// + /// This returns a in order to avoid unneccesary conversions. + /// However, the will be implicitly cast to a if needed. + /// + /// The color's hue. + /// The color's saturation. + /// The color's value. + /// The color's alpha value. + /// The color as a . + public static HsvColor FromAhsv(double a, double h, double s, double v) => HsvColor.Create(h, s, v, a); + } + +#endif } From 63a4a4fe0a02893f3110da7adc7a0c33cdcc3bd6 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Tue, 18 Nov 2025 03:30:04 +0200 Subject: [PATCH 13/22] Added Never EditorBrowsable attributes to deprecated fields to further discourage new usage --- components/Helpers/src/ColorHelper/HslColor.cs | 4 ++++ components/Helpers/src/ColorHelper/HsvColor.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index 13b1fbd6..d6c22aa1 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -15,24 +15,28 @@ public struct HslColor /// /// The hue value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Hue property")] public double H; /// /// The saturation value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Saturation property")] public double S; /// /// The lightness value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Lightness property")] public double L; /// /// The alpha/opacity value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Alpha property")] public double A; diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index 0f8bb3b8..f48730e1 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -16,24 +16,28 @@ public struct HsvColor /// /// The hue value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Hue property")] public double H; /// /// The saturation value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Saturation property")] public double S; /// /// The "value" value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Value property")] public double V; /// /// The alpha/opacity value. /// + [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This field is marked deprecated, and will be removed in a future version. Please replace with the HsvColor.Alpha property")] public double A; From 4b76529fa2c8d73ae4bf85da5eb5ca147d063e0b Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Wed, 26 Nov 2025 16:54:31 +0200 Subject: [PATCH 14/22] Added AlphaOver and Mix color extension methods --- .../src/ColorHelper/ColorExtensions.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs index b435981d..7d667913 100644 --- a/components/Helpers/src/ColorHelper/ColorExtensions.cs +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.ComponentModel.DataAnnotations; using Windows.UI; namespace CommunityToolkit.WinUI.Helpers; @@ -37,6 +38,42 @@ public static int ToInt(this Color color) /// The converted . public static HsvColor ToHsv(this Color color) => (HsvColor)color; + /// + public static Color AlphaOver(this Color @base, Color overlay) + => AlphaOver(@base, overlay, (double)overlay.A / 255); + + /// + /// Performs an AlphaOverlay between two colors. + /// + /// The base color. + /// The overlay color. + /// The alpha factor. + /// The resulting color from alpha overlaying over . + public static Color AlphaOver(this Color @base, Color overlay, double alpha) + { + // Find the color's mix, using the overlay's opacity as a factor + var mix = MixColors(@base, overlay, alpha); + + // Restore the alpha to match the base + mix.A = @base.A; + return mix; + } + + private static Color MixColors(Color color1, Color color2, double factor) + { + // Formula for linearly blending a channel + var invFactor = 1 - factor; + byte Blend(byte c1, byte c2) => (byte)Math.Round((c1 * factor) + (c2 * invFactor)); + + // Blend each channel + var a = Blend(color1.A, color2.A); + var r = Blend(color1.R, color2.R); + var g = Blend(color1.G, color2.G); + var b = Blend(color1.B, color2.B); + + return Color.FromArgb(a, r, g, b); + } + #if NET10_0_OR_GREATER extension(Color color) @@ -68,6 +105,16 @@ public static int ToInt(this Color color) /// The color's alpha value. /// The color as a . public static HsvColor FromAhsv(double a, double h, double s, double v) => HsvColor.Create(h, s, v, a); + + /// + /// Mixes two colors + /// + /// + /// + /// + /// + public static Color Mix(Color color1, Color color2, double factor) + => MixColors(color1, color2, factor); } #endif From e20c31fa6c572939a4cc8301987d7696d2ddb9f0 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 17:30:20 +0200 Subject: [PATCH 15/22] Added H/S/V/L, Add, and Subtract methods --- .../src/ColorHelper/ColorExtensions.cs | 119 +++++++++++++++++- 1 file changed, 113 insertions(+), 6 deletions(-) diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs index 7d667913..0346d693 100644 --- a/components/Helpers/src/ColorHelper/ColorExtensions.cs +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.ComponentModel.DataAnnotations; using Windows.UI; namespace CommunityToolkit.WinUI.Helpers; @@ -59,12 +58,72 @@ public static Color AlphaOver(this Color @base, Color overlay, double alpha) return mix; } + /// + /// Gets a color with the same saturation and value/lightness, but with an adjusted hue. + /// + /// The original color. + /// The new hue. + /// A with a new hue and the same saturation and value/lightness. + public static HsvColor Hue(this Color @base, double hue) + { + var hsv = (HsvColor)@base; + hsv.Hue = hue; + return hsv; + } + + /// + /// Gets a color with the same hue and value/lightness, but with an adjusted saturation. + /// + /// The original color. + /// The new saturation. + /// A with a new saturation and the same hue and value/lightness. + public static HsvColor Saturation(this Color @base, double saturation) + { + var hsv = (HsvColor)@base; + hsv.Saturation = saturation; + return hsv; + } + + /// + /// Gets a color with the same hue and saturation, but with an adjusted saturation. + /// + /// The original color. + /// The new value. + /// A with a new value and the same hue and saturation. + public static HsvColor Value(this Color @base, double value) + { + var hsv = (HsvColor)@base; + hsv.Value = value; + return hsv; + } + + /// + /// Gets a color with the same hue and saturation, but with an adjusted lightness. + /// + /// The original color. + /// The new lightness. + /// A with a new lightness and the same hue and saturation. + public static HslColor Lightness(this Color @base, double lightness) + { + var hsl = (HslColor)@base; + hsl.Lightness = lightness; + return hsl; + } + private static Color MixColors(Color color1, Color color2, double factor) { + factor = Math.Clamp(factor, 0, 1); + // Formula for linearly blending a channel var invFactor = 1 - factor; byte Blend(byte c1, byte c2) => (byte)Math.Round((c1 * factor) + (c2 * invFactor)); + if (factor is 0) + return color1; + + if (factor is 1) + return color2; + // Blend each channel var a = Blend(color1.A, color2.A); var r = Blend(color1.R, color2.R); @@ -107,14 +166,62 @@ private static Color MixColors(Color color1, Color color2, double factor) public static HsvColor FromAhsv(double a, double h, double s, double v) => HsvColor.Create(h, s, v, a); /// - /// Mixes two colors + /// Mixes two colors using a factor for deciding how much influence each color has. /// - /// - /// - /// - /// + /// The first color. + /// The second color. + /// The influence of each color, where 0 is entirely and 1 is entirely . + /// The mix of the two colors. public static Color Mix(Color color1, Color color2, double factor) => MixColors(color1, color2, factor); + + /// + /// Adds two colors. + /// + /// + /// Simple RGB summation, with each channel clamped seperately. + /// + /// The first color. + /// The second color. + /// The sume of the two colors. + public static Color Add(Color color1, Color color2) + { + static byte ClampedAdd(byte b1, byte b2) => (byte)int.Min(b1 + b2, 255); + + var a = ClampedAdd(color1.A, color2.A); + var r = ClampedAdd(color1.R, color2.R); + var g = ClampedAdd(color1.G, color2.G); + var b = ClampedAdd(color1.B, color2.B); + + return Color.FromArgb(a, r, g, b); + } + + /// + /// Subtracts a color from another. + /// + /// + /// Simple RGB subtraction, with each channel clamped seperately. + /// + /// The first color. + /// The second color. + /// The sume of the two colors. + public static Color Subtract(Color color1, Color color2) + { + static byte ClampedSubtract(byte b1, byte b2) => (byte)int.Max(b1 - b2, 0); + + var a = ClampedSubtract(color1.A, color2.A); + var r = ClampedSubtract(color1.R, color2.R); + var g = ClampedSubtract(color1.G, color2.G); + var b = ClampedSubtract(color1.B, color2.B); + + return Color.FromArgb(a, r, g, b); + } + + /// + public static Color operator +(Color color1, Color color2) => Add(color1, color2); + + /// + public static Color operator -(Color color1, Color color2) => Add(color1, color2); } #endif From bf2fa1dcb6aa303bb6f3a2db25fc50b4386660a4 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 18:04:28 +0200 Subject: [PATCH 16/22] Added test code for new ColorExtension methods --- .../src/ColorHelper/ColorExtensions.cs | 28 ++++----- components/Helpers/tests/Test_ColorHelper.cs | 63 +++++++++++++++++++ 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs index 0346d693..47f4f008 100644 --- a/components/Helpers/src/ColorHelper/ColorExtensions.cs +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -59,12 +59,12 @@ public static Color AlphaOver(this Color @base, Color overlay, double alpha) } /// - /// Gets a color with the same saturation and value/lightness, but with an adjusted hue. + /// Gets a color with the same saturation and value, but with an adjusted hue. /// /// The original color. /// The new hue. - /// A with a new hue and the same saturation and value/lightness. - public static HsvColor Hue(this Color @base, double hue) + /// A with a new hue and the same saturation and value. + public static HsvColor WithHue(this Color @base, double hue) { var hsv = (HsvColor)@base; hsv.Hue = hue; @@ -72,12 +72,12 @@ public static HsvColor Hue(this Color @base, double hue) } /// - /// Gets a color with the same hue and value/lightness, but with an adjusted saturation. + /// Gets a color with the same hue and value, but with an adjusted saturation. /// /// The original color. /// The new saturation. - /// A with a new saturation and the same hue and value/lightness. - public static HsvColor Saturation(this Color @base, double saturation) + /// A with a new saturation and the same hue and value. + public static HsvColor WithSaturation(this Color @base, double saturation) { var hsv = (HsvColor)@base; hsv.Saturation = saturation; @@ -90,7 +90,7 @@ public static HsvColor Saturation(this Color @base, double saturation) /// The original color. /// The new value. /// A with a new value and the same hue and saturation. - public static HsvColor Value(this Color @base, double value) + public static HsvColor WithValue(this Color @base, double value) { var hsv = (HsvColor)@base; hsv.Value = value; @@ -103,7 +103,7 @@ public static HsvColor Value(this Color @base, double value) /// The original color. /// The new lightness. /// A with a new lightness and the same hue and saturation. - public static HslColor Lightness(this Color @base, double lightness) + public static HslColor WithLightness(this Color @base, double lightness) { var hsl = (HslColor)@base; hsl.Lightness = lightness; @@ -179,7 +179,7 @@ public static Color Mix(Color color1, Color color2, double factor) /// Adds two colors. /// /// - /// Simple RGB summation, with each channel clamped seperately. + /// Simple RGB summation, with each channel clamped seperately. Alpha is NOT included, and will always be opaque. /// /// The first color. /// The second color. @@ -188,19 +188,18 @@ public static Color Add(Color color1, Color color2) { static byte ClampedAdd(byte b1, byte b2) => (byte)int.Min(b1 + b2, 255); - var a = ClampedAdd(color1.A, color2.A); var r = ClampedAdd(color1.R, color2.R); var g = ClampedAdd(color1.G, color2.G); var b = ClampedAdd(color1.B, color2.B); - return Color.FromArgb(a, r, g, b); + return Color.FromArgb(255, r, g, b); } /// /// Subtracts a color from another. /// /// - /// Simple RGB subtraction, with each channel clamped seperately. + /// Simple RGB subtraction, with each channel clamped seperately. Alpha is NOT included, and will always be opaque. /// /// The first color. /// The second color. @@ -209,19 +208,18 @@ public static Color Subtract(Color color1, Color color2) { static byte ClampedSubtract(byte b1, byte b2) => (byte)int.Max(b1 - b2, 0); - var a = ClampedSubtract(color1.A, color2.A); var r = ClampedSubtract(color1.R, color2.R); var g = ClampedSubtract(color1.G, color2.G); var b = ClampedSubtract(color1.B, color2.B); - return Color.FromArgb(a, r, g, b); + return Color.FromArgb(255, r, g, b); } /// public static Color operator +(Color color1, Color color2) => Add(color1, color2); /// - public static Color operator -(Color color1, Color color2) => Add(color1, color2); + public static Color operator -(Color color1, Color color2) => Subtract(color1, color2); } #endif diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index 7b79e187..de437eb1 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.WinUI.Helpers; +using Windows.UI; using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; namespace HelpersTests; @@ -200,4 +201,66 @@ public void Test_ColorHelper_CreateHsv() { Assert.AreEqual(HsvColor.Create(0.0, 1.0, 1.0), Colors.Red); } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_AlphaOver() + { + Assert.AreEqual(Colors.Red.AlphaOver(Colors.Blue, 0.5), Color.FromArgb(255, 128, 0, 128)); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_Mix() + { + Assert.AreEqual(Color.Mix(Colors.White, Colors.Black, 0.6625), Colors.DarkGray); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_WithHue() + { + Assert.AreEqual(Colors.Blue.WithHue(0), Colors.Red); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_WithSaturation() + { + Assert.AreEqual(Colors.Red.WithSaturation(0), Colors.White); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_WithValue() + { + Assert.AreEqual(Colors.Red.WithValue(0), Colors.Black); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_WithLightness() + { + Assert.AreEqual(Colors.Red.WithLightness(0), Colors.Black); + } + +#if NET10_0_OR_GREATER + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_Add() + { + Assert.AreEqual(Color.Add(Colors.Red, Colors.Blue), Colors.Magenta); + Assert.AreEqual(Colors.Red + Colors.Blue, Colors.Magenta); + } + + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_Subtract() + { + Assert.AreEqual(Color.Subtract(Colors.Magenta, Colors.Red), Colors.Blue); + Assert.AreEqual(Colors.Magenta - Colors.Red, Colors.Blue); + } + +#endif } From 157d8331ba5afde92e7d9d913896a4d955a065bd Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 19:50:48 +0200 Subject: [PATCH 17/22] Renamed @base to original in WithX methods --- .../src/ColorHelper/ColorExtensions.cs | 24 +++++++++---------- .../Helpers/src/ColorHelper/HsvColor.cs | 1 - 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs index 47f4f008..40a7df77 100644 --- a/components/Helpers/src/ColorHelper/ColorExtensions.cs +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -61,12 +61,12 @@ public static Color AlphaOver(this Color @base, Color overlay, double alpha) /// /// Gets a color with the same saturation and value, but with an adjusted hue. /// - /// The original color. + /// The original color. /// The new hue. /// A with a new hue and the same saturation and value. - public static HsvColor WithHue(this Color @base, double hue) + public static HsvColor WithHue(this Color original, double hue) { - var hsv = (HsvColor)@base; + var hsv = (HsvColor)original; hsv.Hue = hue; return hsv; } @@ -74,12 +74,12 @@ public static HsvColor WithHue(this Color @base, double hue) /// /// Gets a color with the same hue and value, but with an adjusted saturation. /// - /// The original color. + /// The original color. /// The new saturation. /// A with a new saturation and the same hue and value. - public static HsvColor WithSaturation(this Color @base, double saturation) + public static HsvColor WithSaturation(this Color original, double saturation) { - var hsv = (HsvColor)@base; + var hsv = (HsvColor)original; hsv.Saturation = saturation; return hsv; } @@ -87,12 +87,12 @@ public static HsvColor WithSaturation(this Color @base, double saturation) /// /// Gets a color with the same hue and saturation, but with an adjusted saturation. /// - /// The original color. + /// The original color. /// The new value. /// A with a new value and the same hue and saturation. - public static HsvColor WithValue(this Color @base, double value) + public static HsvColor WithValue(this Color original, double value) { - var hsv = (HsvColor)@base; + var hsv = (HsvColor)original; hsv.Value = value; return hsv; } @@ -100,12 +100,12 @@ public static HsvColor WithValue(this Color @base, double value) /// /// Gets a color with the same hue and saturation, but with an adjusted lightness. /// - /// The original color. + /// The original color. /// The new lightness. /// A with a new lightness and the same hue and saturation. - public static HslColor WithLightness(this Color @base, double lightness) + public static HslColor WithLightness(this Color original, double lightness) { - var hsl = (HslColor)@base; + var hsl = (HslColor)original; hsl.Lightness = lightness; return hsl; } diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index f48730e1..27cc0ff1 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -7,7 +7,6 @@ namespace CommunityToolkit.WinUI; - /// /// Defines a color in Hue/Saturation/Value (HSV) space with alpha. /// From f115669fc9f5e43138cfef2ab9eaa861d5562467 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 19:54:30 +0200 Subject: [PATCH 18/22] Added depreciation warning suppression in ColorHelper tests --- components/Helpers/tests/Test_ColorHelper.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index de437eb1..84bf2d51 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -11,6 +11,10 @@ namespace HelpersTests; [TestClass] public class Test_ColorHelper { + // Keep testing the old APIs until they are removed + // In the meantime suppress the warnings from using them +#pragma warning disable 0618 + [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_ParseColor_Predifined() @@ -263,4 +267,5 @@ public void Test_ColorHelper_Subtract() } #endif +#pragma warning restore 0618 } From da578c9e5975cc96c91d47587aee85a1cbdaa642 Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 20:11:51 +0200 Subject: [PATCH 19/22] Added static ColorHelper methods to ColorExtensions NET10 API --- .../src/ColorHelper/ColorExtensions.cs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/components/Helpers/src/ColorHelper/ColorExtensions.cs b/components/Helpers/src/ColorHelper/ColorExtensions.cs index 40a7df77..fec8bcb0 100644 --- a/components/Helpers/src/ColorHelper/ColorExtensions.cs +++ b/components/Helpers/src/ColorHelper/ColorExtensions.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.UI; +using System.Globalization; using Windows.UI; namespace CommunityToolkit.WinUI.Helpers; @@ -135,8 +137,44 @@ private static Color MixColors(Color color1, Color color2, double factor) #if NET10_0_OR_GREATER - extension(Color color) + extension(Color _) { + /// + public static bool TryParseColor(string colorString, out Color color) => ColorHelper.TryParseColor(colorString, out color); + + /// + public static bool TryParseHexColor(string hexString, out Color color) => ColorHelper.TryParseHexColor(hexString, out color); + + /// + public static bool TryParseHslColor(string hslColor, out HslColor color) => ColorHelper.TryParseHslColor(hslColor, out color); + + /// + public static bool TryParseHsvColor(string hsvColor, out HsvColor color) => ColorHelper.TryParseHsvColor(hsvColor, out color); + + /// + public static bool TryParseScreenColor(string screenColor, out Color color) => ColorHelper.TryParseScreenColor(screenColor, out color); + + /// + public static bool TryParseColorName(string colorName, out Color color) => ColorHelper.TryParseColorName(colorName, out color); + + /// + public static Color ParseColor(string colorString) => ColorHelper.ParseColor(colorString); + + /// + public static Color ParseHexColor(string hexColor) => ColorHelper.ParseHexColor(hexColor); + + /// + public static HslColor ParseHslColor(string hslColor) => ColorHelper.ParseHslColor(hslColor); + + /// + public static HsvColor ParseHsvColor(string hsvColor) => ColorHelper.ParseHsvColor(hsvColor); + + /// + public static Color ParseScreenColor(string screenColor) => ColorHelper.ParseScreenColor(screenColor); + + /// + public static Color ParseColorName(string colorName) => ColorHelper.ParseColorName(colorName); + /// /// Gets a from alpha, hue, saturation, and lightness channel info. /// From 25a9139309da9359d69074227f356390ca48ee7f Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 20:58:51 +0200 Subject: [PATCH 20/22] Updated ColorPicker to use new ColorHelper APIs because the warnings were being treated as errors in the CI --- components/ColorPicker/src/ColorPicker.cs | 63 +++++++--------- .../src/ColorPickerRenderingHelpers.cs | 72 +++++-------------- .../ColorPicker/src/ColorPickerSlider.cs | 50 +++++-------- .../src/Converters/AccentColorConverter.cs | 51 +++++-------- .../src/Converters/ColorToHexConverter.cs | 59 ++++++--------- 5 files changed, 101 insertions(+), 194 deletions(-) diff --git a/components/ColorPicker/src/ColorPicker.cs b/components/ColorPicker/src/ColorPicker.cs index 276ae7be..dafec310 100644 --- a/components/ColorPicker/src/ColorPicker.cs +++ b/components/ColorPicker/src/ColorPicker.cs @@ -510,12 +510,12 @@ private void UpdateColorControlValues() if (this.IsAlphaEnabled) { // Remove only the "#" sign - this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty); + this.HexInputTextBox.Text = rgbColor.ToString().TrimStart('#'); } else { // Remove the "#" sign and alpha hex - this.HexInputTextBox.Text = rgbColor.ToHex().Replace("#", string.Empty).Substring(2); + this.HexInputTextBox.Text = rgbColor.ToString().TrimStart('#')[2..]; } } @@ -532,10 +532,10 @@ private void UpdateColorControlValues() int decimals = 0; hsvColor = new HsvColor() { - H = Math.Round(hsvColor.H, decimals), - S = Math.Round(hsvColor.S, 2 + decimals), - V = Math.Round(hsvColor.V, 2 + decimals), - A = Math.Round(hsvColor.A, 2 + decimals) + Hue = Math.Round(hsvColor.Hue, decimals), + Saturation = Math.Round(hsvColor.Saturation, 2 + decimals), + Value = Math.Round(hsvColor.Value, 2 + decimals), + Alpha = Math.Round(hsvColor.Alpha, 2 + decimals) }; // Must update HSV color @@ -554,10 +554,10 @@ private void UpdateColorControlValues() { this.ColorSpectrumControl.HsvColor = new System.Numerics.Vector4() { - X = Convert.ToSingle(hsvColor.H), - Y = Convert.ToSingle(hsvColor.S), - Z = Convert.ToSingle(hsvColor.V), - W = Convert.ToSingle(hsvColor.A) + X = Convert.ToSingle(hsvColor.Hue), + Y = Convert.ToSingle(hsvColor.Saturation), + Z = Convert.ToSingle(hsvColor.Value), + W = Convert.ToSingle(hsvColor.Alpha) }; } @@ -565,9 +565,9 @@ private void UpdateColorControlValues() if (this.ColorSpectrumThirdDimensionSlider != null) { // Convert the channels into a usable range for the user - double hue = hsvColor.H; - double staturation = hsvColor.S * 100; - double value = hsvColor.V * 100; + double hue = hsvColor.Hue; + double staturation = hsvColor.Saturation * 100; + double value = hsvColor.Value * 100; switch (this.GetActiveColorSpectrumThirdDimension()) { @@ -610,10 +610,10 @@ private void UpdateColorControlValues() if (this.GetActiveColorRepresentation() == ColorRepresentation.Hsva) { // Convert the channels into a usable range for the user - double hue = hsvColor.H; - double staturation = hsvColor.S * 100; - double value = hsvColor.V * 100; - double alpha = hsvColor.A * 100; + double hue = hsvColor.Hue; + double staturation = hsvColor.Saturation * 100; + double value = hsvColor.Value * 100; + double alpha = hsvColor.Alpha * 100; // Hue if (this.Channel1NumberBox != null) @@ -792,10 +792,10 @@ private void SetColorChannel( oldHsvColor = this.savedHsvColor.Value; } - double hue = oldHsvColor.H; - double saturation = oldHsvColor.S; - double value = oldHsvColor.V; - double alpha = oldHsvColor.A; + double hue = oldHsvColor.Hue; + double saturation = oldHsvColor.Saturation; + double value = oldHsvColor.Value; + double alpha = oldHsvColor.Alpha; switch (channel) { @@ -825,20 +825,11 @@ private void SetColorChannel( } } - newRgbColor = Helpers.ColorHelper.FromHsv( - hue, - saturation, - value, - alpha); + var hsv = HsvColor.Create(hue, saturation, value, alpha); + newRgbColor = hsv; // Must update HSV color - this.savedHsvColor = new HsvColor() - { - H = hue, - S = saturation, - V = value, - A = alpha - }; + this.savedHsvColor = hsv; this.savedHsvColorRgbEquivalent = newRgbColor; } else @@ -1351,16 +1342,14 @@ private void ColorModeComboBox_SelectionChanged(object sender, SelectionChangedE /// private void ColorPreviewer_ColorChangeRequested(object? sender, HsvColor hsvColor) { - Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); - // Regardless of the active color model, the previewer always uses HSV // Therefore, always calculate HSV color here // Warning: Always maintain/use HSV information in the saved HSV color // This avoids loss of precision and drift caused by continuously converting to/from RGB this.savedHsvColor = hsvColor; - this.savedHsvColorRgbEquivalent = rgbColor; + this.savedHsvColorRgbEquivalent = hsvColor; - this.ScheduleColorUpdate(rgbColor); + this.ScheduleColorUpdate(hsvColor); return; } diff --git a/components/ColorPicker/src/ColorPickerRenderingHelpers.cs b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs index 7ac31a24..59a91dfc 100644 --- a/components/ColorPicker/src/ColorPickerRenderingHelpers.cs +++ b/components/ColorPicker/src/ColorPickerRenderingHelpers.cs @@ -79,23 +79,13 @@ public static async Task CreateChannelBitmapAsync( if (isAlphaMaxForced && channel != ColorChannel.Alpha) { - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = baseHsvColor.S, - V = baseHsvColor.V, - A = 1.0 - }; + baseHsvColor.Alpha = 1.0; } // Convert HSV to RGB once if (colorRepresentation == ColorRepresentation.Rgba) { - baseRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - baseHsvColor.S, - baseHsvColor.V, - baseHsvColor.A); + baseRgbColor = baseHsvColor; } // Maximize Saturation and Value channels when in HSVA mode @@ -106,31 +96,15 @@ public static async Task CreateChannelBitmapAsync( switch (channel) { case ColorChannel.Channel1: - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = 1.0, - V = 1.0, - A = baseHsvColor.A - }; + baseHsvColor.Saturation = 1.0; + baseHsvColor.Value = 1.0; break; case ColorChannel.Channel2: - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = baseHsvColor.S, - V = 1.0, - A = baseHsvColor.A - }; + + baseHsvColor.Value = 1; break; case ColorChannel.Channel3: - baseHsvColor = new HsvColor() - { - H = baseHsvColor.H, - S = 1.0, - V = baseHsvColor.V, - A = baseHsvColor.A - }; + baseHsvColor.Saturation = 1; break; } } @@ -300,11 +274,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep hue - newRgbColor = Helpers.ColorHelper.FromHsv( - Math.Clamp(channelValue, 0.0, 360.0), - baseHsvColor.S, - baseHsvColor.V, - baseHsvColor.A); + var hsv = baseHsvColor; + hsv.Hue = channelValue; + newRgbColor = hsv; } else { @@ -326,11 +298,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep saturation - newRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - Math.Clamp(channelValue, 0.0, 1.0), - baseHsvColor.V, - baseHsvColor.A); + var hsv = baseHsvColor; + hsv.Saturation = channelValue; + newRgbColor = hsv; } else { @@ -352,11 +322,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep value - newRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - baseHsvColor.S, - Math.Clamp(channelValue, 0.0, 1.0), - baseHsvColor.A); + var hsv = baseHsvColor; + hsv.Value = channelValue; + newRgbColor = hsv; } else { @@ -378,11 +346,9 @@ Color GetColor(double channelValue) if (colorRepresentation == ColorRepresentation.Hsva) { // Sweep alpha - newRgbColor = Helpers.ColorHelper.FromHsv( - baseHsvColor.H, - baseHsvColor.S, - baseHsvColor.V, - Math.Clamp(channelValue, 0.0, 1.0)); + var hsv = baseHsvColor; + hsv.Alpha = channelValue; + newRgbColor = hsv; } else { diff --git a/components/ColorPicker/src/ColorPickerSlider.cs b/components/ColorPicker/src/ColorPickerSlider.cs index 1bff1d26..98e97e82 100644 --- a/components/ColorPicker/src/ColorPickerSlider.cs +++ b/components/ColorPicker/src/ColorPickerSlider.cs @@ -55,7 +55,7 @@ public void UpdateColors() this.UpdateBackground(hsvColor); // Calculate and set the foreground ensuring contrast with the background - Color rgbColor = Helpers.ColorHelper.FromHsv(hsvColor.H, hsvColor.S, hsvColor.V, hsvColor.A); + Color rgbColor = hsvColor; Color selectedRgbColor; double sliderPercent = this.Value / (this.Maximum - this.Minimum); @@ -64,13 +64,7 @@ public void UpdateColors() if (this.IsAlphaMaxForced && this.ColorChannel != ColorChannel.Alpha) { - hsvColor = new HsvColor() - { - H = hsvColor.H, - S = hsvColor.S, - V = hsvColor.V, - A = 1.0 - }; + hsvColor.Alpha = 1.0; } switch (this.ColorChannel) @@ -78,51 +72,39 @@ public void UpdateColors() case ColorChannel.Channel1: { var channelValue = Math.Clamp(sliderPercent * 360.0, 0.0, 360.0); - - hsvColor = new HsvColor() + hsvColor.Hue = channelValue; + if (this.IsSaturationValueMaxForced) { - H = channelValue, - S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V, - A = hsvColor.A - }; + hsvColor.Saturation = 1.0; + hsvColor.Value = 1.0; + } break; } case ColorChannel.Channel2: { var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0); - - hsvColor = new HsvColor() + hsvColor.Saturation = channelValue; + if (this.IsSaturationValueMaxForced) { - H = hsvColor.H, - S = channelValue, - V = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.V, - A = hsvColor.A - }; + hsvColor.Value = 1.0; + } break; } case ColorChannel.Channel3: { var channelValue = Math.Clamp(sliderPercent * 1.0, 0.0, 1.0); - - hsvColor = new HsvColor() + hsvColor.Value = channelValue; + if (this.IsSaturationValueMaxForced) { - H = hsvColor.H, - S = this.IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - V = channelValue, - A = hsvColor.A - }; + hsvColor.Saturation = 1.0; + } break; } } - selectedRgbColor = Helpers.ColorHelper.FromHsv( - hsvColor.H, - hsvColor.S, - hsvColor.V, - hsvColor.A); + selectedRgbColor = hsvColor; } else { diff --git a/components/ColorPicker/src/Converters/AccentColorConverter.cs b/components/ColorPicker/src/Converters/AccentColorConverter.cs index 2c3204fd..785ff53b 100644 --- a/components/ColorPicker/src/Converters/AccentColorConverter.cs +++ b/components/ColorPicker/src/Converters/AccentColorConverter.cs @@ -33,22 +33,13 @@ public static HsvColor GetAccent(HsvColor hsvColor, int accentStep) { if (accentStep != 0) { - double colorValue = hsvColor.V; + double colorValue = hsvColor.Value; colorValue += accentStep * AccentColorConverter.ValueDelta; colorValue = Math.Round(colorValue, 2); - - return new HsvColor() - { - A = Math.Clamp(hsvColor.A, 0.0, 1.0), - H = Math.Clamp(hsvColor.H, 0.0, 360.0), - S = Math.Clamp(hsvColor.S, 0.0, 1.0), - V = Math.Clamp(colorValue, 0.0, 1.0), - }; - } - else - { - return hsvColor; + hsvColor.Value = colorValue; } + + return hsvColor; } /// @@ -63,28 +54,26 @@ public object Convert( HsvColor? hsvColor = null; // Get the current color in HSV - if (value is Color valueColor) - { - rgbColor = valueColor; - } - else if (value is HsvColor valueHsvColor) + switch (value) { - hsvColor = valueHsvColor; - } - else if (value is SolidColorBrush valueBrush) - { - rgbColor = valueBrush.Color; - } - else - { - // Invalid color value provided - return DependencyProperty.UnsetValue; + case Color valueColor: + rgbColor = valueColor; + break; + case HsvColor valueHsvColor: + hsvColor = valueHsvColor; + break; + case SolidColorBrush valueBrush: + rgbColor = valueBrush.Color; + break; + default: + // Invalid color value provided + return DependencyProperty.UnsetValue; } // Get the value component delta try { - accentStep = int.Parse(parameter?.ToString()!, CultureInfo.InvariantCulture); + accentStep = int.Parse(parameter?.ToString()!, CultureInfo.InvariantCulture); } catch { @@ -100,9 +89,7 @@ public object Convert( if (hsvColor != null) { - var hsv = AccentColorConverter.GetAccent(hsvColor.Value, accentStep); - - return Helpers.ColorHelper.FromHsv(hsv.H, hsv.S, hsv.V, hsv.A); + return (Color)GetAccent(hsvColor.Value, accentStep); } else { diff --git a/components/ColorPicker/src/Converters/ColorToHexConverter.cs b/components/ColorPicker/src/Converters/ColorToHexConverter.cs index 5eadf247..bfa5467d 100644 --- a/components/ColorPicker/src/Converters/ColorToHexConverter.cs +++ b/components/ColorPicker/src/Converters/ColorToHexConverter.cs @@ -2,9 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.WinUI.Helpers; using Windows.UI; +using ColorHelper = CommunityToolkit.WinUI.Helpers.ColorHelper; + namespace CommunityToolkit.WinUI.Controls; /// @@ -21,22 +22,20 @@ public object Convert( { Color color; - if (value is Color valueColor) - { - color = valueColor; - } - else if (value is SolidColorBrush valueBrush) + switch (value) { - color = valueBrush.Color; - } - else - { - // Invalid color value provided - return DependencyProperty.UnsetValue; + case Color valueColor: + color = valueColor; + break; + case SolidColorBrush valueBrush: + color = valueBrush.Color; + break; + default: + // Invalid color value provided + return DependencyProperty.UnsetValue; } - string hexColor = color.ToHex().Replace("#", string.Empty); - return hexColor; + return color.ToString().TrimStart('#'); } /// @@ -48,29 +47,13 @@ public object ConvertBack( { string hexValue = value.ToString()!; - if (hexValue.StartsWith("#")) - { - try - { - return hexValue.ToColor(); - } - catch - { - // Invalid hex color value provided - return DependencyProperty.UnsetValue; - } - } - else - { - try - { - return ("#" + hexValue).ToColor(); - } - catch - { - // Invalid hex color value provided - return DependencyProperty.UnsetValue; - } - } + if (!hexValue.StartsWith('#')) + hexValue = $"#{hexValue}"; + + if (ColorHelper.TryParseHexColor(hexValue, out var color)) + return color; + + // Invalid hex color value provided + return DependencyProperty.UnsetValue; } } From 8393fc6e465b13e8e041aa7d885ee0be1a7680fd Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 21:20:52 +0200 Subject: [PATCH 21/22] Moved Color.Mix test into NET10 API block --- components/Helpers/tests/Test_ColorHelper.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/components/Helpers/tests/Test_ColorHelper.cs b/components/Helpers/tests/Test_ColorHelper.cs index 84bf2d51..e6cd1c21 100644 --- a/components/Helpers/tests/Test_ColorHelper.cs +++ b/components/Helpers/tests/Test_ColorHelper.cs @@ -213,13 +213,6 @@ public void Test_ColorHelper_AlphaOver() Assert.AreEqual(Colors.Red.AlphaOver(Colors.Blue, 0.5), Color.FromArgb(255, 128, 0, 128)); } - [TestCategory("Helpers")] - [TestMethod] - public void Test_ColorHelper_Mix() - { - Assert.AreEqual(Color.Mix(Colors.White, Colors.Black, 0.6625), Colors.DarkGray); - } - [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_WithHue() @@ -250,6 +243,13 @@ public void Test_ColorHelper_WithLightness() #if NET10_0_OR_GREATER + [TestCategory("Helpers")] + [TestMethod] + public void Test_ColorHelper_Mix() + { + Assert.AreEqual(Color.Mix(Colors.White, Colors.Black, 0.6625), Colors.DarkGray); + } + [TestCategory("Helpers")] [TestMethod] public void Test_ColorHelper_Add() From db7f6d8cb212cb7ecb19f9f1e8ffc259d56ddd4e Mon Sep 17 00:00:00 2001 From: Adam Dernis Date: Thu, 27 Nov 2025 21:37:15 +0200 Subject: [PATCH 22/22] Changed HslColor and HsvColor constructors to use fields. This fixes a breaking change betwen ColorHelper.FromHsv and HsvColor.Create --- components/Helpers/src/ColorHelper/HslColor.cs | 12 +++++++----- components/Helpers/src/ColorHelper/HsvColor.cs | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/components/Helpers/src/ColorHelper/HslColor.cs b/components/Helpers/src/ColorHelper/HslColor.cs index d6c22aa1..6638fe3d 100644 --- a/components/Helpers/src/ColorHelper/HslColor.cs +++ b/components/Helpers/src/ColorHelper/HslColor.cs @@ -52,7 +52,7 @@ private HslColor(Color color) // Calculate saturation and lightness double lightness = 0.5 * (max + min); double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1)); - + // Set hsl properties Hue = 60 * h1; Saturation = saturation; @@ -71,10 +71,12 @@ private HslColor(Color color) public static HslColor Create(double hue, double saturation, double lightness, double alpha = 1) { HslColor color = default; - color.Hue = hue; - color.Saturation = saturation; - color.Lightness = lightness; - color.Alpha = alpha; +#pragma warning disable 0618 + color.H = hue; + color.S = saturation; + color.L = lightness; + color.A = alpha; +#pragma warning restore 0618 return color; } diff --git a/components/Helpers/src/ColorHelper/HsvColor.cs b/components/Helpers/src/ColorHelper/HsvColor.cs index 27cc0ff1..0e185cf2 100644 --- a/components/Helpers/src/ColorHelper/HsvColor.cs +++ b/components/Helpers/src/ColorHelper/HsvColor.cs @@ -71,10 +71,12 @@ private HsvColor(Color color) public static HsvColor Create(double hue, double saturation, double value, double alpha = 1) { HsvColor color = default; - color.Hue = hue; - color.Saturation = saturation; - color.Value = value; - color.Alpha = alpha; +#pragma warning disable 0618 + color.H = hue; + color.S = saturation; + color.V = value; + color.A = alpha; +#pragma warning restore 0618 return color; }