diff --git a/src/Avalonia.Visuals/Media/HslColor.cs b/src/Avalonia.Visuals/Media/HslColor.cs index af4a9e20f4..f5d2abc2dc 100644 --- a/src/Avalonia.Visuals/Media/HslColor.cs +++ b/src/Avalonia.Visuals/Media/HslColor.cs @@ -169,6 +169,146 @@ namespace Avalonia.Media return HslColor.ToRgb(H, S, L, A); } + /// + public override string ToString() + { + var sb = new StringBuilder(); + + // Use a format similar to CSS. However: + // - To ensure precision is never lost, allow decimal places. + // This is especially important for round-trip serialization. + // - To maintain numerical consistency, do not use percent. + // + // Example: + // + // hsla(hue, saturation, lightness, alpha) + // hsla(230, 1.0, 0.5, 1.0) + // + + sb.Append("hsva("); + sb.Append(H.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(S.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(L.ToString(CultureInfo.InvariantCulture)); + sb.Append(", "); + sb.Append(A.ToString(CultureInfo.InvariantCulture)); + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Parses an HSL color string. + /// + /// The HSL color string to parse. + /// The parsed . + public static HslColor Parse(string s) + { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + + if (TryParse(s, out HslColor hslColor)) + { + return hslColor; + } + + throw new FormatException($"Invalid HSL color string: '{s}'."); + } + + /// + /// Parses an HSL color string. + /// + /// The HSL color string to parse. + /// The parsed . + /// True if parsing was successful; otherwise, false. + public static bool TryParse(string s, out HslColor hslColor) + { + hslColor = default; + + if (s is null) + { + return false; + } + + string workingString = s.Trim(); + + if (workingString.Length == 0 || + workingString.IndexOf(",", StringComparison.Ordinal) < 0) + { + return false; + } + + if (workingString.Length > 6 && + workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(5, workingString.Length - 6); + } + + if (workingString.Length > 5 && + workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) && + workingString.EndsWith(")", StringComparison.Ordinal)) + { + workingString = workingString.Substring(4, workingString.Length - 5); + } + + string[] components = workingString.Split(','); + + if (components.Length == 3) // HSL + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + TryInternalParse(components[1], out double saturation) && + TryInternalParse(components[2], out double lightness)) + { + hslColor = new HslColor(1.0, hue, saturation, lightness); + return true; + } + } + else if (components.Length == 4) // HSLA + { + if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) && + TryInternalParse(components[1], out double saturation) && + TryInternalParse(components[2], out double lightness) && + TryInternalParse(components[3], out double alpha)) + { + hslColor = new HslColor(alpha, hue, saturation, lightness); + return true; + } + } + + // Local function to specially parse a double value with an optional percentage sign + bool TryInternalParse(string inString, out double outDouble) + { + // The percent sign, if it exists, must be at the end of the number + int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + + if (percentIndex >= 0) + { + var result = double.TryParse( + inString.Substring(0, percentIndex), + NumberStyles.Number, + CultureInfo.InvariantCulture, + out double percentage); + + outDouble = percentage / 100.0; + return result; + } + else + { + return double.TryParse( + inString, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out outDouble); + } + } + + return false; + } + /// /// Creates a new from individual color component values. ///