123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- #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 {
- /// <summary>
- /// Converts the provided <see langword="string" /> to a new <see cref="Color" /> value.
- /// </summary>
- /// <param name="text">
- /// 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 <see cref="Gui.ColorName" /> string values.
- /// </param>
- /// <param name="formatProvider">
- /// If specified and not <see langword="null" />, will be passed to <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)" />.
- /// </param>
- /// <returns>
- /// A <see cref="Color" /> value equivalent to <paramref name="text" />, if parsing was successful.
- /// </returns>
- /// <remarks>
- /// While <see cref="Color" /> supports the alpha channel <see cref="A" />, Terminal.Gui does not.
- /// </remarks>
- /// <exception cref="ArgumentNullException">If <paramref name="text" /> is <see langword="null" />.</exception>
- /// <exception cref="ArgumentException">
- /// If <paramref name="text" /> is an empty string or consists of only whitespace characters.
- /// </exception>
- /// <exception cref="ColorParseException">
- /// If thrown by <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)" />.
- /// </exception>
- [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);
- }
- /// <summary>
- /// Converts the provided <see cref="ReadOnlySpan{T}" /> of <see langword="char" /> to a new <see cref="Color" /> value.
- /// </summary>
- /// <param name="text">
- /// 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 <see cref="Gui.ColorName" /> string values.
- /// </param>
- /// <param name="formatProvider">
- /// Optional <see cref="IFormatProvider" /> to provide parsing services for the input text.
- /// <br />
- /// Defaults to <see cref="CultureInfo.InvariantCulture" /> if <see langword="null" />.
- /// <br />
- /// If not null, must implement <see cref="ICustomColorFormatter" /> or will be ignored and <see cref="CultureInfo.InvariantCulture" /> will
- /// be used.
- /// </param>
- /// <returns>
- /// A <see cref="Color" /> value equivalent to <paramref name="text" />, if parsing was successful.
- /// </returns>
- /// <remarks>
- /// While <see cref="Color" /> supports the alpha channel <see cref="A" />, Terminal.Gui does not.
- /// </remarks>
- /// <exception cref="ArgumentException">
- /// with an inner <see cref="FormatException" /> if <paramref name="text" /> was unable to be successfully parsed as a <see cref="Color" />,
- /// for any reason.
- /// </exception>
- [Pure]
- [SkipLocalsInit]
- public static Color Parse (ReadOnlySpan<char> 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<char> originalString, in int startIndex)
- {
- ReadOnlySpan<char> valuesSubstring = originalString [startIndex..^1];
- Span<Range> 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<char> rSpan, out ReadOnlySpan<char> gSpan, out ReadOnlySpan<char> 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<char> rSpan, out ReadOnlySpan<char> gSpan, out ReadOnlySpan<char> bSpan);
- ReadOnlySpan<char> 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<char> valuesString, in Span<Range> valueComponentRanges, in ReadOnlySpan<char> originalString, out ReadOnlySpan<char> rSpan, out ReadOnlySpan<char> gSpan, out ReadOnlySpan<char> 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));
- }
- }
- }
- }
- /// <inheritdoc />
- [Pure]
- [SkipLocalsInit]
- public static Color Parse (ReadOnlySpan<byte> utf8Text, IFormatProvider? provider)
- {
- return Parse (Encoding.UTF8.GetString (utf8Text), provider);
- }
- /// <inheritdoc cref="object.ToString" />
- /// <summary>
- /// Returns a <see langword="string" /> representation of the current <see cref="Color" /> value, according to the provided
- /// <paramref name="formatString" /> and optional <paramref name="formatProvider" />.
- /// </summary>
- /// <param name="formatString">
- /// A format string that will be passed to <see cref="string.Format(System.IFormatProvider?,string,object?[])" />.<para/>
- /// See remarks for parameters passed to that method.
- /// </param>
- /// <param name="formatProvider">
- /// An optional <see cref="IFormatProvider" /> to use when formatting the <see cref="Color" /> using custom format strings not specified for
- /// this method. Provides this instance as <see cref="Argb" />.
- /// <br />
- /// If this parameter is not null, the specified <see cref="IFormatProvider" /> will be used instead of the custom formatting provided by the
- /// <see cref="Color" /> type.<para/>
- /// See remarks for defined format strings.
- /// </param>
- /// <remarks>
- /// Pre-defined format strings for this method, if a custom <paramref name="formatProvider" /> is not supplied are: <list type="bullet">
- /// <listheader>
- /// <term>Value</term> <description>Result</description>
- /// </listheader> <item>
- /// <term>g or null or empty string</term> <description>
- /// General/default format - Returns a named <see cref="Color" /> if there is a match, or a 24-bit/3-byte/6-hex digit string in
- /// "#RRGGBB" format.
- /// </description>
- /// </item> <item>
- /// <term>G</term> <description>
- /// Extended general format - Returns a named <see cref="Color" /> if there is a match, or a 32-bit/4-byte/8-hex digit string in
- /// "#AARRGGBB" format.
- /// </description>
- /// </item> <item>
- /// <term>d</term> <description>
- /// Decimal format - Returns a 3-component decimal representation of the <see cref="Color" /> in "rgb(R,G,B)" format.
- /// </description>
- /// </item> <item>
- /// <term>D</term> <description>
- /// Extended decimal format - Returns a 4-component decimal representation of the <see cref="Color" /> in "rgba(R,G,B,A)" format.
- /// </description>
- /// </item>
- /// </list>
- /// <para>
- /// If <paramref name="formatProvider" /> is provided and is a non-null <see cref="ICustomColorFormatter" />, the following behaviors are
- /// available, for the specified values of <paramref name="formatString" />: <list type="bullet">
- /// <listheader>
- /// <term>Value</term> <description>Result</description>
- /// </listheader> <item>
- /// <term>null or empty string</term> <description>
- /// Calls <see cref="ICustomColorFormatter.Format(string?,byte,byte,byte,byte)" /> on the provided <paramref name="formatProvider" />
- /// with the null string, and <see cref="R" />, <see cref="G" />, <see cref="B" />, and <see cref="A" /> as typed arguments of type
- /// <see cref="Byte" />.
- /// </description>
- /// </item> <item>
- /// <term>All other values</term> <description>
- /// Calls <see cref="string.Format{TArg0}" /> with the provided <paramref name="formatProvider" /> and
- /// <paramref name="formatString" /> (parsed as a <see cref="CompositeFormat" />), with the value of <see cref="Argb" /> as the sole
- /// <see langword="uint" />-typed argument.
- /// </description>
- /// </item>
- /// </list>
- /// </para>
- /// </remarks>
- [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}");
- }
- /// <inheritdoc />
- /// <remarks>
- /// <para>
- /// This method should be used only when absolutely necessary, because it <b>always</b> has more overhead than
- /// <see cref="ToString(string?,System.IFormatProvider?)" />, as this method results in an intermediate allocation of one or more instances
- /// of <see langword="string" /> and a copy of that string to <paramref name="destination" /> if formatting was successful.
- /// <br />
- /// When possible, use <see cref="ToString(string?,System.IFormatProvider?)" />, which attempts to avoid intermediate allocations.
- /// </para>
- /// <para>
- /// This method only returns <see langword="true" /> and with its output written to <paramref name="destination" /> if the formatted
- /// string, <i>in its entirety</i>, will fit in <paramref name="destination" />. If the resulting formatted string is too large to fit in
- /// <paramref name="destination" />, the result will be false and <paramref name="destination" /> will be unaltered.
- /// </para>
- /// <para>
- /// The resulting formatted string may be <b>shorter</b> than <paramref name="destination" />. When this method returns
- /// <see langword="true" />, use <paramref name="charsWritten" /> when handling the value of <paramref name="destination" />.
- /// </para>
- /// </remarks>
- [Pure]
- [SkipLocalsInit]
- public bool TryFormat (Span<char> destination, out int charsWritten, ReadOnlySpan<char> 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;
- }
- /// <inheritdoc />
- /// <remarks>
- /// Use of this method involves a stack allocation of <paramref name="utf8Destination" />.Length * 2 bytes. Use of the overload taking a char
- /// span is recommended.
- /// </remarks>
- [SkipLocalsInit]
- public bool TryFormat (Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
- {
- Span<char> 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;
- }
- /// <summary>
- /// Converts the provided <see langword="string" /> to a new <see cref="Color" /> value.
- /// </summary>
- /// <param name="text">
- /// 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 <see cref="GetClosestNamedColor" /> string values.
- /// </param>
- /// <param name="formatProvider">
- /// Optional <see cref="IFormatProvider" /> to provide formatting services for the input text.
- /// <br />
- /// Defaults to <see cref="CultureInfo.InvariantCulture" /> if <see langword="null" />.
- /// </param>
- /// <param name="result">
- /// The parsed value, if successful, or <see langword="default" />(<see cref="Color" />), if unsuccessful.
- /// </param>
- /// <returns>A <see langword="bool" /> value indicating whether parsing was successful.</returns>
- /// <remarks>
- /// While <see cref="Color" /> supports the alpha channel <see cref="A" />, Terminal.Gui does not.
- /// </remarks>
- [Pure]
- [SkipLocalsInit]
- public static bool TryParse (string? text, IFormatProvider? formatProvider, out Color result)
- {
- return TryParse (text.AsSpan (), formatProvider ?? CultureInfo.InvariantCulture, out result);
- }
- /// <summary>
- /// Converts the provided <see cref="ReadOnlySpan{T}" /> of <see langword="char" /> to a new <see cref="Color" /> value.
- /// </summary>
- /// <param name="text">
- /// 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 <see cref="GetClosestNamedColor" /> string values.
- /// </param>
- /// <param name="formatProvider">
- /// If specified and not <see langword="null" />, will be passed to <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)" />.
- /// </param>
- /// <param name="color">
- /// The parsed value, if successful, or <see langword="default" />(<see cref="Color" />), if unsuccessful.
- /// </param>
- /// <returns>A <see langword="bool" /> value indicating whether parsing was successful.</returns>
- /// <remarks>
- /// While <see cref="Color" /> supports the alpha channel <see cref="A" />, Terminal.Gui does not.
- /// </remarks>
- [Pure]
- [SkipLocalsInit]
- public static bool TryParse (ReadOnlySpan<char> text, IFormatProvider? formatProvider, out Color color)
- {
- try {
- Color c = Parse (text, formatProvider);
- color = c;
- return true;
- }
- catch ( ColorParseException ) {
- color = default;
- return false;
- }
- }
- /// <inheritdoc />
- [Pure]
- [SkipLocalsInit]
- public static bool TryParse (ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, out Color result)
- {
- return TryParse (Encoding.UTF8.GetString (utf8Text), provider, out result);
- }
- /// <summary>Converts the color to a string representation.</summary>
- /// <remarks>
- /// <para>
- /// If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string.
- /// </para>
- /// <para>
- /// <see cref="A" /> (Alpha channel) is ignored and the returned string will not include it for this overload.
- /// </para>
- /// </remarks>
- /// <returns>The string representation of this value in #RRGGBB format.</returns>
- [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}";
- }
- /// <summary>Converts the provided string to a new <see cref="Color" /> instance.</summary>
- /// <param name="text">
- /// 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 <see cref="Gui.ColorName" /> string values.
- /// </param>
- /// <param name="color">The parsed value.</param>
- /// <returns>A boolean value indicating whether parsing was successful.</returns>
- /// <remarks>
- /// While <see cref="Color" /> supports the alpha channel <see cref="A" />, Terminal.Gui does not.
- /// </remarks>
- 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;
- }
- }
|