#nullable enable using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.Runtime.CompilerServices; namespace Terminal.Gui; public readonly partial record struct Color { /// /// /// Returns a representation of the current value, according to the /// provided and optional . /// /// /// A format string that will be passed to /// . /// /// See remarks for parameters passed to that method. /// /// /// An optional to use when formatting the /// using custom format strings not specified for this method. Provides this instance as .
If /// this parameter is not null, the specified will be used instead of the custom /// formatting provided by the type. /// /// See remarks for defined format strings. /// /// /// Pre-defined format strings for this method, if a custom is not supplied are: /// /// /// Value Result /// /// /// g or null or empty string /// /// General/default format - Returns a named if there is a match, or a /// 24-bit/3-byte/6-hex digit string in "#RRGGBB" format. /// /// /// /// G /// /// Extended general format - Returns a named if there is a match, or a /// 32-bit/4-byte/8-hex digit string in "#AARRGGBB" format. /// /// /// /// d /// /// Decimal format - Returns a 3-component decimal representation of the in /// "rgb(R,G,B)" format. /// /// /// /// D /// /// Extended decimal format - Returns a 4-component decimal representation of the /// in "rgba(R,G,B,A)" format. /// /// /// /// /// If is provided and is a non-null , the /// following behaviors are available, for the specified values of : /// /// /// Value Result /// /// /// null or empty string /// /// Calls on the /// provided with the null string, and , /// , , and as typed arguments of type /// . /// /// /// /// All other values /// /// Calls with the provided /// and (parsed as a /// ), with the value of as the sole /// -typed argument. /// /// /// /// /// [SkipLocalsInit] public string ToString ( [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? formatString, IFormatProvider? formatProvider = null ) { return (formatString, formatProvider) switch { // Null or empty string and null formatProvider - Revert to 'g' case behavior (null or { Length: 0 }, null) => ToString (), // Null or empty string and formatProvider is an ICustomColorFormatter - Output according to the given ICustomColorFormatted, with R, G, B, and A as typed arguments (null or { Length: 0 }, ICustomColorFormatter ccf) => ccf.Format (null, R, G, B, A), // Null or empty string and formatProvider is otherwise non-null but not the invariant culture - Output according to string.Format with the given IFormatProvider and R, G, B, and A as boxed arguments, with string.Empty as the format string (null or { Length: 0 }, { }) when !Equals (formatProvider, CultureInfo.InvariantCulture) => string.Format (formatProvider, formatString ?? string.Empty, R, G, B, A), // Null or empty string and formatProvider is the invariant culture - Output according to string.Format with the given IFormatProvider and R, G, B, and A as boxed arguments, with string.Empty as the format string (null or { Length: 0 }, { }) when Equals (formatProvider, CultureInfo.InvariantCulture) => $"#{R:X2}{G:X2}{B:X2}", // Non-null string and non-null formatProvider - let formatProvider handle it and give it R, G, B, and A ({ }, { }) => string.Format (formatProvider, CompositeFormat.Parse (formatString), R, G, B, A), // g format string and null formatProvider - Output as 24-bit hex according to invariant culture rules from R, G, and B (['g'], null) => ToString (), // G format string and null formatProvider - Output as 32-bit hex according to invariant culture rules from Argb (['G'], null) => $"#{A:X2}{R:X2}{G:X2}{B:X2}", // d format string and null formatProvider - Output as 24-bit decimal rgb(r,g,b) according to invariant culture rules from R, G, and B (['d'], null) => $"rgb({R:D},{G:D},{B:D})", // D format string and null formatProvider - Output as 32-bit decimal rgba(r,g,b,a) according to invariant culture rules from R, G, B, and A. Non-standard: a is a decimal byte value. (['D'], null) => $"rgba({R:D},{G:D},{B:D},{A:D})", // All other cases (formatString is not null here) - Delegate to formatProvider, first, and otherwise to invariant culture, and try to format the provided string from the channels ({ }, _) => string.Format ( formatProvider ?? CultureInfo.InvariantCulture, CompositeFormat.Parse (formatString), R, G, B, A ), _ => throw new InvalidOperationException ( $"Unable to create string from Color with value {Argb}, using format string {formatString}" ) } ?? throw new InvalidOperationException ( $"Unable to create string from Color with value {Argb}, using format string {formatString}" ); } /// /// /// /// This method should be used only when absolutely necessary, because it always has more overhead than /// , as this method results in an intermediate allocation /// of one or more instances of and a copy of that string to /// if formatting was successful.
When possible, use /// , which attempts to avoid intermediate allocations. ///
/// /// This method only returns and with its output written to /// if the formatted string, in its entirety, will fit in . If the resulting /// formatted string is too large to fit in , the result will be false and /// will be unaltered. /// /// /// The resulting formatted string may be shorter than . When this method /// returns , use when handling the value of /// . /// ///
[Pure] [SkipLocalsInit] public bool TryFormat ( Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider ) { // TODO: This can probably avoid a string allocation with a little more work try { string formattedString = ToString (format.ToString (), provider); if (formattedString.Length <= destination.Length) { formattedString.CopyTo (destination); charsWritten = formattedString.Length; return true; } } catch { destination.Clear (); charsWritten = 0; return false; } destination.Clear (); charsWritten = 0; return false; } /// Converts the provided to a new value. /// /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)", /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the string values. /// /// /// If specified and not , will be passed to /// . /// /// A value equivalent to , if parsing was successful. /// While supports the alpha channel , Terminal.Gui does not. /// If is . /// /// If is an empty string or consists of only whitespace /// characters. /// /// /// If thrown by /// . /// [Pure] [SkipLocalsInit] public static Color Parse (string? text, IFormatProvider? formatProvider = null) { ArgumentException.ThrowIfNullOrWhiteSpace (text, nameof (text)); if (text is { Length: < 3 } && formatProvider is null) { throw new ColorParseException ( text, reason: "Provided text is too short to be any known color format.", badValue: text ); } return Parse (text.AsSpan (), formatProvider ?? CultureInfo.InvariantCulture); } /// /// Converts the provided of to a new /// value. /// /// /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#RGBA", "#AARRGGBB", "rgb(r,g,b)", /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the string values. /// /// /// Optional to provide parsing services for the input text. ///
Defaults to if .
If not null, must /// implement or will be ignored and /// will be used. /// /// A value equivalent to , if parsing was successful. /// While supports the alpha channel , Terminal.Gui does not. /// /// with an inner if was unable /// to be successfully parsed as a , for any reason. /// [Pure] [SkipLocalsInit] public static Color Parse (ReadOnlySpan text, IFormatProvider? formatProvider = null) { return text switch { // Null string or empty span provided { IsEmpty: true } when formatProvider is null => throw new ColorParseException ( in text, "The text provided was null or empty.", in text ), // A valid ICustomColorFormatter was specified and the text wasn't null or empty { IsEmpty: false } when formatProvider is ICustomColorFormatter f => f.Parse (text), // Input string is only whitespace { Length: > 0 } when text.IsWhiteSpace () => throw new ColorParseException ( in text, "The text provided consisted of only whitespace characters.", in text ), // Any string too short to possibly be any supported format. { Length: > 0 and < 4 } => throw new ColorParseException ( in text, "Text was too short to be any possible supported format.", in text ), // The various hexadecimal cases ['#', ..] hexString => hexString switch { // #RGB ['#', var rChar, var gChar, var bChar] chars when chars [1..] .IsAllAsciiHexDigits () => new Color ( byte.Parse ([rChar, rChar], NumberStyles.HexNumber), byte.Parse ([gChar, gChar], NumberStyles.HexNumber), byte.Parse ([bChar, bChar], NumberStyles.HexNumber) ), // #ARGB ['#', var aChar, var rChar, var gChar, var bChar] chars when chars [1..] .IsAllAsciiHexDigits () => new Color ( byte.Parse ([rChar, rChar], NumberStyles.HexNumber), byte.Parse ([gChar, gChar], NumberStyles.HexNumber), byte.Parse ([bChar, bChar], NumberStyles.HexNumber), byte.Parse ([aChar, aChar], NumberStyles.HexNumber) ), // #RRGGBB [ '#', var r1Char, var r2Char, var g1Char, var g2Char, var b1Char, var b2Char ] chars when chars [1..].IsAllAsciiHexDigits () => new Color ( byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber), byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber), byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber) ), // #AARRGGBB [ '#', var a1Char, var a2Char, var r1Char, var r2Char, var g1Char, var g2Char, var b1Char, var b2Char ] chars when chars [1..].IsAllAsciiHexDigits () => new Color ( byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber), byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber), byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber), byte.Parse ([a1Char, a2Char], NumberStyles.HexNumber) ), _ => throw new ColorParseException ( in hexString, $"Color hex string {hexString} was not in a supported format", in hexString ) }, // rgb(r,g,b) or rgb(r,g,b,a) ['r', 'g', 'b', '(', .., ')'] => ParseRgbaFormat (in text, 4), // rgba(r,g,b,a) or rgba(r,g,b) ['r', 'g', 'b', 'a', '(', .., ')'] => ParseRgbaFormat (in text, 5), // Attempt to parse as a named color from the ColorName enum { } when char.IsLetter (text [0]) && Enum.TryParse (text, true, out ColorName colorName) => new Color (colorName), // Any other input _ => throw new ColorParseException (in text, "Text did not match any expected format.", in text, []) }; [Pure] [SkipLocalsInit] static Color ParseRgbaFormat (in ReadOnlySpan originalString, in int startIndex) { ReadOnlySpan valuesSubstring = originalString [startIndex..^1]; Span valueRanges = stackalloc Range [4]; int rangeCount = valuesSubstring.Split ( valueRanges, ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); switch (rangeCount) { case 3: { // rgba(r,g,b) ParseRgbValues ( in valuesSubstring, in valueRanges, in originalString, out ReadOnlySpan rSpan, out ReadOnlySpan gSpan, out ReadOnlySpan bSpan ); return new Color (int.Parse (rSpan), int.Parse (gSpan), int.Parse (bSpan)); } case 4: { // rgba(r,g,b,a) ParseRgbValues ( in valuesSubstring, in valueRanges, in originalString, out ReadOnlySpan rSpan, out ReadOnlySpan gSpan, out ReadOnlySpan bSpan ); ReadOnlySpan aSpan = valuesSubstring [valueRanges [3]]; if (!aSpan.IsAllAsciiDigits ()) { throw new ColorParseException ( in originalString, "Value was not composed entirely of decimal digits.", in aSpan, nameof (A) ); } return new Color (int.Parse (rSpan), int.Parse (gSpan), int.Parse (bSpan), int.Parse (aSpan)); } default: throw new ColorParseException ( in originalString, $"Wrong number of values. Expected 3 or 4 decimal integers. Got {rangeCount}.", in originalString ); } [Pure] [SkipLocalsInit] static void ParseRgbValues ( in ReadOnlySpan valuesString, in Span valueComponentRanges, in ReadOnlySpan originalString, out ReadOnlySpan rSpan, out ReadOnlySpan gSpan, out ReadOnlySpan bSpan ) { rSpan = valuesString [valueComponentRanges [0]]; if (!rSpan.IsAllAsciiDigits ()) { throw new ColorParseException ( in originalString, "Value was not composed entirely of decimal digits.", in rSpan, nameof (R) ); } gSpan = valuesString [valueComponentRanges [1]]; if (!gSpan.IsAllAsciiDigits ()) { throw new ColorParseException ( in originalString, "Value was not composed entirely of decimal digits.", in gSpan, nameof (G) ); } bSpan = valuesString [valueComponentRanges [2]]; if (!bSpan.IsAllAsciiDigits ()) { throw new ColorParseException ( in originalString, "Value was not composed entirely of decimal digits.", in bSpan, nameof (B) ); } } } } /// Converts the provided to a new value. /// /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)", /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the string /// values. /// /// /// Optional to provide formatting services for the input text. ///
Defaults to if . /// /// /// The parsed value, if successful, or (), if /// unsuccessful. /// /// A value indicating whether parsing was successful. /// While supports the alpha channel , Terminal.Gui does not. [Pure] [SkipLocalsInit] public static bool TryParse (string? text, IFormatProvider? formatProvider, out Color result) { return TryParse ( text.AsSpan (), formatProvider ?? CultureInfo.InvariantCulture, out result ); } /// /// Converts the provided of to a new /// value. /// /// /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)", /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the string /// values. /// /// /// If specified and not , will be passed to /// . /// /// /// The parsed value, if successful, or (), if /// unsuccessful. /// /// A value indicating whether parsing was successful. /// While supports the alpha channel , Terminal.Gui does not. [Pure] [SkipLocalsInit] public static bool TryParse (ReadOnlySpan text, IFormatProvider? formatProvider, out Color color) { try { Color c = Parse (text, formatProvider); color = c; return true; } catch (ColorParseException) { color = default (Color); return false; } } /// /// /// Use of this method involves a stack allocation of .Length * 2 bytes. Use of /// the overload taking a char span is recommended. /// [SkipLocalsInit] public bool TryFormat ( Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider ) { Span charDestination = stackalloc char [utf8Destination.Length * 2]; if (TryFormat (charDestination, out int charsWritten, format, provider)) { Encoding.UTF8.GetBytes (charDestination, utf8Destination); bytesWritten = charsWritten / 2; return true; } utf8Destination.Clear (); bytesWritten = 0; return false; } /// [Pure] [SkipLocalsInit] public static Color Parse (ReadOnlySpan utf8Text, IFormatProvider? provider) { return Parse (Encoding.UTF8.GetString (utf8Text), provider); } /// [Pure] [SkipLocalsInit] public static bool TryParse (ReadOnlySpan utf8Text, IFormatProvider? provider, out Color result) { return TryParse (Encoding.UTF8.GetString (utf8Text), provider, out result); } /// Converts the color to a string representation. /// /// If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string. /// (Alpha channel) is ignored and the returned string will not include it for this overload. /// /// The string representation of this value in #RRGGBB format. [Pure] [SkipLocalsInit] public override string ToString () { // If Values has an exact match with a named color (in _colorNames), use that. return ColorExtensions.ColorToNameMap.TryGetValue (this, out ColorName colorName) ? Enum.GetName (typeof (ColorName), colorName) ?? $"#{R:X2}{G:X2}{B:X2}" : // Otherwise return as an RGB hex value. $"#{R:X2}{G:X2}{B:X2}"; } /// Converts the provided string to a new instance. /// /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)", /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the string values. /// /// The parsed value. /// A boolean value indicating whether parsing was successful. /// While supports the alpha channel , Terminal.Gui does not. public static bool TryParse (string text, [NotNullWhen (true)] out Color? color) { if (TryParse (text.AsSpan (), null, out Color c)) { color = c; return true; } color = null; return false; } }