#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;
}
}