Browse Source

Merge branch 'v2_develop' into v2_fixes_2432_Dim_AutoSize

Tig Kindel 1 year ago
parent
commit
c2ccf3f73b
45 changed files with 2355 additions and 1376 deletions
  1. 3 5
      Terminal.Gui/Configuration/ColorJsonConverter.cs
  2. 1 1
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  3. 2 2
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  4. 1 1
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  5. 1 1
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  6. 87 0
      Terminal.Gui/Drawing/AnsiColorCode.cs
  7. 171 0
      Terminal.Gui/Drawing/Attribute.cs
  8. 424 0
      Terminal.Gui/Drawing/Color.Formatting.cs
  9. 90 0
      Terminal.Gui/Drawing/Color.Operators.cs
  10. 162 770
      Terminal.Gui/Drawing/Color.cs
  11. 74 0
      Terminal.Gui/Drawing/ColorExtensions.cs
  12. 97 0
      Terminal.Gui/Drawing/ColorName.cs
  13. 92 0
      Terminal.Gui/Drawing/ColorParseException.cs
  14. 30 24
      Terminal.Gui/Drawing/ColorScheme.cs
  15. 29 0
      Terminal.Gui/Drawing/ICustomColorFormatter.cs
  16. 6 2
      Terminal.Gui/Terminal.Gui.csproj
  17. 27 3
      Terminal.Gui/Text/StringExtensions.cs
  18. 102 30
      Terminal.Gui/Views/DateField.cs
  19. 60 13
      Terminal.Gui/Views/DatePicker.cs
  20. 14 8
      Terminal.Gui/Views/FileSystemColorProvider.cs
  21. 8 35
      Terminal.Gui/Views/TextView.cs
  22. 1 1
      Terminal.Gui/Views/TimeField.cs
  23. 0 132
      Terminal.sln.DotSettings
  24. 4 4
      UICatalog/Scenarios/Adornments.cs
  25. 3 3
      UICatalog/Scenarios/BasicColors.cs
  26. 2 2
      UICatalog/Scenarios/ColorPicker.cs
  27. 2 2
      UICatalog/Scenarios/ProgressBarStyles.cs
  28. 5 5
      UICatalog/Scenarios/TrueColors.cs
  29. 15 0
      UnitTests/.filenesting.json
  30. 5 6
      UnitTests/Configuration/JsonConverterTests.cs
  31. 1 1
      UnitTests/Configuration/ThemeTests.cs
  32. 173 0
      UnitTests/Drawing/ColorTests.Constructors.cs
  33. 180 0
      UnitTests/Drawing/ColorTests.Operators.cs
  34. 148 0
      UnitTests/Drawing/ColorTests.ParsingAndFormatting.cs
  35. 135 0
      UnitTests/Drawing/ColorTests.TypeChecks.cs
  36. 43 292
      UnitTests/Drawing/ColorTests.cs
  37. 7 0
      UnitTests/UnitTests.csproj
  38. 4 4
      UnitTests/View/Adornment/BorderTests.cs
  39. 4 5
      UnitTests/View/Adornment/MarginTests.cs
  40. 2 2
      UnitTests/View/Adornment/PaddingTests.cs
  41. 74 0
      UnitTests/Views/DateFieldTests.cs
  42. 60 18
      UnitTests/Views/DatePickerTests.cs
  43. 1 1
      UnitTests/Views/RuneCellTests.cs
  44. 1 1
      UnitTests/Views/TimeFieldTests.cs
  45. 4 2
      docfx/docfx.json

+ 3 - 5
Terminal.Gui/Configuration/ColorJsonConverter.cs

@@ -1,7 +1,5 @@
-using System;
 using System.Text.Json.Serialization;
 using System.Text.Json;
-using System.Text.RegularExpressions;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -28,14 +26,14 @@ namespace Terminal.Gui {
 			// Check if the value is a string
 			if (reader.TokenType == JsonTokenType.String) {
 				// Get the color string
-				var colorString = reader.GetString ();
+				ReadOnlySpan<char> colorString = reader.GetString ();
 
 				// Check if the color string is a color name
 				if (Enum.TryParse (colorString, ignoreCase: true, out ColorName color)) {
 					// Return the parsed color
-					return new Color(color);
+					return new Color(in color);
 				}
-				if (Color.TryParse (colorString, out Color parsedColor)) {
+				if (Color.TryParse(colorString,null, out Color parsedColor)) {
 					return parsedColor;
 				}
 				throw new JsonException ($"Unexpected color name: {colorString}.");

+ 1 - 1
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -186,7 +186,7 @@ class CursesDriver : ConsoleDriver {
 	public override Attribute MakeColor (Color foreground, Color background)
 	{
 		if (!RunningUnitTests) {
-			return MakeColor (ColorNameToCursesColorNumber (foreground.ColorName), ColorNameToCursesColorNumber (background.ColorName));
+			return MakeColor (ColorNameToCursesColorNumber (foreground.GetClosestNamedColor ()), ColorNameToCursesColorNumber (background.GetClosestNamedColor ()));
 		} else {
 			return new Attribute (
 				0,

+ 2 - 2
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -134,8 +134,8 @@ public class FakeDriver : ConsoleDriver {
 					// Performance: Only send the escape sequence if the attribute has changed.
 					if (attr != redrawAttr) {
 						redrawAttr = attr;
-						FakeConsole.ForegroundColor = (ConsoleColor)attr.Foreground.ColorName;
-						FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.ColorName;
+						FakeConsole.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor ();
+						FakeConsole.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor ();
 					}
 					outputWidth++;
 					var rune = (Rune)Contents [row, col].Rune;

+ 1 - 1
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -853,7 +853,7 @@ class NetDriver : ConsoleDriver {
 
 						if (Force16Colors) {
 							output.Append (EscSeqUtils.CSI_SetGraphicsRendition (
-								MapColors ((ConsoleColor)attr.Background.ColorName, false), MapColors ((ConsoleColor)attr.Foreground.ColorName, true)));
+												MapColors ((ConsoleColor)attr.Background.GetClosestNamedColor (), false), MapColors ((ConsoleColor)attr.Foreground.GetClosestNamedColor ())));
 						} else {
 							output.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
 							output.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));

+ 1 - 1
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -68,7 +68,7 @@ internal class WindowsConsole {
 			foreach (ExtendedCharInfo info in charInfoBuffer) {
 				ci [i++] = new CharInfo () {
 					Char = new CharUnion () { UnicodeChar = info.Char },
-					Attributes = (ushort)(((int)info.Attribute.Foreground.ColorName) | ((int)info.Attribute.Background.ColorName << 4))
+					Attributes = (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor () | (int)info.Attribute.Background.GetClosestNamedColor () << 4)
 				};
 			}
 

+ 87 - 0
Terminal.Gui/Drawing/AnsiColorCode.cs

@@ -0,0 +1,87 @@
+namespace Terminal.Gui;
+
+/// <summary>
+/// The 16 foreground color codes used by ANSI Esc sequences for 256 color terminals. Add 10 to these values for background
+/// color.
+/// </summary>
+public enum AnsiColorCode {
+	/// <summary>
+	/// The ANSI color code for Black.
+	/// </summary>
+	BLACK = 30,
+
+	/// <summary>
+	/// The ANSI color code for Red.
+	/// </summary>
+	RED = 31,
+
+	/// <summary>
+	/// The ANSI color code for Green.
+	/// </summary>
+	GREEN = 32,
+
+	/// <summary>
+	/// The ANSI color code for Yellow.
+	/// </summary>
+	YELLOW = 33,
+
+	/// <summary>
+	/// The ANSI color code for Blue.
+	/// </summary>
+	BLUE = 34,
+
+	/// <summary>
+	/// The ANSI color code for Magenta.
+	/// </summary>
+	MAGENTA = 35,
+
+	/// <summary>
+	/// The ANSI color code for Cyan.
+	/// </summary>
+	CYAN = 36,
+
+	/// <summary>
+	/// The ANSI color code for White.
+	/// </summary>
+	WHITE = 37,
+
+	/// <summary>
+	/// The ANSI color code for Bright Black.
+	/// </summary>
+	BRIGHT_BLACK = 90,
+
+	/// <summary>
+	/// The ANSI color code for Bright Red.
+	/// </summary>
+	BRIGHT_RED = 91,
+
+	/// <summary>
+	/// The ANSI color code for Bright Green.
+	/// </summary>
+	BRIGHT_GREEN = 92,
+
+	/// <summary>
+	/// The ANSI color code for Bright Yellow.
+	/// </summary>
+	BRIGHT_YELLOW = 93,
+
+	/// <summary>
+	/// The ANSI color code for Bright Blue.
+	/// </summary>
+	BRIGHT_BLUE = 94,
+
+	/// <summary>
+	/// The ANSI color code for Bright Magenta.
+	/// </summary>
+	BRIGHT_MAGENTA = 95,
+
+	/// <summary>
+	/// The ANSI color code for Bright Cyan.
+	/// </summary>
+	BRIGHT_CYAN = 96,
+
+	/// <summary>
+	/// The ANSI color code for Bright White.
+	/// </summary>
+	BRIGHT_WHITE = 97
+}

+ 171 - 0
Terminal.Gui/Drawing/Attribute.cs

@@ -0,0 +1,171 @@
+#nullable enable
+using System.Text.Json.Serialization;
+namespace Terminal.Gui;
+
+/// <summary>
+/// Attributes represent how text is styled when displayed in the terminal.
+/// </summary>
+/// <remarks>
+/// <see cref="Attribute"/> provides a platform independent representation of colors (and someday other forms of text
+/// styling).
+/// They encode both the foreground and the background color and are used in the <see cref="ColorScheme"/>
+/// class to define color schemes that can be used in an application.
+/// </remarks>
+[JsonConverter (typeof (AttributeJsonConverter))]
+public readonly struct Attribute : IEquatable<Attribute> {
+	/// <summary>
+	/// Default empty attribute.
+	/// </summary>
+	public static readonly Attribute Default = new (Color.White, ColorName.Black);
+
+	/// <summary>
+	/// The <see cref="ConsoleDriver"/>-specific color value.
+	/// </summary>
+	[JsonIgnore (Condition = JsonIgnoreCondition.Always)]
+	internal int PlatformColor { get; }
+
+	/// <summary>
+	/// The foreground color.
+	/// </summary>
+	[JsonConverter (typeof (ColorJsonConverter))]
+	public Color Foreground { get; }
+
+	/// <summary>
+	/// The background color.
+	/// </summary>
+	[JsonConverter (typeof (ColorJsonConverter))]
+	public Color Background { get; }
+
+	/// <summary>
+	/// Initializes a new instance with default values.
+	/// </summary>
+	public Attribute ()
+	{
+		PlatformColor = -1;
+		Foreground = Default.Foreground;
+		Background = Default.Background;
+	}
+
+	/// <summary>
+	/// Initializes a new instance from an existing instance.
+	/// </summary>
+	public Attribute (in Attribute attr)
+	{
+		PlatformColor = -1;
+		Foreground = attr.Foreground;
+		Background = attr.Background;
+	}
+
+	/// <summary>
+	/// Initializes a new instance with platform specific color value.
+	/// </summary>
+	/// <param name="platformColor">Value.</param>
+	internal Attribute (int platformColor)
+	{
+		PlatformColor = platformColor;
+		Foreground = Default.Foreground;
+		Background = Default.Background;
+	}
+
+	/// <summary>
+	/// Initializes a new instance of the <see cref="Attribute"/> struct.
+	/// </summary>
+	/// <param name="platformColor">platform-dependent color value.</param>
+	/// <param name="foreground">Foreground</param>
+	/// <param name="background">Background</param>
+	internal Attribute (int platformColor, in Color foreground, in Color background)
+	{
+		Foreground = foreground;
+		Background = background;
+		PlatformColor = platformColor;
+	}
+
+	/// <summary>
+	/// Initializes a new instance of the <see cref="Attribute"/> struct.
+	/// </summary>
+	/// <param name="foreground">Foreground</param>
+	/// <param name="background">Background</param>
+	public Attribute (Color foreground, Color background)
+	{
+		Foreground = foreground;
+		Background = background;
+
+		// TODO: Once CursesDriver supports truecolor all the PlatformColor stuff goes away
+		if (Application.Driver == null) {
+			PlatformColor = -1;
+			return;
+		}
+
+		var make = Application.Driver.MakeColor (foreground, background);
+		PlatformColor = make.PlatformColor;
+	}
+
+	/// <summary>
+	/// Initializes a new instance with a <see cref="ColorName"/> value. Both <see cref="Foreground"/> and
+	/// <see cref="Background"/> will be set to the specified color.
+	/// </summary>
+	/// <param name="colorName">Value.</param>
+	internal Attribute (ColorName colorName) : this (colorName, colorName) { }
+
+	/// <summary>
+	/// Initializes a new instance of the <see cref="Attribute"/> struct.
+	/// </summary>
+	/// <param name="foregroundName">Foreground</param>
+	/// <param name="backgroundName">Background</param>
+	public Attribute (in ColorName foregroundName, in ColorName backgroundName) : this (new Color (foregroundName), new Color (backgroundName)) { }
+
+
+	/// <summary>
+	/// Initializes a new instance of the <see cref="Attribute"/> struct.
+	/// </summary>
+	/// <param name="foregroundName">Foreground</param>
+	/// <param name="background">Background</param>
+	public Attribute (ColorName foregroundName, Color background) : this (new Color (foregroundName), background) { }
+
+	/// <summary>
+	/// Initializes a new instance of the <see cref="Attribute"/> struct.
+	/// </summary>
+	/// <param name="foreground">Foreground</param>
+	/// <param name="backgroundName">Background</param>
+	public Attribute (Color foreground, ColorName backgroundName) : this (foreground, new Color (backgroundName)) { }
+
+	/// <summary>
+	/// Initializes a new instance of the <see cref="Attribute"/> struct
+	/// with the same colors for the foreground and background.
+	/// </summary>
+	/// <param name="color">The color.</param>
+	public Attribute (Color color) : this (color, color) { }
+
+
+	/// <summary>
+	/// Compares two attributes for equality.
+	/// </summary>
+	/// <param name="left"></param>
+	/// <param name="right"></param>
+	/// <returns></returns>
+	public static bool operator == (Attribute left, Attribute right) => left.Equals (right);
+
+	/// <summary>
+	/// Compares two attributes for inequality.
+	/// </summary>
+	/// <param name="left"></param>
+	/// <param name="right"></param>
+	/// <returns></returns>
+	public static bool operator != (Attribute left, Attribute right) => !(left == right);
+
+	/// <inheritdoc/>
+	public override bool Equals (object? obj) => obj is Attribute other && Equals (other);
+
+	/// <inheritdoc/>
+	public bool Equals (Attribute other) => PlatformColor == other.PlatformColor &&
+											Foreground == other.Foreground &&
+											Background == other.Background;
+
+	/// <inheritdoc/>
+	public override int GetHashCode () => HashCode.Combine (PlatformColor, Foreground, Background);
+
+	/// <inheritdoc/>
+	public override string ToString () =>
+		// Note: Unit tests are dependent on this format
+		$"[{Foreground},{Background}]";
+}

+ 424 - 0
Terminal.Gui/Drawing/Color.Formatting.cs

@@ -0,0 +1,424 @@
+#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;
+	}
+}

+ 90 - 0
Terminal.Gui/Drawing/Color.Operators.cs

@@ -0,0 +1,90 @@
+#nullable enable
+using System.Diagnostics.Contracts;
+using System.Numerics;
+
+namespace Terminal.Gui;
+
+public readonly partial record struct Color {
+
+	/// <inheritdoc />
+	/// <returns>
+	///   A <see cref="Color" /> <see langword="struct" /> with all values set to <see cref="byte.MaxValue" />, meaning white.
+	/// </returns>
+	public static Color MaxValue => new Color (uint.MaxValue);
+
+	/// <inheritdoc />
+	/// <returns>A <see cref="Color" /> <see langword="struct" /> with all values set to zero.</returns>
+	/// <remarks>
+	///   Though this returns a <see cref="Color" /> with <see cref="A" />, <see cref="R" />, <see cref="G" />, and <see cref="B" /> all set to
+	///   zero, Terminal.Gui will treat it as black, because the alpha channel is not supported.
+	/// </remarks>
+	public static Color MinValue => new Color (uint.MinValue);
+
+	/// <inheritdoc />
+	[Pure]
+	public override int GetHashCode () => Rgba.GetHashCode ();
+
+	/// <summary>
+	///   Implicit conversion from <see cref="Color" /> to <see cref="Vector3" /> via <see cref="Vector3(float,float,float)" /> where (
+	///   <see cref="Vector3.X" />, <see cref="Vector3.Y" />, <see cref="Vector3.Z" />) is (R,G,B).
+	/// </summary>
+	/// <remarks>
+	///   This cast is narrowing and drops the alpha channel.
+	///   <para />
+	///   Use <see cref="implicit operator Vector4(Color)" /> to maintain full value.
+	/// </remarks>
+	[Pure]
+	public static explicit operator Vector3 (Color color) => new Vector3 (color.R, color.G, color.B);
+	/// <summary>
+	///   Implicit conversion from <see langword="int" /> to <see cref="Color" />, via the <see cref="Color(int)" /> costructor.
+	/// </summary>
+	[Pure]
+	public static implicit operator Color (int rgba) => new Color (rgba);
+
+	/// <summary>
+	///   Implicit conversion from <see cref="Color" /> to <see langword="int" /> by returning the value of the <see cref="Rgba" /> field.
+	/// </summary>
+	[Pure]
+	public static implicit operator int (Color color) => color.Rgba;
+
+	/// <summary>
+	///   Implicit conversion from <see langword="uint" /> to <see cref="Color" />, via the <see cref="Color(uint)" /> costructor.
+	/// </summary>
+	[Pure]
+	public static implicit operator Color (uint u) => new Color (u);
+
+	/// <summary>
+	///   Implicit conversion from <see cref="Color" /> to <see langword="uint" /> by returning the value of the <see cref="Argb" /> field.
+	/// </summary>
+	[Pure]
+	public static implicit operator uint (Color color) => color.Argb;
+
+	/// <summary>
+	///   Implicit conversion from <see cref="GetClosestNamedColor" /> to <see cref="Color" /> via lookup from
+	///   <see cref="ColorExtensions.ColorNameToColorMap" />.
+	/// </summary>
+	[Pure]
+	public static implicit operator Color (ColorName colorName) => ColorExtensions.ColorNameToColorMap [colorName];
+
+	/// <summary>
+	///   Implicit conversion from <see cref="Vector4" /> to <see cref="Color" />, where (<see cref="Vector4.X" />, <see cref="Vector4.Y" />,
+	///   <see cref="Vector4.Z" />, <see cref="Vector4.W" />) is (<see cref="A" />,<see cref="R" />,<see cref="G" />,<see cref="B" />), via
+	///   <see cref="Color(int,int,int,int)" />.
+	/// </summary>
+	[Pure]
+	public static implicit operator Color (Vector4 v) => new Color ((byte)v.X, (byte)v.Y, (byte)v.Z, (byte)v.W);
+
+	/// <summary>
+	///   Implicit conversion to <see cref="Vector3" />, where <see cref="Vector3.X" /> = <see cref="R" />, <see cref="Vector3.Y" /> =
+	///   <see cref="G" />, and <see cref="Vector3.Z" /> = <see cref="B" />.
+	/// </summary>
+	[Pure]
+	public static implicit operator Vector4 (Color color) => new Vector4 (color.R, color.G, color.B, color.A);
+
+	/// <summary>
+	///   Implicit conversion from <see cref="Vector3" />, where <see cref="Vector3.X" /> = <see cref="R" />, <see cref="Vector3.Y" /> =
+	///   <see cref="G" />, and <see cref="Vector3.Z" /> = <see cref="B" />.
+	/// </summary>
+	[Pure]
+	public static implicit operator Color (Vector3 v) => new Color ((byte)v.X, (byte)v.Y, (byte)v.Z);
+}

+ 162 - 770
Terminal.Gui/Drawing/Color.cs

@@ -1,890 +1,282 @@
-global using Attribute = Terminal.Gui.Attribute;
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
+#nullable enable
+using System.Collections.Frozen;
+using System.Diagnostics.Contracts;
+using System.Globalization;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 using System.Text.Json.Serialization;
-using System.Text.RegularExpressions;
 
 namespace Terminal.Gui;
 
-/// <summary>
-/// Defines the 16 legacy color names and values that can be used to set the
-/// foreground and background colors in Terminal.Gui apps. Used with <see cref="Color"/>.
+/// <summary>Represents a 24-bit color encoded in ARGB32 format.
+///   <para />
 /// </summary>
-/// <remarks>
-///         <para>
-///         These colors match the 16 colors defined for ANSI escape sequences for 4-bit (16) colors.
-///         </para>
-///         <para>
-///         For terminals that support 24-bit color (TrueColor), the RGB values for each of these colors can be configured
-///         using the
-///         <see cref="Color.Colors"/> property.
-///         </para>
-/// </remarks>
-public enum ColorName {
-	/// <summary>
-	/// The black color. ANSI escape sequence: <c>\u001b[30m</c>.
-	/// </summary>
-	Black,
-
-	/// <summary>
-	/// The blue color. ANSI escape sequence: <c>\u001b[34m</c>.
-	/// </summary>
-	Blue,
-
-	/// <summary>
-	/// The green color. ANSI escape sequence: <c>\u001b[32m</c>.
-	/// </summary>
-	Green,
-
-	/// <summary>
-	/// The cyan color. ANSI escape sequence: <c>\u001b[36m</c>.
-	/// </summary>
-	Cyan,
-
-	/// <summary>
-	/// The red color. ANSI escape sequence: <c>\u001b[31m</c>.
-	/// </summary>
-	Red,
-
-	/// <summary>
-	/// The magenta color. ANSI escape sequence: <c>\u001b[35m</c>.
-	/// </summary>
-	Magenta,
-
-	/// <summary>
-	/// The yellow color (also known as Brown). ANSI escape sequence: <c>\u001b[33m</c>.
-	/// </summary>
-	Yellow,
-
-	/// <summary>
-	/// The gray color (also known as White). ANSI escape sequence: <c>\u001b[37m</c>.
-	/// </summary>
-	Gray,
-
-	/// <summary>
-	/// The dark gray color (also known as Bright Black). ANSI escape sequence: <c>\u001b[30;1m</c>.
-	/// </summary>
-	DarkGray,
-
-	/// <summary>
-	/// The bright blue color. ANSI escape sequence: <c>\u001b[34;1m</c>.
-	/// </summary>
-	BrightBlue,
-
-	/// <summary>
-	/// The bright green color. ANSI escape sequence: <c>\u001b[32;1m</c>.
-	/// </summary>
-	BrightGreen,
-
-	/// <summary>
-	/// The bright cyan color. ANSI escape sequence: <c>\u001b[36;1m</c>.
-	/// </summary>
-	BrightCyan,
-
-	/// <summary>
-	/// The bright red color. ANSI escape sequence: <c>\u001b[31;1m</c>.
-	/// </summary>
-	BrightRed,
-
-	/// <summary>
-	/// The bright magenta color. ANSI escape sequence: <c>\u001b[35;1m</c>.
-	/// </summary>
-	BrightMagenta,
-
-	/// <summary>
-	/// The bright yellow color. ANSI escape sequence: <c>\u001b[33;1m</c>.
-	/// </summary>
-	BrightYellow,
-
-	/// <summary>
-	/// The White color (also known as Bright White). ANSI escape sequence: <c>\u001b[37;1m</c>.
-	/// </summary>
-	White
-}
-
-/// <summary>
-/// The 16 foreground color codes used by ANSI Esc sequences for 256 color terminals. Add 10 to these values for background
-/// color.
-/// </summary>
-public enum AnsiColorCode {
-	/// <summary>
-	/// The ANSI color code for Black.
-	/// </summary>
-	BLACK = 30,
-
-	/// <summary>
-	/// The ANSI color code for Red.
-	/// </summary>
-	RED = 31,
-
-	/// <summary>
-	/// The ANSI color code for Green.
-	/// </summary>
-	GREEN = 32,
-
-	/// <summary>
-	/// The ANSI color code for Yellow.
-	/// </summary>
-	YELLOW = 33,
-
-	/// <summary>
-	/// The ANSI color code for Blue.
-	/// </summary>
-	BLUE = 34,
-
-	/// <summary>
-	/// The ANSI color code for Magenta.
-	/// </summary>
-	MAGENTA = 35,
-
-	/// <summary>
-	/// The ANSI color code for Cyan.
-	/// </summary>
-	CYAN = 36,
-
-	/// <summary>
-	/// The ANSI color code for White.
-	/// </summary>
-	WHITE = 37,
-
-	/// <summary>
-	/// The ANSI color code for Bright Black.
-	/// </summary>
-	BRIGHT_BLACK = 90,
-
-	/// <summary>
-	/// The ANSI color code for Bright Red.
-	/// </summary>
-	BRIGHT_RED = 91,
-
-	/// <summary>
-	/// The ANSI color code for Bright Green.
-	/// </summary>
-	BRIGHT_GREEN = 92,
-
-	/// <summary>
-	/// The ANSI color code for Bright Yellow.
-	/// </summary>
-	BRIGHT_YELLOW = 93,
-
-	/// <summary>
-	/// The ANSI color code for Bright Blue.
-	/// </summary>
-	BRIGHT_BLUE = 94,
-
-	/// <summary>
-	/// The ANSI color code for Bright Magenta.
-	/// </summary>
-	BRIGHT_MAGENTA = 95,
+/// <seealso cref="Attribute" />
+/// <seealso cref="ColorExtensions" />
+/// <seealso cref="ColorName" />
+[JsonConverter (typeof (ColorJsonConverter))]
+[StructLayout (LayoutKind.Explicit)]
+public readonly partial record struct Color : ISpanParsable<Color>, IUtf8SpanParsable<Color>, ISpanFormattable, IUtf8SpanFormattable, IMinMaxValue<Color> {
 
-	/// <summary>
-	/// The ANSI color code for Bright Cyan.
-	/// </summary>
-	BRIGHT_CYAN = 96,
+	/// <summary>The value of the alpha channel component</summary>
+	/// <remarks>
+	///   The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
+	/// </remarks>
+	[JsonIgnore]
+	[field: FieldOffset (3)]
+	public readonly byte A;
 
 	/// <summary>
-	/// The ANSI color code for Bright White.
+	///   The value of this <see cref="Color" /> as a <see langword="uint" /> in ARGB32 format.
 	/// </summary>
-	BRIGHT_WHITE = 97
-}
+	/// <remarks>
+	///   The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
+	/// </remarks>
+	[JsonIgnore]
+	[field: FieldOffset (0)]
+	public readonly uint Argb;
 
-/// <summary>
-/// Represents a 24-bit color. Provides automatic mapping between the legacy 4-bit (16 color) system and 24-bit colors (see
-/// <see cref="ColorName"/>). Used with <see cref="Attribute"/>.
-/// </summary>
-[JsonConverter (typeof (ColorJsonConverter))]
-public readonly struct Color : IEquatable<Color> {
+	/// <summary>The value of the blue color component.</summary>
+	[JsonIgnore]
+	[field: FieldOffset (0)]
+	public readonly byte B;
 
-	// TODO: Make this map configurable via ConfigurationManager
-	// TODO: This does not need to be a Dictionary, but can be an 16 element array.
-	/// <summary>
-	/// Maps legacy 16-color values to the corresponding 24-bit RGB value.
-	/// </summary>
-	internal static ImmutableDictionary<Color, ColorName> _colorToNameMap = new Dictionary<Color, ColorName> {
-		// using "Windows 10 Console/PowerShell 6" here: https://i.stack.imgur.com/9UVnC.png
-		// See also: https://en.wikipedia.org/wiki/ANSI_escape_code
-		{ new Color (12, 12, 12), ColorName.Black },
-		{ new Color (0, 55, 218), ColorName.Blue },
-		{ new Color (19, 161, 14), ColorName.Green },
-		{ new Color (58, 150, 221), ColorName.Cyan },
-		{ new Color (197, 15, 31), ColorName.Red },
-		{ new Color (136, 23, 152), ColorName.Magenta },
-		{ new Color (128, 64, 32), ColorName.Yellow },
-		{ new Color (204, 204, 204), ColorName.Gray },
-		{ new Color (118, 118, 118), ColorName.DarkGray },
-		{ new Color (59, 120, 255), ColorName.BrightBlue },
-		{ new Color (22, 198, 12), ColorName.BrightGreen },
-		{ new Color (97, 214, 214), ColorName.BrightCyan },
-		{ new Color (231, 72, 86), ColorName.BrightRed },
-		{ new Color (180, 0, 158), ColorName.BrightMagenta },
-		{ new Color (249, 241, 165), ColorName.BrightYellow },
-		{ new Color (242, 242, 242), ColorName.White }
-	}.ToImmutableDictionary ();
+	/// <summary>The value of the green color component.</summary>
+	[JsonIgnore]
+	[field: FieldOffset (1)]
+	public readonly byte G;
 
+	/// <summary>The value of the red color component.</summary>
+	[JsonIgnore]
+	[field: FieldOffset (2)]
+	public readonly byte R;
 
 	/// <summary>
-	/// Defines the 16 legacy color names and values that can be used to set the
+	///   The value of this <see cref="Color" /> encoded as a signed 32-bit integer in ARGB32 format.
 	/// </summary>
-	internal static ImmutableDictionary<ColorName, AnsiColorCode> _colorNameToAnsiColorMap = new Dictionary<ColorName, AnsiColorCode> {
-		{ ColorName.Black, AnsiColorCode.BLACK },
-		{ ColorName.Blue, AnsiColorCode.BLUE },
-		{ ColorName.Green, AnsiColorCode.GREEN },
-		{ ColorName.Cyan, AnsiColorCode.CYAN },
-		{ ColorName.Red, AnsiColorCode.RED },
-		{ ColorName.Magenta, AnsiColorCode.MAGENTA },
-		{ ColorName.Yellow, AnsiColorCode.YELLOW },
-		{ ColorName.Gray, AnsiColorCode.WHITE },
-		{ ColorName.DarkGray, AnsiColorCode.BRIGHT_BLACK },
-		{ ColorName.BrightBlue, AnsiColorCode.BRIGHT_BLUE },
-		{ ColorName.BrightGreen, AnsiColorCode.BRIGHT_GREEN },
-		{ ColorName.BrightCyan, AnsiColorCode.BRIGHT_CYAN },
-		{ ColorName.BrightRed, AnsiColorCode.BRIGHT_RED },
-		{ ColorName.BrightMagenta, AnsiColorCode.BRIGHT_MAGENTA },
-		{ ColorName.BrightYellow, AnsiColorCode.BRIGHT_YELLOW },
-		{ ColorName.White, AnsiColorCode.BRIGHT_WHITE }
-	}.ToImmutableDictionary ();
-
+	[JsonIgnore]
+	[field: FieldOffset (0)]
+	public readonly int Rgba;
 	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> class.
+	///   Initializes a new instance of the <see cref="Color" /> <see langword="struct" /> using the supplied component values.
 	/// </summary>
 	/// <param name="red">The red 8-bits.</param>
 	/// <param name="green">The green 8-bits.</param>
 	/// <param name="blue">The blue 8-bits.</param>
 	/// <param name="alpha">Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui.</param>
-	public Color (int red, int green, int blue, int alpha = 0xFF)
+	/// <remarks>Alpha channel is not currently supported by Terminal.Gui.</remarks>
+	/// <exception cref="OverflowException">If the value of any parameter is greater than <see cref="byte.MaxValue" />.</exception>
+	/// <exception cref="ArgumentOutOfRangeException">If the value of any parameter is negative.</exception>
+	public Color (int red = 0, int green = 0, int blue = 0, int alpha = byte.MaxValue)
 	{
-		R = red;
-		G = green;
-		B = blue;
-		A = alpha;
+		ArgumentOutOfRangeException.ThrowIfNegative (red, nameof (red));
+		ArgumentOutOfRangeException.ThrowIfNegative (green, nameof (green));
+		ArgumentOutOfRangeException.ThrowIfNegative (blue, nameof (blue));
+		ArgumentOutOfRangeException.ThrowIfNegative (alpha, nameof (alpha));
+
+		A = Convert.ToByte (alpha);
+		R = Convert.ToByte (red);
+		G = Convert.ToByte (green);
+		B = Convert.ToByte (blue);
 	}
 
 	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> class with an encoded 24-bit color value.
+	///   Initializes a new instance of the <see cref="Color" /> class with an encoded signed 32-bit color value in ARGB32 format.
 	/// </summary>
-	/// <param name="rgba">The encoded 24-bit color value (see <see cref="Rgba"/>).</param>
+	/// <param name="rgba">The encoded 32-bit color value (see <see cref="Rgba" />).</param>
+	/// <remarks>
+	///   The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
+	/// </remarks>
 	public Color (int rgba)
 	{
-		A = (byte)(rgba >> 24 & 0xFF);
-		R = (byte)(rgba >> 16 & 0xFF);
-		G = (byte)(rgba >> 8 & 0xFF);
-		B = (byte)(rgba & 0xFF);
+		Rgba = rgba;
+	}
+
+	/// <summary>
+	///   Initializes a new instance of the <see cref="Color" /> class with an encoded unsigned 32-bit color value in ARGB32 format.
+	/// </summary>
+	/// <param name="argb">The encoded unsigned 32-bit color value (see <see cref="Argb" />).</param>
+	/// <remarks>
+	///   The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
+	/// </remarks>
+	public Color (uint argb)
+	{
+		Argb = argb;
 	}
 
 	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> color from a legacy 16-color value.
+	///   Initializes a new instance of the <see cref="Color" /> color from a legacy 16-color named value.
 	/// </summary>
 	/// <param name="colorName">The 16-color value.</param>
-	public Color (ColorName colorName)
+	public Color (in ColorName colorName)
 	{
-		var c = FromColorName (colorName);
-		R = c.R;
-		G = c.G;
-		B = c.B;
-		A = c.A;
+		this = ColorExtensions.ColorNameToColorMap [colorName];
 	}
 
 	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/> color from string. See <see cref="TryParse(string, out Color)"/>
-	/// for details.
+	///   Initializes a new instance of the <see cref="Color" /> color from string. See <see cref="TryParse(string, out Color?)" /> for details.
 	/// </summary>
 	/// <param name="colorString"></param>
-	/// <exception cref="Exception"></exception>
+	/// <exception cref="ArgumentNullException">If <paramref name="colorString" /> is <see langword="null" />.</exception>
+	/// <exception cref="ArgumentException">
+	///   If <paramref name="colorString" /> is an empty string or consists of only whitespace characters.
+	/// </exception>
+	/// <exception cref="ColorParseException">If thrown by <see cref="Parse(string?,System.IFormatProvider?)" /></exception>
 	public Color (string colorString)
 	{
-		if (!TryParse (colorString, out var c)) {
-			throw new ArgumentOutOfRangeException (nameof (colorString));
-		}
-		R = c.R;
-		G = c.G;
-		B = c.B;
-		A = c.A;
+		ArgumentException.ThrowIfNullOrWhiteSpace (colorString, nameof (colorString));
+		this = Parse (colorString, CultureInfo.InvariantCulture);
 	}
 
 	/// <summary>
-	/// Initializes a new instance of the <see cref="Color"/>.
+	///   Initializes a new instance of the <see cref="Color" /> with all channels set to 0.
 	/// </summary>
 	public Color ()
 	{
-		R = 0;
-		G = 0;
-		B = 0;
-		A = 0xFF;
+		Argb = 0u;
 	}
 
 	/// <summary>
-	/// Red color component.
-	/// </summary>
-	public int R { get; }
-
-	/// <summary>
-	/// Green color component.
-	/// </summary>
-	public int G { get; }
-
-	/// <summary>
-	/// Blue color component.
-	/// </summary>
-	public int B { get; }
-
-	/// <summary>
-	/// Alpha color component.
-	/// </summary>
-	/// <remarks>
-	/// The Alpha channel is not supported by Terminal.Gui.
-	/// </remarks>
-	public int A { get; } // Not currently supported; here for completeness.
-
-	/// <summary>
-	/// Gets or sets the color value encoded as ARGB32.
-	/// <code>
-	/// (&lt;see cref="A"/&gt; &lt;&lt; 24) | (&lt;see cref="R"/&gt; &lt;&lt; 16) | (&lt;see cref="G"/&gt; &lt;&lt; 8) | &lt;see cref="B"/&gt;
-	/// </code>
-	/// </summary>
-	[JsonIgnore]
-	public int Rgba => A << 24 | R << 16 | G << 8 | B;
-
-	/// <summary>
-	/// Gets or sets the 24-bit color value for each of the legacy 16-color values.
+	///   Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values.
 	/// </summary>
 	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
 	public static Dictionary<ColorName, string> Colors {
 		get =>
 			// Transform _colorToNameMap into a Dictionary<ColorNames,string>
-			_colorToNameMap.ToDictionary (kvp => kvp.Value, kvp => $"#{kvp.Key.R:X2}{kvp.Key.G:X2}{kvp.Key.B:X2}");
+			ColorExtensions.ColorToNameMap.ToDictionary (static kvp => kvp.Value, static kvp => kvp.Key.ToString ("g"));
 		set {
 			// Transform Dictionary<ColorNames,string> into _colorToNameMap
-			var newMap = value.ToDictionary (kvp => new Color (kvp.Value), kvp => {
-				if (Enum.TryParse<ColorName> (kvp.Key.ToString (), true, out var colorName)) {
-					return colorName;
-				}
-				throw new ArgumentException ($"Invalid color name: {kvp.Key}");
-			});
-			_colorToNameMap = newMap.ToImmutableDictionary ();
+			ColorExtensions.ColorToNameMap = value.ToFrozenDictionary (GetColorToNameMapKey, GetColorToNameMapValue);
+			return;
+
+			static Color GetColorToNameMapKey (KeyValuePair<ColorName, string> kvp) => new Color (kvp.Value);
+			static ColorName GetColorToNameMapValue (KeyValuePair<ColorName, string> kvp) => Enum.TryParse<ColorName> (kvp.Key.ToString (), true, out var colorName) ? colorName : throw new ArgumentException ($"Invalid color name: {kvp.Key}");
 		}
 	}
 
 	/// <summary>
-	/// Gets the <see cref="Color"/> using a legacy 16-color <see cref="Gui.ColorName"/> value.
-	/// <see langword="get"/> will return the closest 16 color match to the true color when no exact value is found.
+	///   Gets the <see cref="Color" /> using a legacy 16-color <see cref="ColorName" /> value. <see langword="get" /> will return the closest 16
+	///   color match to the true color when no exact value is found.
 	/// </summary>
 	/// <remarks>
-	/// Get returns the <see cref="ColorName"/> of the closest 24-bit color value. Set sets the RGB value using a hard-coded
-	/// map.
+	///   Get returns the <see cref="GetClosestNamedColor" /> of the closest 24-bit color value. Set sets the RGB value using a hard-coded map.
 	/// </remarks>
-	[JsonIgnore]
-	public ColorName ColorName => FindClosestColor (this);
+	public AnsiColorCode GetAnsiColorCode () => ColorExtensions.ColorNameToAnsiColorMap [GetClosestNamedColor ()];
 
 	/// <summary>
-	/// Gets the <see cref="Color"/> using a legacy 16-color <see cref="Gui.ColorName"/> value.
-	/// <see langword="get"/> will return the closest 16 color match to the true color when no exact value is found.
+	///   Gets the <see cref="Color" /> using a legacy 16-color <see cref="Gui.ColorName" /> value. <see langword="get" /> will return the closest
+	///   16 color match to the true color when no exact value is found.
 	/// </summary>
 	/// <remarks>
-	/// Get returns the <see cref="ColorName"/> of the closest 24-bit color value. Set sets the RGB value using a hard-coded
-	/// map.
+	///   Get returns the <see cref="GetClosestNamedColor" /> of the closest 24-bit color value. Set sets the RGB value using a hard-coded map.
 	/// </remarks>
-	[JsonIgnore]
-	public AnsiColorCode AnsiColorCode => _colorNameToAnsiColorMap [ColorName];
+	public ColorName GetClosestNamedColor () => GetClosestNamedColor (this);
 
 	/// <summary>
-	/// Converts a legacy <see cref="Gui.ColorName"/> to a 24-bit <see cref="Color"/>.
+	///   Determines if the closest named <see cref="Color" /> to <see langword="this" /> is the provided <paramref name="namedColor" />.
 	/// </summary>
-	/// <param name="colorName">The <see cref="Color"/> to convert.</param>
-	/// <returns></returns>
-	static Color FromColorName (ColorName colorName) => _colorToNameMap.FirstOrDefault (x => x.Value == colorName).Key;
-
-	// Iterates through the entries in the _colorNames dictionary, calculates the
-	// Euclidean distance between the input color and each dictionary color in RGB space,
-	// and keeps track of the closest entry found so far. The function returns a KeyValuePair
-	// representing the closest color entry and its associated color name.
-	internal static ColorName FindClosestColor (Color inputColor)
-	{
-		var closestColor = ColorName.Black; // Default to Black
-		var closestDistance = double.MaxValue;
-
-		foreach (var colorEntry in _colorToNameMap) {
-			var distance = CalculateColorDistance (inputColor, colorEntry.Key);
-			if (distance < closestDistance) {
-				closestDistance = distance;
-				closestColor = colorEntry.Value;
-			}
-		}
-
-		return closestColor;
-	}
-
-	static double CalculateColorDistance (Color color1, Color color2)
-	{
-		// Calculate the Euclidean distance between two colors
-		var deltaR = color1.R - (double)color2.R;
-		var deltaG = color1.G - (double)color2.G;
-		var deltaB = color1.B - (double)color2.B;
+	/// <param name="namedColor">
+	///   The <see cref="GetClosestNamedColor" /> to check if this <see cref="Color" /> is closer to than any other configured named color.
+	/// </param>
+	/// <returns>
+	///   <see langword="true" /> if the closest named color is the provided value.
+	///   <br />
+	///   <see langword="false" /> if any other named color is closer to this <see cref="Color" /> than <paramref name="namedColor" />.
+	/// </returns>
+	/// <remarks>
+	///   If <see langword="this" /> is equidistant from two named colors, the result of this method is not guaranteed to be determinate.
+	/// </remarks>
+	[Pure]
+	[MethodImpl (MethodImplOptions.AggressiveInlining)]
+	public bool IsClosestToNamedColor (in ColorName namedColor) => GetClosestNamedColor () == namedColor;
 
-		return Math.Sqrt (deltaR * deltaR + deltaG * deltaG + deltaB * deltaB);
-	}
 
 	/// <summary>
-	/// Converts the provided string to a new <see cref="Color"/> instance.
+	///   Determines if the closest named <see cref="Color" /> to <paramref name="color" />/> is the provided <paramref name="namedColor" />.
 	/// </summary>
-	/// <param name="text">
-	/// The text to analyze. Formats supported are
-	/// "#RGB", "#RRGGBB", "#RGBA", "#RRGGBBAA", "rgb(r,g,b)", "rgb(r,g,b,a)", and any of the
-	/// <see cref="Gui.ColorName"/>.
+	/// <param name="color">
+	///   The color to test against the <see cref="GetClosestNamedColor" /> value in <paramref name="namedColor" />.
 	/// </param>
-	/// <param name="color">The parsed value.</param>
-	/// <returns>A boolean value indicating whether parsing was successful.</returns>
+	/// <param name="namedColor">
+	///   The <see cref="GetClosestNamedColor" /> to check if this <see cref="Color" /> is closer to than any other configured named color.
+	/// </param>
+	/// <returns>
+	///   <see langword="true" /> if the closest named color to <paramref name="color" /> is the provided value.
+	///   <br />
+	///   <see langword="false" /> if any other named color is closer to <paramref name="color" /> than <paramref name="namedColor" />.
+	/// </returns>
 	/// <remarks>
-	/// While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.
+	///   If <paramref name="color" /> is equidistant from two named colors, the result of this method is not guaranteed to be determinate.
 	/// </remarks>
-	public static bool TryParse (string text, [NotNullWhen (true)] out Color color)
+	[Pure]
+	[MethodImpl (MethodImplOptions.AggressiveInlining)]
+	public static bool IsColorClosestToNamedColor (in Color color, in ColorName namedColor)
 	{
-		// empty color
-		if (string.IsNullOrEmpty (text)) {
-			color = new Color ();
-			return false;
-		}
-
-		// #RRGGBB, #RGB
-		if (text [0] == '#' && text.Length is 7 or 4) {
-			if (text.Length == 7) {
-				var r = Convert.ToInt32 (text.Substring (1, 2), 16);
-				var g = Convert.ToInt32 (text.Substring (3, 2), 16);
-				var b = Convert.ToInt32 (text.Substring (5, 2), 16);
-				color = new Color (r, g, b);
-			} else {
-				var rText = char.ToString (text [1]);
-				var gText = char.ToString (text [2]);
-				var bText = char.ToString (text [3]);
-
-				var r = Convert.ToInt32 (rText + rText, 16);
-				var g = Convert.ToInt32 (gText + gText, 16);
-				var b = Convert.ToInt32 (bText + bText, 16);
-				color = new Color (r, g, b);
-			}
-			return true;
-		}
-
-		// #RRGGBB, #RGBA
-		if (text [0] == '#' && text.Length is 8 or 5) {
-			if (text.Length == 7) {
-				var r = Convert.ToInt32 (text.Substring (1, 2), 16);
-				var g = Convert.ToInt32 (text.Substring (3, 2), 16);
-				var b = Convert.ToInt32 (text.Substring (5, 2), 16);
-				var a = Convert.ToInt32 (text.Substring (7, 2), 16);
-				color = new Color (a, r, g, b);
-			} else {
-				var rText = char.ToString (text [1]);
-				var gText = char.ToString (text [2]);
-				var bText = char.ToString (text [3]);
-				var aText = char.ToString (text [4]);
-
-				var r = Convert.ToInt32 (aText + aText, 16);
-				var g = Convert.ToInt32 (rText + rText, 16);
-				var b = Convert.ToInt32 (gText + gText, 16);
-				var a = Convert.ToInt32 (bText + bText, 16);
-				color = new Color (r, g, b, a);
-			}
-			return true;
-		}
-
-		// rgb(r,g,b)
-		var match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+)\)");
-		if (match.Success) {
-			var r = int.Parse (match.Groups [1].Value);
-			var g = int.Parse (match.Groups [2].Value);
-			var b = int.Parse (match.Groups [3].Value);
-			color = new Color (r, g, b);
-			return true;
-		}
-
-		// rgb(r,g,b,a)
-		match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+),(\d+)\)");
-		if (match.Success) {
-			var r = int.Parse (match.Groups [1].Value);
-			var g = int.Parse (match.Groups [2].Value);
-			var b = int.Parse (match.Groups [3].Value);
-			var a = int.Parse (match.Groups [4].Value);
-			color = new Color (r, g, b, a);
-			return true;
-		}
-
-		if (Enum.TryParse<ColorName> (text, true, out var colorName)) {
-			color = new Color (colorName);
-			return true;
-		}
-
-		color = new Color ();
-		return false;
+		return color.IsClosestToNamedColor (in namedColor);
 	}
 
-	/// <summary>
-	/// Converts the color to a string representation.
-	/// </summary>
+	/// <summary>Gets the "closest" named color to this <see cref="Color" /> value.</summary>
+	/// <param name="inputColor"></param>
 	/// <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.
-	///         </para>
+	///   Distance is defined here as the Euclidean distance between each color interpreted as a <see cref="Vector3" />.
+	///   <para />
+	///   The order of the values in the passed Vector3 must be
 	/// </remarks>
 	/// <returns></returns>
-	public override string ToString ()
-	{
-		// If Values has an exact match with a named color (in _colorNames), use that.
-		if (_colorToNameMap.TryGetValue (this, out var colorName)) {
-			return Enum.GetName (typeof (ColorName), colorName);
-		}
-		// Otherwise return as an RGB hex value.
-		return $"#{R:X2}{G:X2}{B:X2}";
-	}
+	[SkipLocalsInit]
+	internal static ColorName GetClosestNamedColor (Color inputColor) => ColorExtensions.ColorToNameMap.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
+
+	[SkipLocalsInit]
+	static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) => Vector4.Distance (color1, color2);
 
 	#region Legacy Color Names
-	/// <summary>
-	/// The black color.
-	/// </summary>
+	/// <summary>The black color.</summary>
 	public const ColorName Black = ColorName.Black;
 
-	/// <summary>
-	/// The blue color.
-	/// </summary>
+	/// <summary>The blue color.</summary>
 	public const ColorName Blue = ColorName.Blue;
 
-	/// <summary>
-	/// The green color.
-	/// </summary>
+	/// <summary>The green color.</summary>
 	public const ColorName Green = ColorName.Green;
 
-	/// <summary>
-	/// The cyan color.
-	/// </summary>
+	/// <summary>The cyan color.</summary>
 	public const ColorName Cyan = ColorName.Cyan;
 
-	/// <summary>
-	/// The red color.
-	/// </summary>
+	/// <summary>The red color.</summary>
 	public const ColorName Red = ColorName.Red;
 
-	/// <summary>
-	/// The magenta color.
-	/// </summary>
+	/// <summary>The magenta color.</summary>
 	public const ColorName Magenta = ColorName.Magenta;
 
-	/// <summary>
-	/// The yellow color.
-	/// </summary>
+	/// <summary>The yellow color.</summary>
 	public const ColorName Yellow = ColorName.Yellow;
 
-	/// <summary>
-	/// The gray color.
-	/// </summary>
+	/// <summary>The gray color.</summary>
 	public const ColorName Gray = ColorName.Gray;
 
-	/// <summary>
-	/// The dark gray color.
-	/// </summary>
+	/// <summary>The dark gray color.</summary>
 	public const ColorName DarkGray = ColorName.DarkGray;
 
-	/// <summary>
-	/// The bright bBlue color.
-	/// </summary>
+	/// <summary>The bright bBlue color.</summary>
 	public const ColorName BrightBlue = ColorName.BrightBlue;
 
-	/// <summary>
-	/// The bright green color.
-	/// </summary>
+	/// <summary>The bright green color.</summary>
 	public const ColorName BrightGreen = ColorName.BrightGreen;
 
-	/// <summary>
-	/// The bright cyan color.
-	/// </summary>
+	/// <summary>The bright cyan color.</summary>
 	public const ColorName BrightCyan = ColorName.BrightCyan;
 
-	/// <summary>
-	/// The bright red color.
-	/// </summary>
+	/// <summary>The bright red color.</summary>
 	public const ColorName BrightRed = ColorName.BrightRed;
 
-	/// <summary>
-	/// The bright magenta color.
-	/// </summary>
+	/// <summary>The bright magenta color.</summary>
 	public const ColorName BrightMagenta = ColorName.BrightMagenta;
 
-	/// <summary>
-	/// The bright yellow color.
-	/// </summary>
+	/// <summary>The bright yellow color.</summary>
 	public const ColorName BrightYellow = ColorName.BrightYellow;
 
-	/// <summary>
-	/// The White color.
-	/// </summary>
+	/// <summary>The White color.</summary>
 	public const ColorName White = ColorName.White;
 	#endregion
-
-	// TODO: Verify implict/explicit are correct for below
-	#region Operators
-	/// <summary>
-	/// Cast from int.
-	/// </summary>
-	/// <param name="rgba"></param>
-	public static implicit operator Color (int rgba) => new (rgba);
-
-	/// <summary>
-	/// Cast to int. 
-	/// </summary>
-	/// <param name="color"></param>
-	public static implicit operator int (Color color) => color.Rgba;
-
-	/// <summary>
-	/// Cast from <see cref="Gui.ColorName"/>. May fail if the color is not a named color.
-	/// </summary>
-	/// <param name="colorName"></param>
-	public static explicit operator Color (ColorName colorName) => new (colorName);
-
-	/// <summary>
-	/// Cast to <see cref="Gui.ColorName"/>. May fail if the color is not a named color.
-	/// </summary>
-	/// <param name="color"></param>
-	public static explicit operator ColorName (Color color) => color.ColorName;
-
-	/// <summary>
-	/// Equality operator for two <see cref="Color"/> objects..
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (Color left, Color right) => left.Equals (right);
-
-	/// <summary>
-	/// Inequality operator for two <see cref="Color"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (Color left, Color right) => !left.Equals (right);
-
-	/// <summary>
-	/// Equality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (ColorName left, Color right) => left == right.ColorName;
-
-	/// <summary>
-	/// Inequality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (ColorName left, Color right) => left != right.ColorName;
-
-	/// <summary>
-	/// Equality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (Color left, ColorName right) => left.ColorName == right;
-
-	/// <summary>
-	/// Inequality operator for <see cref="Color"/> and <see cref="Gui.ColorName"/> objects.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (Color left, ColorName right) => left.ColorName != right;
-
-
-	/// <inheritdoc/>
-	public override bool Equals (object obj) => obj is Color other && Equals (other);
-
-	/// <inheritdoc/>
-	public bool Equals (Color other) => R == other.R &&
-	                                    G == other.G &&
-	                                    B == other.B &&
-	                                    A == other.A;
-
-	/// <inheritdoc/>
-	public override int GetHashCode () => HashCode.Combine (R, G, B, A);
-	#endregion
-}
-
-/// <summary>
-/// Attributes represent how text is styled when displayed in the terminal.
-/// </summary>
-/// <remarks>
-/// <see cref="Attribute"/> provides a platform independent representation of colors (and someday other forms of text
-/// styling).
-/// They encode both the foreground and the background color and are used in the <see cref="ColorScheme"/>
-/// class to define color schemes that can be used in an application.
-/// </remarks>
-[JsonConverter (typeof (AttributeJsonConverter))]
-public readonly struct Attribute : IEquatable<Attribute> {
-	/// <summary>
-	/// Default empty attribute.
-	/// </summary>
-	public static readonly Attribute Default = new (Color.White, Color.Black);
-
-	/// <summary>
-	/// The <see cref="ConsoleDriver"/>-specific color value.
-	/// </summary>
-	[JsonIgnore (Condition = JsonIgnoreCondition.Always)]
-	internal int PlatformColor { get; }
-
-	/// <summary>
-	/// The foreground color.
-	/// </summary>
-	[JsonConverter (typeof (ColorJsonConverter))]
-	public Color Foreground { get; }
-
-	/// <summary>
-	/// The background color.
-	/// </summary>
-	[JsonConverter (typeof (ColorJsonConverter))]
-	public Color Background { get; }
-
-	/// <summary>
-	/// Initializes a new instance with default values.
-	/// </summary>
-	public Attribute ()
-	{
-		PlatformColor = -1;
-		Foreground = new Color (Default.Foreground.ColorName);
-		Background = new Color (Default.Background.ColorName);
-	}
-
-	/// <summary>
-	/// Initializes a new instance from an existing instance.
-	/// </summary>
-	public Attribute (Attribute attr)
-	{
-		PlatformColor = -1;
-		Foreground = new Color (attr.Foreground.ColorName);
-		Background = new Color (attr.Background.ColorName);
-	}
-
-	/// <summary>
-	/// Initializes a new instance with platform specific color value.
-	/// </summary>
-	/// <param name="platformColor">Value.</param>
-	internal Attribute (int platformColor)
-	{
-		PlatformColor = platformColor;
-		Foreground = new Color (Default.Foreground.ColorName);
-		Background = new Color (Default.Background.ColorName);
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="platformColor">platform-dependent color value.</param>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="background">Background</param>
-	internal Attribute (int platformColor, Color foreground, Color background)
-	{
-		Foreground = foreground;
-		Background = background;
-		PlatformColor = platformColor;
-	}
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="platformColor">platform-dependent color value.</param>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="background">Background</param>
-	internal Attribute (int platformColor, ColorName foreground, ColorName background) : this (platformColor, new Color (foreground), new Color (background)) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="background">Background</param>
-	public Attribute (Color foreground, Color background)
-	{
-		Foreground = foreground;
-		Background = background;
-
-		// TODO: Once CursesDriver supports truecolor all the PlatformColor stuff goes away
-		if (Application.Driver == null) {
-			PlatformColor = -1;
-			return;
-		}
-
-		var make = Application.Driver.MakeColor (foreground, background);
-		PlatformColor = make.PlatformColor;
-	}
-
-	/// <summary>
-	/// Initializes a new instance with a <see cref="ColorName"/> value. Both <see cref="Foreground"/> and
-	/// <see cref="Background"/> will be set to the specified color.
-	/// </summary>
-	/// <param name="colorName">Value.</param>
-	internal Attribute (ColorName colorName) : this (colorName, colorName) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foregroundName">Foreground</param>
-	/// <param name="backgroundName">Background</param>
-	public Attribute (ColorName foregroundName, ColorName backgroundName) : this (new Color (foregroundName), new Color (backgroundName)) { }
-
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foregroundName">Foreground</param>
-	/// <param name="background">Background</param>
-	public Attribute (ColorName foregroundName, Color background) : this (new Color (foregroundName), background) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct.
-	/// </summary>
-	/// <param name="foreground">Foreground</param>
-	/// <param name="backgroundName">Background</param>
-	public Attribute (Color foreground, ColorName backgroundName) : this (foreground, new Color (backgroundName)) { }
-
-	/// <summary>
-	/// Initializes a new instance of the <see cref="Attribute"/> struct
-	/// with the same colors for the foreground and background.
-	/// </summary>
-	/// <param name="color">The color.</param>
-	public Attribute (Color color) : this (color, color) { }
-
-
-	/// <summary>
-	/// Compares two attributes for equality.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator == (Attribute left, Attribute right) => left.Equals (right);
-
-	/// <summary>
-	/// Compares two attributes for inequality.
-	/// </summary>
-	/// <param name="left"></param>
-	/// <param name="right"></param>
-	/// <returns></returns>
-	public static bool operator != (Attribute left, Attribute right) => !(left == right);
-
-	/// <inheritdoc/>
-	public override bool Equals (object obj) => obj is Attribute other && Equals (other);
-
-	/// <inheritdoc/>
-	public bool Equals (Attribute other) => PlatformColor == other.PlatformColor &&
-	                                        Foreground == other.Foreground &&
-	                                        Background == other.Background;
-
-	/// <inheritdoc/>
-	public override int GetHashCode () => HashCode.Combine (PlatformColor, Foreground, Background);
-
-	/// <inheritdoc/>
-	public override string ToString () =>
-		// Note: Unit tests are dependent on this format
-		$"[{Foreground},{Background}]";
 }

+ 74 - 0
Terminal.Gui/Drawing/ColorExtensions.cs

@@ -0,0 +1,74 @@
+using System.Collections.Frozen;
+
+namespace Terminal.Gui;
+
+internal static class ColorExtensions {
+
+	static FrozenDictionary<Color, ColorName> colorToNameMap;
+
+	static ColorExtensions ()
+	{
+		Dictionary<ColorName, AnsiColorCode> nameToCodeMap = new () {
+			{ ColorName.Black, AnsiColorCode.BLACK },
+			{ ColorName.Blue, AnsiColorCode.BLUE },
+			{ ColorName.Green, AnsiColorCode.GREEN },
+			{ ColorName.Cyan, AnsiColorCode.CYAN },
+			{ ColorName.Red, AnsiColorCode.RED },
+			{ ColorName.Magenta, AnsiColorCode.MAGENTA },
+			{ ColorName.Yellow, AnsiColorCode.YELLOW },
+			{ ColorName.Gray, AnsiColorCode.WHITE },
+			{ ColorName.DarkGray, AnsiColorCode.BRIGHT_BLACK },
+			{ ColorName.BrightBlue, AnsiColorCode.BRIGHT_BLUE },
+			{ ColorName.BrightGreen, AnsiColorCode.BRIGHT_GREEN },
+			{ ColorName.BrightCyan, AnsiColorCode.BRIGHT_CYAN },
+			{ ColorName.BrightRed, AnsiColorCode.BRIGHT_RED },
+			{ ColorName.BrightMagenta, AnsiColorCode.BRIGHT_MAGENTA },
+			{ ColorName.BrightYellow, AnsiColorCode.BRIGHT_YELLOW },
+			{ ColorName.White, AnsiColorCode.BRIGHT_WHITE }
+		};
+		ColorNameToAnsiColorMap = nameToCodeMap.ToFrozenDictionary ();
+
+		ColorToNameMap = new Dictionary<Color, ColorName> {
+			// using "Windows 10 Console/PowerShell 6" here: https://i.stack.imgur.com/9UVnC.png
+			// See also: https://en.wikipedia.org/wiki/ANSI_escape_code
+			{ new Color (12, 12, 12), ColorName.Black },
+			{ new Color (0, 55, 218), ColorName.Blue },
+			{ new Color (19, 161, 14), ColorName.Green },
+			{ new Color (58, 150, 221), ColorName.Cyan },
+			{ new Color (197, 15, 31), ColorName.Red },
+			{ new Color (136, 23, 152), ColorName.Magenta },
+			{ new Color (128, 64, 32), ColorName.Yellow },
+			{ new Color (204, 204, 204), ColorName.Gray },
+			{ new Color (118, 118, 118), ColorName.DarkGray },
+			{ new Color (59, 120, 255), ColorName.BrightBlue },
+			{ new Color (22, 198, 12), ColorName.BrightGreen },
+			{ new Color (97, 214, 214), ColorName.BrightCyan },
+			{ new Color (231, 72, 86), ColorName.BrightRed },
+			{ new Color (180, 0, 158), ColorName.BrightMagenta },
+			{ new Color (249, 241, 165), ColorName.BrightYellow },
+			{ new Color (242, 242, 242), ColorName.White }
+		}.ToFrozenDictionary ();
+	}
+
+	/// <summary>Defines the 16 legacy color names and their corresponding ANSI color codes.</summary>
+	internal static FrozenDictionary<ColorName, AnsiColorCode> ColorNameToAnsiColorMap { get; }
+
+	/// <summary>
+	///   Gets or sets a <see cref="FrozenDictionary{TKey,TValue}" /> that maps legacy 16-color values to the corresponding
+	///   <see cref="ColorName" />.
+	/// </summary>
+	/// <remarks>
+	///   Setter should be called as infrequently as possible, as <see cref="FrozenDictionary{TKey,TValue}" /> is expensive to create.
+	/// </remarks>
+	internal static FrozenDictionary<Color, ColorName> ColorToNameMap {
+		get => colorToNameMap;
+		set {
+			colorToNameMap = value;
+			//Also be sure to set the reverse mapping
+			ColorNameToColorMap = value.ToFrozenDictionary (static kvp => kvp.Value, static kvp => kvp.Key);
+		}
+	}
+
+	/// <summary>Reverse mapping for <see cref="ColorToNameMap" />.</summary>
+	internal static FrozenDictionary<ColorName, Color> ColorNameToColorMap { get; private set; }
+}

+ 97 - 0
Terminal.Gui/Drawing/ColorName.cs

@@ -0,0 +1,97 @@
+namespace Terminal.Gui;
+
+/// <summary>
+/// Defines the 16 legacy color names and values that can be used to set the
+/// foreground and background colors in Terminal.Gui apps. Used with <see cref="Color"/>.
+/// </summary>
+/// <remarks>
+///         <para>
+///         These colors match the 16 colors defined for ANSI escape sequences for 4-bit (16) colors.
+///         </para>
+///         <para>
+///         For terminals that support 24-bit color (TrueColor), the RGB values for each of these colors can be configured
+///         using the
+///         <see cref="Color.Colors"/> property.
+///         </para>
+/// </remarks>
+public enum ColorName {
+	/// <summary>
+	/// The black color. ANSI escape sequence: <c>\u001b[30m</c>.
+	/// </summary>
+	Black,
+
+	/// <summary>
+	/// The blue color. ANSI escape sequence: <c>\u001b[34m</c>.
+	/// </summary>
+	Blue,
+
+	/// <summary>
+	/// The green color. ANSI escape sequence: <c>\u001b[32m</c>.
+	/// </summary>
+	Green,
+
+	/// <summary>
+	/// The cyan color. ANSI escape sequence: <c>\u001b[36m</c>.
+	/// </summary>
+	Cyan,
+
+	/// <summary>
+	/// The red color. ANSI escape sequence: <c>\u001b[31m</c>.
+	/// </summary>
+	Red,
+
+	/// <summary>
+	/// The magenta color. ANSI escape sequence: <c>\u001b[35m</c>.
+	/// </summary>
+	Magenta,
+
+	/// <summary>
+	/// The yellow color (also known as Brown). ANSI escape sequence: <c>\u001b[33m</c>.
+	/// </summary>
+	Yellow,
+
+	/// <summary>
+	/// The gray color (also known as White). ANSI escape sequence: <c>\u001b[37m</c>.
+	/// </summary>
+	Gray,
+
+	/// <summary>
+	/// The dark gray color (also known as Bright Black). ANSI escape sequence: <c>\u001b[30;1m</c>.
+	/// </summary>
+	DarkGray,
+
+	/// <summary>
+	/// The bright blue color. ANSI escape sequence: <c>\u001b[34;1m</c>.
+	/// </summary>
+	BrightBlue,
+
+	/// <summary>
+	/// The bright green color. ANSI escape sequence: <c>\u001b[32;1m</c>.
+	/// </summary>
+	BrightGreen,
+
+	/// <summary>
+	/// The bright cyan color. ANSI escape sequence: <c>\u001b[36;1m</c>.
+	/// </summary>
+	BrightCyan,
+
+	/// <summary>
+	/// The bright red color. ANSI escape sequence: <c>\u001b[31;1m</c>.
+	/// </summary>
+	BrightRed,
+
+	/// <summary>
+	/// The bright magenta color. ANSI escape sequence: <c>\u001b[35;1m</c>.
+	/// </summary>
+	BrightMagenta,
+
+	/// <summary>
+	/// The bright yellow color. ANSI escape sequence: <c>\u001b[33;1m</c>.
+	/// </summary>
+	BrightYellow,
+
+	/// <summary>
+	/// The White color (also known as Bright White). ANSI escape sequence: <c>\u001b[37;1m</c>.
+	/// </summary>
+	White
+}

+ 92 - 0
Terminal.Gui/Drawing/ColorParseException.cs

@@ -0,0 +1,92 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///   An exception thrown when something goes wrong when trying to parse a <see cref="Color" />.
+/// </summary>
+/// <remarks>
+///   Contains additional information to help locate the problem.
+///   <br />
+///   Not intended to be thrown by consumers.
+/// </remarks>
+public sealed class ColorParseException : FormatException {
+
+	internal const string DefaultMessage = "Failed to parse text as Color.";
+	internal ColorParseException (string colorString, string? message, Exception? innerException = null) : base (message ?? DefaultMessage, innerException)
+	{
+		ColorString = colorString;
+	}
+
+	internal ColorParseException (string colorString, string? message = DefaultMessage) : base (message)
+	{
+		ColorString = colorString;
+	}
+
+	/// <summary>
+	///   Creates a new instance of a <see cref="ColorParseException" /> populated with the provided values.
+	/// </summary>
+	/// <param name="colorString">The text that caused this exception, as a <see langword="string" />.</param>
+	/// <param name="badValue">
+	///   The specific value in <paramref name="colorString" /> that caused this exception.
+	/// </param>
+	/// <param name="badValueName">
+	///   The name of the value (red, green, blue, alpha) that <paramref name="badValue" /> represents.
+	/// </param>
+	/// <param name="reason">The reason that <paramref name="badValue" /> failed to parse.</param>
+	internal ColorParseException (in ReadOnlySpan<char> colorString, string reason, in ReadOnlySpan<char> badValue = default, in ReadOnlySpan<char> badValueName = default) : base (DefaultMessage)
+	{
+		ColorString = colorString.ToString ();
+		BadValue = badValue.ToString ();
+		BadValueName = badValueName.ToString ();
+		Reason = reason;
+	}
+
+	/// <summary>
+	///   Creates a new instance of a <see cref="ColorParseException" /> populated with the provided values.
+	/// </summary>
+	/// <param name="colorString">The text that caused this exception, as a <see langword="string" />.</param>
+	/// <param name="badValue">
+	///   The specific value in <paramref name="colorString" /> that caused this exception.
+	/// </param>
+	/// <param name="badValueName">
+	///   The name of the value (red, green, blue, alpha) that <paramref name="badValue" /> represents.
+	/// </param>
+	/// <param name="reason">The reason that <paramref name="badValue" /> failed to parse.</param>
+	internal ColorParseException (in ReadOnlySpan<char> colorString, string? badValue = null, string? badValueName = null, string? reason = null) : base (DefaultMessage)
+	{
+		ColorString = colorString.ToString ();
+		BadValue = badValue;
+		BadValueName = badValueName;
+		Reason = reason;
+	}
+
+	/// <summary>Gets the text that failed to parse, as a <see langword="string" /></summary>
+	/// <remarks>
+	///   Is marked <see langword="required" />, so must be set by a constructor or initializer.
+	/// </remarks>
+	public string ColorString { get; }
+
+	/// <summary>
+	///   Gets the substring of <see cref="ColorString" /> caused this exception, as a <see langword="string" />
+	/// </summary>
+	/// <remarks>May be null or empty - only set if known.</remarks>
+	public string? BadValue { get; }
+
+	/// <summary>
+	///   Gets the name of the color component corresponding to <see cref="BadValue" />, if known.
+	/// </summary>
+	/// <remarks>May be null or empty - only set if known.</remarks>
+	public string? BadValueName { get; }
+
+	/// <summary>Gets the reason that <see cref="BadValue" /> failed to parse, if known.</summary>
+	/// <remarks>May be null or empty - only set if known.</remarks>
+	public string? Reason { get; }
+
+	[DoesNotReturn]
+	internal static void Throw (in ReadOnlySpan<char> colorString, string reason, in ReadOnlySpan<char> badValue = default, in ReadOnlySpan<char> badValueName = default) => throw new ColorParseException (in colorString, reason, in badValue, in badValueName);
+
+	[DoesNotReturn]
+	internal static void ThrowIfNotAsciiDigits (in ReadOnlySpan<char> valueText, string reason, in ReadOnlySpan<char> badValue = default, in ReadOnlySpan<char> badValueName = default) => throw new ColorParseException (in valueText, reason, in badValue, in badValueName);
+}

+ 30 - 24
Terminal.Gui/Drawing/ColorScheme.cs

@@ -1,5 +1,5 @@
-using System;
-using System.Collections.Generic;
+#nullable enable
+using System.Globalization;
 using System.Text.Json.Serialization;
 
 namespace Terminal.Gui;
@@ -14,16 +14,16 @@ namespace Terminal.Gui;
 /// using the <see cref="ColorScheme(ColorScheme)"/> constructor.
 /// </para>
 /// <para>
-/// See also: <see cref="Colors.ColorSchemes"/>.
+/// See also: <see cref="ColorSchemesConfiguration.ColorSchemes"/>.
 /// </para>
 /// </remarks>
 [JsonConverter (typeof (ColorSchemeJsonConverter))]
 public class ColorScheme : IEquatable<ColorScheme> {
-	readonly Attribute _disabled = Attribute.Default;
-	readonly Attribute _focus = Attribute.Default;
-	readonly Attribute _hotFocus = Attribute.Default;
-	readonly Attribute _hotNormal = Attribute.Default;
-	readonly Attribute _normal = Attribute.Default;
+	readonly Attribute _disabled;
+	readonly Attribute _focus;
+	readonly Attribute _hotFocus;
+	readonly Attribute _hotNormal;
+	readonly Attribute _normal;
 
 	/// <summary>
 	/// Creates a new instance set to the default colors (see <see cref="Attribute.Default"/>).
@@ -36,7 +36,7 @@ public class ColorScheme : IEquatable<ColorScheme> {
 	/// <param name="scheme">The scheme to initialize the new instance with.</param>
 	public ColorScheme (ColorScheme scheme)
 	{
-		if (scheme == null) {
+		if (scheme is null) {
 			throw new ArgumentNullException (nameof (scheme));
 		}
 		_normal = scheme.Normal;
@@ -104,19 +104,19 @@ public class ColorScheme : IEquatable<ColorScheme> {
 	/// </summary>
 	/// <param name="other"></param>
 	/// <returns>true if the two objects are equal</returns>
-	public bool Equals (ColorScheme other) => other != null &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_normal, other._normal) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_focus, other._focus) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_hotNormal, other._hotNormal) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_hotFocus, other._hotFocus) &&
-	                                          EqualityComparer<Attribute>.Default.Equals (_disabled, other._disabled);
+	public bool Equals (ColorScheme? other) => other is { } &&
+												EqualityComparer<Attribute>.Default.Equals (_normal, other._normal) &&
+												EqualityComparer<Attribute>.Default.Equals (_focus, other._focus) &&
+												EqualityComparer<Attribute>.Default.Equals (_hotNormal, other._hotNormal) &&
+												EqualityComparer<Attribute>.Default.Equals (_hotFocus, other._hotFocus) &&
+												EqualityComparer<Attribute>.Default.Equals (_disabled, other._disabled);
 
 	/// <summary>
 	/// Compares two <see cref="ColorScheme"/> objects for equality.
 	/// </summary>
 	/// <param name="obj"></param>
 	/// <returns>true if the two objects are equal</returns>
-	public override bool Equals (object obj) => Equals (obj is ColorScheme ? (ColorScheme)obj : default);
+	public override bool Equals (object? obj) => Equals (obj as ColorScheme);
 
 	/// <summary>
 	/// Returns a hashcode for this instance.
@@ -148,6 +148,9 @@ public class ColorScheme : IEquatable<ColorScheme> {
 	/// <param name="right"></param>
 	/// <returns><c>true</c> if the two objects are not equivalent</returns>
 	public static bool operator != (ColorScheme left, ColorScheme right) => !(left == right);
+
+	/// <inheritdoc />
+	public override string ToString ( ) => $"Normal: {Normal}; Focus: {Focus}; HotNormal: {HotNormal}; HotFocus: {HotFocus}; Disabled: {Disabled}";
 }
 
 /// <summary>
@@ -223,14 +226,17 @@ public static class Colors {
 	/// <summary>
 	/// Resets the <see cref="ColorSchemes"/> dictionary to the default values.
 	/// </summary>
-	public static Dictionary<string, ColorScheme> Reset () =>
-		ColorSchemes = new Dictionary<string, ColorScheme> (comparer: new SchemeNameComparerIgnoreCase ()) {
-			{ "TopLevel", new ColorScheme () },
-			{ "Base", new ColorScheme () },
-			{ "Dialog", new ColorScheme () },
-			{ "Menu", new ColorScheme () },
-			{ "Error", new ColorScheme () },
-		};
+	public static Dictionary<string, ColorScheme> Reset ()
+	{
+		ColorSchemes ??= new Dictionary<string, ColorScheme> (5, CultureInfo.InvariantCulture.CompareInfo.GetStringComparer (CompareOptions.IgnoreCase));
+		ColorSchemes.Clear ();
+		ColorSchemes.Add ("TopLevel", new ColorScheme ());
+		ColorSchemes.Add ("Base", new ColorScheme ());
+		ColorSchemes.Add ("Dialog", new ColorScheme ());
+		ColorSchemes.Add ("Menu", new ColorScheme ());
+		ColorSchemes.Add ("Error", new ColorScheme ());
+		return ColorSchemes;
+	}
 
 	class SchemeNameComparerIgnoreCase : IEqualityComparer<string> {
 		public bool Equals (string x, string y)

+ 29 - 0
Terminal.Gui/Drawing/ICustomColorFormatter.cs

@@ -0,0 +1,29 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>
+///   An interface to support custom formatting and parsing of <see cref="Color" /> values.
+/// </summary>
+public interface ICustomColorFormatter : IFormatProvider, ICustomFormatter {
+
+	/// <summary>
+	///   A method that returns a <see langword="string" /> based on the <paramref name="formatString" /> specified and the byte parameters
+	///   <paramref name="r" />, <paramref name="g" />, <paramref name="b" />, and <paramref name="a" />, which are provided by
+	///   <see cref="Color" />
+	/// </summary>
+	/// <param name="formatString"></param>
+	/// <param name="r"></param>
+	/// <param name="g"></param>
+	/// <param name="b"></param>
+	/// <param name="a"></param>
+	/// <returns></returns>
+	string Format (string? formatString, byte r, byte g, byte b, byte a);
+	/// <summary>
+	///   A method that returns a <see cref="Color" /> value based on the <paramref name="text" /> specified.
+	/// </summary>
+	/// <param name="text">
+	///   A string or other <see cref="ReadOnlySpan{T}" /> of <see langword="char" /> to parse as a <see cref="Color" />.
+	/// </param>
+	/// <returns>A <see cref="Color" /> value equivalent to <paramref name="text" />.</returns>
+	Color Parse (ReadOnlySpan<char> text);
+}

+ 6 - 2
Terminal.Gui/Terminal.Gui.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <!-- =================================================================== -->
   <!-- Version numbers -->
   <!-- Automatically updated by gitversion (run `dotnet-gitversion /updateprojectfiles`)  -->
@@ -20,7 +20,7 @@
   </PropertyGroup>
   <PropertyGroup>
     <TargetFrameworks>net8.0</TargetFrameworks>
-    <!--<LangVersion>11.0</LangVersion>-->
+    <LangVersion>12</LangVersion>
     <RootNamespace>Terminal.Gui</RootNamespace>
     <AssemblyName>Terminal.Gui</AssemblyName>
     <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
@@ -81,6 +81,9 @@
       <LastGenOutput>Strings.Designer.cs</LastGenOutput>
     </EmbeddedResource>
   </ItemGroup>
+  <ItemGroup>
+    <Using Include="System.Text" />
+  </ItemGroup>
   <!-- =================================================================== -->
   <!-- Nuget  -->
   <!-- =================================================================== -->
@@ -114,5 +117,6 @@
     <!--<DebugType>Embedded</DebugType>-->
     <Authors>Miguel de Icaza, Tig Kindel (@tig), @BDisp</Authors>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <ImplicitUsings>enable</ImplicitUsings>
   </PropertyGroup>
 </Project>

+ 27 - 3
Terminal.Gui/Text/StringExtensions.cs

@@ -148,9 +148,33 @@ public static class StringExtensions {
 	/// <returns></returns>
 	public static string ToString (IEnumerable<byte> bytes, Encoding? encoding = null)
 	{
-		if (encoding == null) {
-			encoding = Encoding.UTF8;
-		}
+		encoding ??= Encoding.UTF8;
 		return encoding.GetString (bytes.ToArray ());
 	}
+
+	/// <summary>
+	///   Determines if this <see cref="ReadOnlySpan{T}" /> of <see langword="char" /> is composed entirely of ASCII digits.
+	/// </summary>
+	/// <param name="stringSpan">A <see cref="ReadOnlySpan{T}" /> of <see langword="char" /> to check.</param>
+	/// <returns>
+	///   A <see langword="bool" /> indicating if all elements of the <see cref="ReadOnlySpan{T}" /> are ASCII digits (<see langword="true" />) or
+	///   not (<see langword="false" />
+	/// </returns>
+	public static bool IsAllAsciiDigits (this ReadOnlySpan<char> stringSpan)
+	{
+		return stringSpan.ToString ().All (char.IsAsciiDigit);
+	}
+
+	/// <summary>
+	///   Determines if this <see cref="ReadOnlySpan{T}" /> of <see langword="char" /> is composed entirely of ASCII digits.
+	/// </summary>
+	/// <param name="stringSpan">A <see cref="ReadOnlySpan{T}" /> of <see langword="char" /> to check.</param>
+	/// <returns>
+	///   A <see langword="bool" /> indicating if all elements of the <see cref="ReadOnlySpan{T}" /> are ASCII digits (<see langword="true" />) or
+	///   not (<see langword="false" />
+	/// </returns>
+	public static bool IsAllAsciiHexDigits (this ReadOnlySpan<char> stringSpan)
+	{
+		return stringSpan.ToString ().All (char.IsAsciiHexDigit);
+	}
 }

+ 102 - 30
Terminal.Gui/Views/DateField.cs

@@ -19,10 +19,14 @@ namespace Terminal.Gui;
 ///   The <see cref="DateField"/> <see cref="View"/> provides date editing functionality with mouse support.
 /// </remarks>
 public class DateField : TextField {
+
+	private const string RIGHT_TO_LEFT_MARK = "\u200f";
+
 	DateTime _date;
-	int _fieldLen = 10;
-	string _sepChar;
-	string _format;
+	private string _separator;
+	private string _format;
+	private readonly int _dateFieldLength = 12;
+	private int FormatLength => StandardizeDateFormat (_format).Trim ().Length;
 
 	/// <summary>
 	///   DateChanged event, raised when the <see cref="Date"/> property has changed.
@@ -46,15 +50,14 @@ public class DateField : TextField {
 	/// <param name="date"></param>
 	public DateField (DateTime date) : base ("")
 	{
-		Width = _fieldLen + 2;
+		Width = _dateFieldLength;
 		SetInitialProperties (date);
 	}
 
 	void SetInitialProperties (DateTime date)
 	{
-		var cultureInfo = CultureInfo.CurrentCulture;
-		_sepChar = cultureInfo.DateTimeFormat.DateSeparator;
-		_format = $" {cultureInfo.DateTimeFormat.ShortDatePattern}";
+		_format = $" {StandardizeDateFormat (Culture.DateTimeFormat.ShortDatePattern)}";
+		_separator = GetDataSeparator (Culture.DateTimeFormat.DateSeparator);
 		Date = date;
 		CursorPosition = 1;
 		TextChanging += DateField_Changing;
@@ -111,8 +114,6 @@ public class DateField : TextField {
 	void DateField_Changing (object sender, TextChangingEventArgs e)
 	{
 		try {
-			var cultureInfo = CultureInfo.CurrentCulture;
-			DateTimeFormatInfo ccFmt = cultureInfo.DateTimeFormat;
 			int spaces = 0;
 			for (int i = 0; i < e.NewText.Length; i++) {
 				if (e.NewText [i] == ' ') {
@@ -121,13 +122,13 @@ public class DateField : TextField {
 					break;
 				}
 			}
-			spaces += _fieldLen;
+			spaces += FormatLength;
 			string trimedText = e.NewText [..spaces];
-			spaces -= _fieldLen;
+			spaces -= FormatLength;
 			trimedText = trimedText.Replace (new string (' ', spaces), " ");
-			var date = Convert.ToDateTime (trimedText, ccFmt).ToString (ccFmt.ShortDatePattern);
+			var date = Convert.ToDateTime (trimedText).ToString (_format.Trim ());
 			if ($" {date}" != e.NewText) {
-				e.NewText = $" {date}";
+				e.NewText = $" {date}".Replace (RIGHT_TO_LEFT_MARK, "");
 			}
 			AdjCursorPosition (CursorPosition, true);
 		} catch (Exception) {
@@ -149,7 +150,8 @@ public class DateField : TextField {
 
 			var oldData = _date;
 			_date = value;
-			Text = value.ToString (_format);
+			Text = value.ToString (" " + StandardizeDateFormat (_format.Trim ()))
+				.Replace (RIGHT_TO_LEFT_MARK, "");
 			var args = new DateTimeEventArgs<DateTime> (oldData, value, _format);
 			if (oldData != value) {
 				OnDateChanged (args);
@@ -157,16 +159,31 @@ public class DateField : TextField {
 		}
 	}
 
+	/// <summary>
+	/// CultureInfo for date. The default is CultureInfo.CurrentCulture.
+	/// </summary>
+	public CultureInfo Culture {
+		get => CultureInfo.CurrentCulture;
+		set {
+			if (value is not null) {
+				CultureInfo.CurrentCulture = value;
+				_separator = GetDataSeparator (value.DateTimeFormat.DateSeparator);
+				_format = " " + StandardizeDateFormat (value.DateTimeFormat.ShortDatePattern);
+				Text = Date.ToString (_format).Replace (RIGHT_TO_LEFT_MARK, "");
+			}
+		}
+	}
+
 	/// <inheritdoc/>
 	public override int CursorPosition {
 		get => base.CursorPosition;
-		set => base.CursorPosition = Math.Max (Math.Min (value, _fieldLen), 1);
+		set => base.CursorPosition = Math.Max (Math.Min (value, FormatLength), 1);
 	}
 
 	bool SetText (Rune key)
 	{
-		if (CursorPosition > _fieldLen) {
-			CursorPosition = _fieldLen;
+		if (CursorPosition > FormatLength) {
+			CursorPosition = FormatLength;
 			return false;
 		} else if (CursorPosition < 1) {
 			CursorPosition = 1;
@@ -176,7 +193,7 @@ public class DateField : TextField {
 		var text = Text.EnumerateRunes ().ToList ();
 		var newText = text.GetRange (0, CursorPosition);
 		newText.Add (key);
-		if (CursorPosition < _fieldLen) {
+		if (CursorPosition < FormatLength) {
 			newText = [.. newText, .. text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))];
 		}
 		return SetText (StringExtensions.ToString (newText));
@@ -189,8 +206,13 @@ public class DateField : TextField {
 		}
 
 		text = NormalizeFormat (text);
-		string [] vals = text.Split (_sepChar);
-		string [] frm = _format.Split (_sepChar);
+		string [] vals = text.Split (_separator);
+		for (var i = 0; i < vals.Length; i++) {
+			if (vals [i].Contains (RIGHT_TO_LEFT_MARK)) {
+				vals [i] = vals [i].Replace (RIGHT_TO_LEFT_MARK, "");
+			}
+		}
+		string [] frm = _format.Split (_separator);
 		int year;
 		int month;
 		int day;
@@ -223,10 +245,13 @@ public class DateField : TextField {
 		}
 		string d = GetDate (month, day, year, frm);
 
-		if (!DateTime.TryParseExact (d, _format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) {
+		DateTime date;
+		try {
+			date = Convert.ToDateTime (d);
+		} catch (Exception) {
 			return false;
 		}
-		Date = result;
+		Date = date;
 		return true;
 	}
 
@@ -236,7 +261,7 @@ public class DateField : TextField {
 			fmt = _format;
 		}
 		if (string.IsNullOrEmpty (sepChar)) {
-			sepChar = _sepChar;
+			sepChar = _separator;
 		}
 		if (fmt.Length != text.Length) {
 			return text;
@@ -265,7 +290,7 @@ public class DateField : TextField {
 				date += $"{year,4:0000}";
 			}
 			if (i < 2) {
-				date += $"{_sepChar}";
+				date += $"{_separator}";
 			}
 		}
 		return date;
@@ -283,10 +308,57 @@ public class DateField : TextField {
 		return idx;
 	}
 
+	private string GetDataSeparator (string separator)
+	{
+		var sepChar = separator.Trim ();
+		if (sepChar.Length > 1 && sepChar.Contains (RIGHT_TO_LEFT_MARK)) {
+			sepChar = sepChar.Replace (RIGHT_TO_LEFT_MARK, "");
+		}
+
+		return sepChar;
+	}
+
+
+
+	// Converts various date formats to a uniform 10-character format. 
+	// This aids in simplifying the handling of single-digit months and days, 
+	// and reduces the number of distinct date formats to maintain.
+	private static string StandardizeDateFormat (string format) =>
+	    format switch {
+		    "MM/dd/yyyy" => "MM/dd/yyyy",
+		    "yyyy-MM-dd" => "yyyy-MM-dd",
+		    "yyyy/MM/dd" => "yyyy/MM/dd",
+		    "dd/MM/yyyy" => "dd/MM/yyyy",
+		    "d?/M?/yyyy" => "dd/MM/yyyy",
+		    "dd.MM.yyyy" => "dd.MM.yyyy",
+		    "dd-MM-yyyy" => "dd-MM-yyyy",
+		    "dd/MM yyyy" => "dd/MM/yyyy",
+		    "d. M. yyyy" => "dd.MM.yyyy",
+		    "yyyy.MM.dd" => "yyyy.MM.dd",
+		    "g yyyy/M/d" => "yyyy/MM/dd",
+		    "d/M/yyyy" => "dd/MM/yyyy",
+		    "d?/M?/yyyy g" => "dd/MM/yyyy",
+		    "d-M-yyyy" => "dd-MM-yyyy",
+		    "d.MM.yyyy" => "dd.MM.yyyy",
+		    "d.MM.yyyy '?'." => "dd.MM.yyyy",
+		    "M/d/yyyy" => "MM/dd/yyyy",
+		    "d. M. yyyy." => "dd.MM.yyyy",
+		    "d.M.yyyy." => "dd.MM.yyyy",
+		    "g yyyy-MM-dd" => "yyyy-MM-dd",
+		    "d.M.yyyy" => "dd.MM.yyyy",
+		    "d/MM/yyyy" => "dd/MM/yyyy",
+		    "yyyy/M/d" => "yyyy/MM/dd",
+		    "dd. MM. yyyy." => "dd.MM.yyyy",
+		    "yyyy. MM. dd." => "yyyy.MM.dd",
+		    "yyyy. M. d." => "yyyy.MM.dd",
+		    "d. MM. yyyy" => "dd.MM.yyyy",
+		    _ => "dd/MM/yyyy"
+	    };
+
 	void IncCursorPosition ()
 	{
-		if (CursorPosition >= _fieldLen) {
-			CursorPosition = _fieldLen;
+		if (CursorPosition >= FormatLength) {
+			CursorPosition = FormatLength;
 			return;
 		}
 		CursorPosition++;
@@ -306,8 +378,8 @@ public class DateField : TextField {
 	void AdjCursorPosition (int point, bool increment = true)
 	{
 		var newPoint = point;
-		if (point > _fieldLen) {
-			newPoint = _fieldLen;
+		if (point > FormatLength) {
+			newPoint = FormatLength;
 		}
 		if (point < 1) {
 			newPoint = 1;
@@ -316,7 +388,7 @@ public class DateField : TextField {
 			CursorPosition = newPoint;
 		}
 
-		while (Text [CursorPosition] == _sepChar [0]) {
+		while (Text [CursorPosition].ToString () == _separator) {
 			if (increment) {
 				CursorPosition++;
 			} else {
@@ -335,7 +407,7 @@ public class DateField : TextField {
 	new bool MoveEnd ()
 	{
 		ClearAllSelection ();
-		CursorPosition = _fieldLen;
+		CursorPosition = FormatLength;
 		return true;
 	}
 

+ 60 - 13
Terminal.Gui/Views/DatePicker.cs

@@ -24,10 +24,21 @@ public class DatePicker : View {
 
 	private DateTime _date = DateTime.Now;
 
+
 	/// <summary>
-	/// Format of date. The default is MM/dd/yyyy.
+	/// CultureInfo for date. The default is CultureInfo.CurrentCulture.
 	/// </summary>
-	public string Format { get; set; } = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
+	public CultureInfo Culture {
+		get => CultureInfo.CurrentCulture;
+		set {
+			if (value is not null) {
+				CultureInfo.CurrentCulture = value;
+				Text = Date.ToString (Format);
+			}
+		}
+	}
+
+	private string Format => StandardizeDateFormat (Culture.DateTimeFormat.ShortDatePattern);
 
 	/// <summary>
 	/// Get or set the date.
@@ -53,15 +64,6 @@ public class DatePicker : View {
 		SetInitialProperties (date);
 	}
 
-	/// <summary>
-	/// Initializes a new instance of <see cref="DatePicker"/> with the specified date and format.
-	/// </summary>
-	public DatePicker (DateTime date, string format)
-	{
-		Format = format;
-		SetInitialProperties (date);
-	}
-
 	private void SetInitialProperties (DateTime date)
 	{
 		Title = "Date Picker";
@@ -77,7 +79,8 @@ public class DatePicker : View {
 			X = Pos.Right (_dateLabel),
 			Y = 0,
 			Width = Dim.Fill (1),
-			Height = 1
+			Height = 1,
+			Culture = Culture,
 		};
 
 		_calendar = new TableView () {
@@ -146,10 +149,22 @@ public class DatePicker : View {
 
 	private void DateField_DateChanged (object sender, DateTimeEventArgs<DateTime> e)
 	{
+		Date = e.NewValue;
 		if (e.NewValue.Date.Day != _date.Day) {
 			SelectDayOnCalendar (e.NewValue.Day);
 		}
-		Date = e.NewValue;
+
+		if (_date.Month == DateTime.MinValue.Month && _date.Year == DateTime.MinValue.Year) {
+			_previousMonthButton.Enabled = false;
+		} else {
+			_previousMonthButton.Enabled = true;
+		}
+
+		if (_date.Month == DateTime.MaxValue.Month && _date.Year == DateTime.MaxValue.Year) {
+			_nextMonthButton.Enabled = false;
+		} else {
+			_nextMonthButton.Enabled = true;
+		}
 		CreateCalendar ();
 		SelectDayOnCalendar (_date.Day);
 	}
@@ -226,6 +241,38 @@ public class DatePicker : View {
 
 	private string GetBackButtonText () => Glyphs.LeftArrow.ToString () + Glyphs.LeftArrow.ToString ();
 
+	private static string StandardizeDateFormat (string format) =>
+	    format switch {
+		    "MM/dd/yyyy" => "MM/dd/yyyy",
+		    "yyyy-MM-dd" => "yyyy-MM-dd",
+		    "yyyy/MM/dd" => "yyyy/MM/dd",
+		    "dd/MM/yyyy" => "dd/MM/yyyy",
+		    "d?/M?/yyyy" => "dd/MM/yyyy",
+		    "dd.MM.yyyy" => "dd.MM.yyyy",
+		    "dd-MM-yyyy" => "dd-MM-yyyy",
+		    "dd/MM yyyy" => "dd/MM/yyyy",
+		    "d. M. yyyy" => "dd.MM.yyyy",
+		    "yyyy.MM.dd" => "yyyy.MM.dd",
+		    "g yyyy/M/d" => "yyyy/MM/dd",
+		    "d/M/yyyy" => "dd/MM/yyyy",
+		    "d?/M?/yyyy g" => "dd/MM/yyyy",
+		    "d-M-yyyy" => "dd-MM-yyyy",
+		    "d.MM.yyyy" => "dd.MM.yyyy",
+		    "d.MM.yyyy '?'." => "dd.MM.yyyy",
+		    "M/d/yyyy" => "MM/dd/yyyy",
+		    "d. M. yyyy." => "dd.MM.yyyy",
+		    "d.M.yyyy." => "dd.MM.yyyy",
+		    "g yyyy-MM-dd" => "yyyy-MM-dd",
+		    "d.M.yyyy" => "dd.MM.yyyy",
+		    "d/MM/yyyy" => "dd/MM/yyyy",
+		    "yyyy/M/d" => "yyyy/MM/dd",
+		    "dd. MM. yyyy." => "dd.MM.yyyy",
+		    "yyyy. MM. dd." => "yyyy.MM.dd",
+		    "yyyy. M. d." => "yyyy.MM.dd",
+		    "d. MM. yyyy" => "dd.MM.yyyy",
+		    _ => "dd/MM/yyyy"
+	    };
+
 	///<inheritdoc/>
 	protected override void Dispose (bool disposing)
 	{

+ 14 - 8
Terminal.Gui/Views/FileSystemColorProvider.cs

@@ -1,6 +1,6 @@
 // This code is adapted from https://github.com/devblackops/Terminal-Icons (which also uses the MIT license).
 
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO.Abstractions;
 
 namespace Terminal.Gui {
@@ -16,12 +16,12 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public Color? GetColor (IFileSystemInfo file)
 		{
-			if (FilenameToColor.ContainsKey (file.Name)) {
-				return FilenameToColor [file.Name];
+			if ( FilenameToColor.TryGetValue (file.Name, out Color nameColor) ) {
+				return nameColor;
 			}
 
-			if (ExtensionToColor.ContainsKey (file.Extension)) {
-				return ExtensionToColor [file.Extension];
+			if ( ExtensionToColor.TryGetValue (file.Extension, out Color extColor) ) {
+				return extColor;
 			}
 
 			return null;
@@ -443,10 +443,16 @@ namespace Terminal.Gui {
 
 		private static Color StringToColor (string str)
 		{
-			if (!Color.TryParse (str, out var c)) {
-				throw new System.Exception ("Failed to parse Color from " + str);
+			if ( !Color.TryParse (str, out var c) ) {
+				ThrowFormatException (str);
+			}
+			return c.Value;
+
+			[DoesNotReturn]
+			static void ThrowFormatException (string s)
+			{
+				throw new FormatException ($"Failed to parse Color from {s}");
 			}
-			return c;
 		}
 	}
 }

+ 8 - 35
Terminal.Gui/Views/TextView.cs

@@ -1,16 +1,10 @@
 #nullable enable
 
 // TextView.cs: multi-line text editing
-using System;
-using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
-using System.IO;
-using System.Linq;
 using System.Runtime.CompilerServices;
-using System.Text;
 using System.Text.Json.Serialization;
-using System.Threading;
 using Terminal.Gui.Resources;
 
 namespace Terminal.Gui;
@@ -19,7 +13,7 @@ namespace Terminal.Gui;
 /// Represents a single row/column within the <see cref="TextView"/>. Includes the glyph and the foreground/background
 /// colors.
 /// </summary>
-[DebuggerDisplay ("{DebuggerDisplay}")]
+[DebuggerDisplay ("{ColorSchemeDebuggerDisplay}")]
 public class RuneCell : IEquatable<RuneCell> {
 	/// <summary>
 	/// The glyph to draw.
@@ -33,44 +27,25 @@ public class RuneCell : IEquatable<RuneCell> {
 	[JsonConverter (typeof (ColorSchemeJsonConverter))]
 	public ColorScheme? ColorScheme { get; set; }
 
-	string DebuggerDisplay {
-		get {
-			var colorSchemeStr = ColorSchemeDebuggerDisplay ();
-			return $"U+{Rune.Value:X4} '{Rune.ToString ()}'; {colorSchemeStr}";
-		}
-	}
-
 	/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
 	/// <param name="other">An object to compare with this object.</param>
 	/// <returns>
 	/// <see langword="true"/> if the current object is equal to the <paramref name="other"/> parameter;
 	/// otherwise, <see langword="false"/>.
 	/// </returns>
-	public bool Equals (RuneCell? other) => other != null &&
-						Rune.Equals (other.Rune) &&
-						ColorScheme == other.ColorScheme;
+	public bool Equals ( RuneCell? other ) => other is {} &&
+											Rune.Equals ( other.Rune ) &&
+											ColorScheme == other.ColorScheme;
 
 	/// <summary>Returns a string that represents the current object.</summary>
 	/// <returns>A string that represents the current object.</returns>
 	public override string ToString ()
 	{
-		var colorSchemeStr = ColorSchemeDebuggerDisplay ();
-		return DebuggerDisplay;
+		var colorSchemeStr = ColorScheme?.ToString () ?? "null";
+		return $"U+{Rune.Value:X4} '{Rune.ToString ()}'; {colorSchemeStr}";
 	}
 
-	string ColorSchemeDebuggerDisplay ()
-	{
-		var colorSchemeStr = "null";
-		if (ColorScheme != null) {
-			colorSchemeStr = $"Normal: {ColorScheme.Normal.Foreground},{ColorScheme.Normal.Background}; " +
-					 $"Focus: {ColorScheme.Focus.Foreground},{ColorScheme.Focus.Background}; " +
-					 $"HotNormal: {ColorScheme.HotNormal.Foreground},{ColorScheme.HotNormal.Background}; " +
-					 $"HotFocus: {ColorScheme.HotFocus.Foreground},{ColorScheme.HotFocus.Background}; " +
-					 $"Disabled: {ColorScheme.Disabled.Foreground},{ColorScheme.Disabled.Background}";
-		}
-
-		return colorSchemeStr;
-	}
+	string ColorSchemeDebuggerDisplay => ToString ();
 }
 
 class TextModel {
@@ -229,9 +204,7 @@ class TextModel {
 	{
 		foreach (var line in _lines) {
 			foreach (var cell in line) {
-				if (cell.ColorScheme == null) {
-					cell.ColorScheme = colorScheme;
-				}
+				cell.ColorScheme ??= colorScheme;
 			}
 		}
 	}

+ 1 - 1
Terminal.Gui/Views/TimeField.cs

@@ -1,4 +1,4 @@
-//
+//
 // TimeField.cs: text entry for time
 //
 // Author: Jörg Preiß

+ 0 - 132
Terminal.sln.DotSettings

@@ -1,132 +0,0 @@
-<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
-	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeAccessorOwnerBody/@EntryIndexedValue">WARNING</s:String>
-	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>
-	<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBePrivate_002EGlobal/@EntryIndexedValue">HINT</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ACCESSOR_OWNER_BODY/@EntryValue">ExpressionBody</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ARGUMENTS_SKIP_SINGLE/@EntryValue">True</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue">NotRequired</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue">NotRequired</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue">NotRequired</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_LOCK/@EntryValue">RequiredForMultiline</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_WHILE/@EntryValue">RequiredForMultiline</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_REDUNDANT/@EntryValue">False</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/CONSTRUCTOR_OR_DESTRUCTOR_BODY/@EntryValue">BlockBody</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Explicit</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_PRIVATE_MODIFIER/@EntryValue">Implicit</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/FORCE_ATTRIBUTE_STYLE/@EntryValue">Separate</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/LOCAL_FUNCTION_BODY/@EntryValue">BlockBody</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">BlockBody</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/MODIFIERS_ORDER/@EntryValue">internal volatile public private new static async protected extern sealed override virtual unsafe abstract readonly</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/NAMESPACE_BODY/@EntryValue">FileScoped</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/OBJECT_CREATION_WHEN_TYPE_EVIDENT/@EntryValue">ExplicitlyTyped</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/OBJECT_CREATION_WHEN_TYPE_NOT_EVIDENT/@EntryValue">ExplicitlyTyped</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_GROUP_NON_OBVIOUS_OPERATIONS/@EntryValue">Conditional</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_NON_OBVIOUS_OPERATIONS/@EntryValue">Shift, Bitwise, Conditional</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_REDUNDANCY_STYLE/@EntryValue">Remove</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/PARENTHESES_SAME_TYPE_OPERATIONS/@EntryValue">True</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_DECLARED_IN/@EntryValue">0</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_OWNER_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_STATEMENT_CONDITIONS/@EntryValue">False</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGNMENT_TAB_FILL_STYLE/@EntryValue">USE_TABS_ONLY</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_BLOCK_STATEMENTS/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_MULTILINE_STATEMENTS/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_START_COMMENT/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AFTER_USING_LIST/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_AUTO_PROPERTY/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_FIELD/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_INVOCABLE/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_LOCAL_METHOD/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_NAMESPACE/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_PROPERTY/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_REGION/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_SINGLE_LINE_TYPE/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_AROUND_TYPE/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_BEFORE_SINGLE_LINE_COMMENT/@EntryValue">0</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_INSIDE_REGION/@EntryValue">0</s:Int64>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_CASE_FROM_SWITCH/@EntryValue">False</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_PARS/@EntryValue">OUTSIDE</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_PREPROCESSOR_REGION/@EntryValue">USUAL_INDENT</s:String>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_SIZE/@EntryValue">8</s:Int64>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_STATEMENT_PARS/@EntryValue">OUTSIDE</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_STYLE/@EntryValue">Tab</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_TYPE_CONSTRAINTS/@EntryValue">False</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INITIALIZER_BRACES/@EntryValue">END_OF_LINE</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_COMMENTS/@EntryValue">False</s:Boolean>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_CODE/@EntryValue">100</s:Int64>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_DECLARATIONS/@EntryValue">100</s:Int64>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_ATTRIBUTE_ARRANGEMENT/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_DECLARATION_BLOCK_ARRANGEMENT/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EMBEDDED_ARRANGEMENT/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EMBEDDED_BLOCK_ARRANGEMENT/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_ENUM_ARRANGEMENT/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_EXPR_MEMBER_ARRANGEMENT/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_INITIALIZER_ARRANGEMENT/@EntryValue">True</s:Boolean>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_INITIALIZER_ELEMENTS_ON_LINE/@EntryValue">2</s:Int64>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/NESTED_TERNARY_STYLE/@EntryValue">SIMPLE_WRAP</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/OTHER_BRACES/@EntryValue">END_OF_LINE</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_EXPR_ACCESSOR_ON_SINGLE_LINE/@EntryValue">ALWAYS</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_EXPR_METHOD_ON_SINGLE_LINE/@EntryValue">ALWAYS</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_EXPR_PROPERTY_ON_SINGLE_LINE/@EntryValue">ALWAYS</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_METHOD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ACCESSOR_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ENUM_ON_SINGLE_LINE/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_INITIALIZER_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_METHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_TYPE_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/REMOVE_BLANK_LINES_NEAR_BRACES_IN_CODE/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/REMOVE_BLANK_LINES_NEAR_BRACES_IN_DECLARATIONS/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_ARRAY_ACCESS_BRACKETS/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_ARRAY_RANK_BRACKETS/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_DEFAULT_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_EMPTY_METHOD_CALL_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_EMPTY_METHOD_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_EXTENDS_COLON/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_METHOD_CALL_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_METHOD_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_NAMEOF_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_NEW_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_TYPE_PARAMETER_CONSTRAINT_COLON/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_TYPEOF_PARENTHESES/@EntryValue">True</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/TYPE_DECLARATION_BRACES/@EntryValue">END_OF_LINE</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARRAY_INITIALIZER_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_BINARY_OPSIGN/@EntryValue">True</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_CHAINED_BINARY_EXPRESSIONS/@EntryValue">WRAP_IF_LONG</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_CHAINED_METHOD_CALLS/@EntryValue">WRAP_IF_LONG</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_FOR_STMT_HEADER_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
-	<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LIMIT/@EntryValue">527</s:Int64>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINQ_EXPRESSIONS/@EntryValue">CHOP_ALWAYS</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_MULTIPLE_DECLARATION_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_OBJECT_AND_COLLECTION_INITIALIZER_STYLE/@EntryValue">CHOP_ALWAYS</s:String>
-	<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_TERNARY_EXPR_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/CSharpUsing/AddImportsToDeepestScope/@EntryValue">False</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/PreferExplicitDiscardDeclaration/@EntryValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/PreferSeparateDeconstructedVariablesDeclaration/@EntryValue">True</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
-	<s:Boolean x:Key="/Default/CodeStyle/Naming/CSharpNaming/ApplyAutoDetectedRules/@EntryValue">False</s:Boolean>
-	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
-	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
-	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
-	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;</s:String>
-	<s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=CAF4ECB3AC41AE43BD233D613AC1562C/@KeyIndexDefined">True</s:Boolean>
-	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=CAF4ECB3AC41AE43BD233D613AC1562C/AbsolutePath/@EntryValue">Terminal.sln.DotSettings</s:String>
-	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=CAF4ECB3AC41AE43BD233D613AC1562C/RelativePath/@EntryValue"></s:String>
-	<s:Boolean x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=F728A143A60F2142985180A92B1C45E8/@KeyIndexDefined">True</s:Boolean>
-	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=F728A143A60F2142985180A92B1C45E8/AbsolutePath/@EntryValue">/Users/alex/Development/Terminal.Gui/Terminal.sln.DotSettings</s:String>
-	<s:String x:Key="/Default/Environment/InjectedLayers/FileInjectedLayer/=F728A143A60F2142985180A92B1C45E8/RelativePath/@EntryValue"></s:String>
-	<s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileCAF4ECB3AC41AE43BD233D613AC1562C/@KeyIndexDefined">True</s:Boolean>
-	<s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileCAF4ECB3AC41AE43BD233D613AC1562C/RelativePriority/@EntryValue">1</s:Double>
-	<s:Boolean x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileF728A143A60F2142985180A92B1C45E8/@KeyIndexDefined">True</s:Boolean>
-	<s:Double x:Key="/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileF728A143A60F2142985180A92B1C45E8/RelativePriority/@EntryValue">2</s:Double>
-	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/UserDictionary/Words/=argb/@EntryIndexedValue">True</s:Boolean>
-	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevel/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 4 - 4
UICatalog/Scenarios/Adornments.cs

@@ -120,8 +120,8 @@ public class Adornments : Scenario {
 		public Attribute Color {
 			get => new (_foregroundColorPicker.SelectedColor, _backgroundColorPicker.SelectedColor);
 			set {
-				_foregroundColorPicker.SelectedColor = value.Foreground.ColorName;
-				_backgroundColorPicker.SelectedColor = value.Background.ColorName;
+				_foregroundColorPicker.SelectedColor = value.Foreground.GetClosestNamedColor ();
+				_backgroundColorPicker.SelectedColor = value.Background.GetClosestNamedColor ();
 			}
 		}
 
@@ -208,7 +208,7 @@ public class Adornments : Scenario {
 			// Foreground ColorPicker.
 			_foregroundColorPicker.X = -1;
 			_foregroundColorPicker.Y = Pos.Bottom (copyTop) + 1;
-			_foregroundColorPicker.SelectedColor = Color.Foreground.ColorName;
+			_foregroundColorPicker.SelectedColor = Color.Foreground.GetClosestNamedColor ();
 			_foregroundColorPicker.ColorChanged += (o, a) =>
 				AttributeChanged?.Invoke (this,
 					new Attribute (_foregroundColorPicker.SelectedColor, _backgroundColorPicker.SelectedColor));
@@ -217,7 +217,7 @@ public class Adornments : Scenario {
 			// Background ColorPicker.
 			_backgroundColorPicker.X = Pos.Right (_foregroundColorPicker) - 1;
 			_backgroundColorPicker.Y = Pos.Top (_foregroundColorPicker);
-			_backgroundColorPicker.SelectedColor = Color.Background.ColorName;
+			_backgroundColorPicker.SelectedColor = Color.Background.GetClosestNamedColor ();
 			_backgroundColorPicker.ColorChanged += (o, a) =>
 				AttributeChanged?.Invoke (this,
 					new Attribute (

+ 3 - 3
UICatalog/Scenarios/BasicColors.cs

@@ -37,7 +37,7 @@ namespace UICatalog.Scenarios {
 				foreach (ColorName fg in colors) {
 					var c = new Attribute (fg, bg);
 					var t = x.ToString ();
-					var l = new Label (x, y, t [t.Length - 1].ToString ()) {
+					var l = new Label (x, y, t [^1].ToString ()) {
 						ColorScheme = new ColorScheme () { Normal = c }
 					};
 					Win.Add (l);
@@ -90,10 +90,10 @@ namespace UICatalog.Scenarios {
 				if (e.MouseEvent.View != null) {
 					var fore = e.MouseEvent.View.GetNormalColor ().Foreground;
 					var back = e.MouseEvent.View.GetNormalColor ().Background;
-					lblForeground.Text = $"#{fore.R:X2}{fore.G:X2}{fore.B:X2} {fore.ColorName} ";
+					lblForeground.Text = $"#{fore.R:X2}{fore.G:X2}{fore.B:X2} {fore.GetClosestNamedColor ()} ";
 					viewForeground.ColorScheme = new ColorScheme (viewForeground.ColorScheme) { Normal = new Attribute (fore, fore) };
 
-					lblBackground.Text = $"#{back.R:X2}{back.G:X2}{back.B:X2} {back.ColorName} ";
+					lblBackground.Text = $"#{back.R:X2}{back.G:X2}{back.B:X2} {back.GetClosestNamedColor ()} ";
 					viewBackground.ColorScheme = new ColorScheme (viewBackground.ColorScheme) { Normal = new Attribute (back, back) };
 				}
 			};

+ 2 - 2
UICatalog/Scenarios/ColorPicker.cs

@@ -87,8 +87,8 @@ public class ColorPickers : Scenario {
 		Win.Add (_demoView);
 
 		// Set default colors.
-		foregroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Foreground.ColorName;
-		backgroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Background.ColorName;
+		foregroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Foreground.GetClosestNamedColor ();
+		backgroundColorPicker.SelectedColor = _demoView.SuperView.ColorScheme.Normal.Background.GetClosestNamedColor ();
 		Win.Initialized += (s, e) => Win.LayoutSubviews ();
 	}
 

+ 2 - 2
UICatalog/Scenarios/ProgressBarStyles.cs

@@ -89,7 +89,7 @@ public class ProgressBarStyles : Scenario {
 		};
 		editor.Add (fgColorPickerBtn);
 		fgColorPickerBtn.Clicked += (s, e) => {
-			var newColor = ChooseColor (fgColorPickerBtn.Text, editor.ViewToEdit.ColorScheme.HotNormal.Foreground.ColorName);
+			var newColor = ChooseColor (fgColorPickerBtn.Text, editor.ViewToEdit.ColorScheme.HotNormal.Foreground.GetClosestNamedColor ());
 			var cs = new ColorScheme (editor.ViewToEdit.ColorScheme) {
 				HotNormal = new Attribute (newColor, editor.ViewToEdit.ColorScheme.HotNormal.Background)
 			};
@@ -103,7 +103,7 @@ public class ProgressBarStyles : Scenario {
 		};
 		editor.Add (bgColorPickerBtn);
 		bgColorPickerBtn.Clicked += (s, e) => {
-			var newColor = ChooseColor (fgColorPickerBtn.Text, editor.ViewToEdit.ColorScheme.HotNormal.Background.ColorName);
+			var newColor = ChooseColor (fgColorPickerBtn.Text, editor.ViewToEdit.ColorScheme.HotNormal.Background.GetClosestNamedColor ());
 			var cs = new ColorScheme (editor.ViewToEdit.ColorScheme) {
 				HotNormal = new Attribute (editor.ViewToEdit.ColorScheme.HotNormal.Foreground, newColor)
 			};

+ 5 - 5
UICatalog/Scenarios/TrueColors.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
@@ -103,10 +103,10 @@ namespace UICatalog.Scenarios {
 				var l = new Label (" ") {
 					X = dx++,
 					Y = y,
-					ColorScheme = new ColorScheme () {
-						Normal = new Terminal.Gui.Attribute (
-						colorFunc (i > 255 ? 255 : i),
-						colorFunc (i > 255 ? 255 : i)
+					ColorScheme = new ColorScheme {
+						Normal = new Attribute (
+							colorFunc (Math.Clamp (i, 0, 255)),
+							colorFunc (Math.Clamp (i, 0, 255))
 						)
 					}
 				};

+ 15 - 0
UnitTests/.filenesting.json

@@ -0,0 +1,15 @@
+{
+  "help": "https://go.microsoft.com/fwlink/?linkid=866610",
+  "root": false,
+  "dependentFileProviders": {
+    "add": {
+      "pathSegment": {
+        "add": {
+          ".*": [
+            ".cs"
+          ]
+        }
+      }
+    }
+  }
+}

+ 5 - 6
UnitTests/Configuration/JsonConverterTests.cs

@@ -1,9 +1,8 @@
 using System.Text.Encodings.Web;
 using System.Text.Json;
 using System.Text.Unicode;
-using Xunit;
 
-namespace Terminal.Gui.ConfigurationTests; 
+namespace Terminal.Gui.ConfigurationTests;
 
 public class ColorJsonConverterTests {
 	[Theory]
@@ -181,14 +180,14 @@ public class AttributeJsonConverterTests {
 		// Test deserializing from human-readable color names
 		string json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\"}";
 		var attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
-		Assert.Equal (Color.Blue, attribute.Foreground.ColorName);
-		Assert.Equal (Color.Green, attribute.Background.ColorName);
+		Assert.Equal (Color.Blue, attribute.Foreground.GetClosestNamedColor ());
+		Assert.Equal (Color.Green, attribute.Background.GetClosestNamedColor ());
 
 		// Test deserializing from RGB values
 		json = "{\"Foreground\":\"rgb(255,0,0)\",\"Background\":\"rgb(0,255,0)\"}";
 		attribute = JsonSerializer.Deserialize<Attribute> (json, ConfigurationManagerTests._jsonOptions);
-		Assert.Equal (Color.Red, attribute.Foreground.ColorName);
-		Assert.Equal (Color.BrightGreen, attribute.Background.ColorName);
+		Assert.Equal (Color.Red, attribute.Foreground.GetClosestNamedColor ());
+		Assert.Equal (Color.BrightGreen, attribute.Background.GetClosestNamedColor ());
 	}
 
 	[Fact, AutoInitShutdown]

+ 1 - 1
UnitTests/Configuration/ThemeTests.cs

@@ -157,7 +157,7 @@ namespace Terminal.Gui.ConfigurationTests {
 			((Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue) ["test"] = colorScheme;
 
 			colorSchemes = (Dictionary<string, ColorScheme>)theme ["ColorSchemes"].PropertyValue;
-			Assert.Equal (Colors.ColorSchemes.Count + 1, colorSchemes.Count);
+			Assert.Equal (Colors.ColorSchemes.Count, colorSchemes.Count);
 
 			// Act
 			theme.Update (newTheme);

+ 173 - 0
UnitTests/Drawing/ColorTests.Constructors.cs

@@ -0,0 +1,173 @@
+namespace Terminal.Gui.DrawingTests;
+
+public partial class ColorTests {
+
+	[Fact]
+	public void Constructor_Empty_ReturnsColorWithZeroValue ()
+	{
+		Color color = new ();
+		Assert.Multiple (
+			() => Assert.Equal (0, color.Rgba),
+			() => Assert.Equal (0u, color.Argb),
+			() => Assert.Equal (0, color.R),
+			() => Assert.Equal (0, color.G),
+			() => Assert.Equal (0, color.B),
+			() => Assert.Equal (0, color.A)
+		);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void Constructor_WithByteRGBAValues_AllValuesCorrect ([CombinatorialValues (0, 1, 254)] byte r, [CombinatorialValues (0, 1, 253)] byte g, [CombinatorialValues (0, 1, 252)] byte b, [CombinatorialValues (0, 1, 251)] byte a)
+	{
+		var color = new Color (r, g, b, a);
+
+		ReadOnlySpan<byte> bytes = [b, g, r, a];
+		int expectedRgba = BitConverter.ToInt32 (bytes);
+		uint expectedArgb = BitConverter.ToUInt32 (bytes);
+
+		Assert.Multiple (
+			() => Assert.Equal (r, color.R),
+			() => Assert.Equal (g, color.G),
+			() => Assert.Equal (b, color.B),
+			() => Assert.Equal (a, color.A),
+			() => Assert.Equal (expectedRgba, color.Rgba),
+			() => Assert.Equal (expectedArgb, color.Argb)
+		);
+	}
+	[Theory]
+	[CombinatorialData]
+	public void Constructor_WithByteRGBValues_AllValuesCorrect ([CombinatorialValues (0, 1, 254)] byte r, [CombinatorialValues (0, 1, 253)] byte g, [CombinatorialValues (0, 1, 252)] byte b)
+	{
+		var color = new Color (r, g, b);
+
+		ReadOnlySpan<byte> bytes = [b, g, r, 255];
+		int expectedRgba = BitConverter.ToInt32 (bytes);
+		uint expectedArgb = BitConverter.ToUInt32 (bytes);
+
+		Assert.Multiple (
+			() => Assert.Equal (r, color.R),
+			() => Assert.Equal (g, color.G),
+			() => Assert.Equal (b, color.B),
+			() => Assert.Equal (byte.MaxValue, color.A),
+			() => Assert.Equal (expectedRgba, color.Rgba),
+			() => Assert.Equal (expectedArgb, color.Argb)
+		);
+	}
+
+	[Theory]
+	[MemberData (nameof (ColorTestsTheoryDataGenerators.Constructor_WithColorName_AllChannelsCorrect), MemberType = typeof (ColorTestsTheoryDataGenerators))]
+	public void Constructor_WithColorName_AllChannelsCorrect (ColorName cname, ValueTuple<byte, byte, byte> expectedColorValues)
+	{
+		var color = new Color (cname);
+
+		(byte r, byte g, byte b) = expectedColorValues;
+		Assert.Multiple (
+			() => Assert.Equal (r, color.R),
+			() => Assert.Equal (g, color.G),
+			() => Assert.Equal (b, color.B),
+			() => Assert.Equal (byte.MaxValue, color.A)
+		);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void Constructor_WithInt32_AllValuesCorrect ([CombinatorialValues (0, 1, 254)] byte r, [CombinatorialValues (0, 1, 253)] byte g, [CombinatorialValues (0, 1, 252)] byte b, [CombinatorialValues (0, 1, 251)] byte a)
+	{
+		ReadOnlySpan<byte> bytes = [b, g, r, a];
+		int expectedRgba = BitConverter.ToInt32 (bytes);
+		uint expectedArgb = BitConverter.ToUInt32 (bytes);
+
+		var color = new Color (expectedRgba);
+
+		Assert.Multiple (
+			() => Assert.Equal (r, color.R),
+			() => Assert.Equal (g, color.G),
+			() => Assert.Equal (b, color.B),
+			() => Assert.Equal (a, color.A),
+			() => Assert.Equal (expectedRgba, color.Rgba),
+			() => Assert.Equal (expectedArgb, color.Argb)
+		);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void Constructor_WithInt32RGBAValues_AllValuesCorrect ([CombinatorialRandomData (Count = 4, Minimum = 0, Maximum = 255)] int r, [CombinatorialRandomData (Count = 4, Minimum = 0, Maximum = 255)] int g, [CombinatorialRandomData (Count = 4, Minimum = 0, Maximum = 255)] int b, [CombinatorialRandomData (Count = 4, Minimum = 0, Maximum = 255)] int a)
+	{
+		var color = new Color (r, g, b, a);
+
+		Assert.Multiple (
+			() => Assert.Equal (r, color.R),
+			() => Assert.Equal (g, color.G),
+			() => Assert.Equal (b, color.B),
+			() => Assert.Equal (a, color.A)
+		);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void Constructor_WithInt32RGBValues_AllValuesCorrect ([CombinatorialRandomData (Count = 4, Minimum = 0, Maximum = 255)] int r, [CombinatorialRandomData (Count = 4, Minimum = 0, Maximum = 255)] int g, [CombinatorialRandomData (Count = 4, Minimum = 0, Maximum = 255)] int b)
+	{
+		var color = new Color (r, g, b);
+
+		Assert.Multiple (
+			() => Assert.Equal (r, color.R),
+			() => Assert.Equal (g, color.G),
+			() => Assert.Equal (b, color.B),
+			() => Assert.Equal (byte.MaxValue, color.A)
+		);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void Constructor_WithString_EmptyOrWhitespace_ThrowsArgumentException ([CombinatorialValues ("", "\t", " ", "\r", "\r\n", "\n", "   ")] string badString)
+	{
+		Assert.Throws<ArgumentException> (() => Color.Parse (badString));
+	}
+
+	[Fact]
+	public void Constructor_WithString_Null_ThrowsArgumentNullException ()
+	{
+		Assert.Throws<ArgumentNullException> (static () => Color.Parse (null));
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void Constructor_WithUInt32_AllChannelsCorrect ([CombinatorialValues (0, 1, 254)] byte r, [CombinatorialValues (0, 1, 253)] byte g, [CombinatorialValues (0, 1, 252)] byte b, [CombinatorialValues (0, 1, 251)] byte a)
+	{
+		ReadOnlySpan<byte> bytes = [b, g, r, a];
+		uint expectedArgb = BitConverter.ToUInt32 (bytes);
+
+		var color = new Color (expectedArgb);
+
+		Assert.Multiple (
+			() => Assert.Equal (r, color.R),
+			() => Assert.Equal (g, color.G),
+			() => Assert.Equal (b, color.B),
+			() => Assert.Equal (a, color.A)
+		);
+	}
+}
+public static partial class ColorTestsTheoryDataGenerators {
+	public static TheoryData<ColorName, ValueTuple<byte, byte, byte>> Constructor_WithColorName_AllChannelsCorrect ()
+	{
+		TheoryData<ColorName, ValueTuple<byte, byte, byte>> data = [];
+		data.Add (ColorName.Black, new ValueTuple<byte, byte, byte> (12, 12, 12));
+		data.Add (ColorName.Blue, new ValueTuple<byte, byte, byte> (0, 55, 218));
+		data.Add (ColorName.Green, new ValueTuple<byte, byte, byte> (19, 161, 14));
+		data.Add (ColorName.Cyan, new ValueTuple<byte, byte, byte> (58, 150, 221));
+		data.Add (ColorName.Red, new ValueTuple<byte, byte, byte> (197, 15, 31));
+		data.Add (ColorName.Magenta, new ValueTuple<byte, byte, byte> (136, 23, 152));
+		data.Add (ColorName.Yellow, new ValueTuple<byte, byte, byte> (128, 64, 32));
+		data.Add (ColorName.Gray, new ValueTuple<byte, byte, byte> (204, 204, 204));
+		data.Add (ColorName.DarkGray, new ValueTuple<byte, byte, byte> (118, 118, 118));
+		data.Add (ColorName.BrightBlue, new ValueTuple<byte, byte, byte> (59, 120, 255));
+		data.Add (ColorName.BrightGreen, new ValueTuple<byte, byte, byte> (22, 198, 12));
+		data.Add (ColorName.BrightCyan, new ValueTuple<byte, byte, byte> (97, 214, 214));
+		data.Add (ColorName.BrightRed, new ValueTuple<byte, byte, byte> (231, 72, 86));
+		data.Add (ColorName.BrightMagenta, new ValueTuple<byte, byte, byte> (180, 0, 158));
+		data.Add (ColorName.BrightYellow, new ValueTuple<byte, byte, byte> (249, 241, 165));
+		data.Add (ColorName.White, new ValueTuple<byte, byte, byte> (242, 242, 242));
+		return data;
+	}
+}

+ 180 - 0
UnitTests/Drawing/ColorTests.Operators.cs

@@ -0,0 +1,180 @@
+using System.Numerics;
+using System.Reflection;
+
+namespace Terminal.Gui.DrawingTests;
+
+public partial class ColorTests {
+
+	[Theory]
+	[Trait ("Category", "Operators")]
+	[CombinatorialData]
+	public void ExplicitOperator_ToVector3_ReturnsCorrectValue ([CombinatorialRange (0, 255, 51)] byte r, [CombinatorialRange (0, 255, 51)] byte g, [CombinatorialRange (0, 255, 51)] byte b, [CombinatorialValues (0, 255)] byte a)
+	{
+		Color color = new (r, g, b, a);
+
+		Vector3 vector = (Vector3)color;
+
+		Assert.Equal (color.R, vector.X);
+		Assert.Equal (color.G, vector.Y);
+		Assert.Equal (color.B, vector.Z);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void GeneratedEqualityOperators_BehaveAsExpected ([CombinatorialValues (0, short.MaxValue, int.MaxValue, uint.MaxValue)] uint u1, [CombinatorialValues (0, short.MaxValue, int.MaxValue, uint.MaxValue)] uint u2)
+	{
+		Color color1 = u1;
+		Color color2 = u2;
+
+		if ( u1 == u2 ) {
+			Assert.True (color1 == color2);
+			Assert.False (color1 != color2);
+		}
+		else {
+			Assert.True (color1 != color2);
+			Assert.False (color1 == color2);
+		}
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void GetHashCode_DelegatesTo_Rgba ([CombinatorialRandomData (Count = 16)] int rgba)
+	{
+		Color color = new (rgba);
+
+		Assert.Equal (rgba.GetHashCode (), color.GetHashCode ());
+	}
+
+	[Theory]
+	[Trait ("Category", "Operators")]
+	[MemberData (nameof (ColorTestsTheoryDataGenerators.ExplicitOperator_FromColorName_RoundTripsCorrectly), MemberType = typeof (ColorTestsTheoryDataGenerators))]
+	public void ImplicitOperator_FromColorName_ReturnsCorrectColorValue (ColorName cname, Color expectedColor)
+	{
+		Color color = cname;
+
+		Assert.Equal (expectedColor, color);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void ImplicitOperator_FromInt32_ReturnsCorrectColorValue ([CombinatorialRandomData (Count = 16)] int expectedValue)
+	{
+		Color color = expectedValue;
+
+		Assert.Equal (expectedValue, color.Rgba);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void ImplicitOperator_FromUInt32_ReturnsCorrectColorValue ([CombinatorialRandomData (Count = 16)] uint expectedValue)
+	{
+		Color color = expectedValue;
+
+		Assert.Equal (expectedValue, color.Argb);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void ImplicitOperator_FromVector3_ReturnsCorrectColorValue ([CombinatorialRange (0, 255, 51)] byte r, [CombinatorialRange (0, 255, 51)] byte g, [CombinatorialRange (0, 255, 51)] byte b)
+	{
+		Vector3 vector = new (r, g, b);
+		Color color = vector;
+
+		Assert.Equal (r, color.R);
+		Assert.Equal (g, color.G);
+		Assert.Equal (b, color.B);
+		Assert.Equal (byte.MaxValue, color.A);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void ImplicitOperator_FromVector4_ReturnsCorrectColorValue ([CombinatorialRange (0, 255, 51)] byte r, [CombinatorialRange (0, 255, 51)] byte g, [CombinatorialRange (0, 255, 51)] byte b, [CombinatorialValues (0, 255)] byte a)
+	{
+		Vector4 vector = new (r, g, b, a);
+		Color color = vector;
+
+		Assert.Equal (r, color.R);
+		Assert.Equal (g, color.G);
+		Assert.Equal (b, color.B);
+		Assert.Equal (a, color.A);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void ImplicitOperator_ToInt32_ReturnsCorrectInt32Value ([CombinatorialRandomData (Count = 16)] int expectedValue)
+	{
+		Color color = new (expectedValue);
+
+		int colorAsInt32 = color;
+
+		Assert.Equal (expectedValue, colorAsInt32);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void ImplicitOperator_ToUInt32_ReturnsCorrectUInt32Value ([CombinatorialRandomData (Count = 16)] uint expectedValue)
+	{
+		Color color = new (expectedValue);
+
+		uint colorAsInt32 = color;
+
+		Assert.Equal (expectedValue, colorAsInt32);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	[Trait ("Category", "Operators")]
+	public void ImplicitOperator_ToVector4_ReturnsCorrectVector4Value ([CombinatorialRange (0, 255, 51)] byte r, [CombinatorialRange (0, 255, 51)] byte g, [CombinatorialRange (0, 255, 51)] byte b, [CombinatorialValues (0, 255)] byte a)
+	{
+		Color color = new (r, g, b, a);
+		Vector4 vector = color;
+
+		Assert.Equal (r, vector.X);
+		Assert.Equal (g, vector.Y);
+		Assert.Equal (b, vector.Z);
+		Assert.Equal (a, vector.W);
+	}
+}
+public static partial class ColorTestsTheoryDataGenerators {
+
+	public static TheoryData<ColorName, Color> ExplicitOperator_FromColorName_RoundTripsCorrectly ()
+	{
+		TheoryData<ColorName, Color> data = [];
+		data.Add (ColorName.Black, new Color (12, 12, 12));
+		data.Add (ColorName.Blue, new Color (0, 55, 218));
+		data.Add (ColorName.Green, new Color (19, 161, 14));
+		data.Add (ColorName.Cyan, new Color (58, 150, 221));
+		data.Add (ColorName.Red, new Color (197, 15, 31));
+		data.Add (ColorName.Magenta, new Color (136, 23, 152));
+		data.Add (ColorName.Yellow, new Color (128, 64, 32));
+		data.Add (ColorName.Gray, new Color (204, 204, 204));
+		data.Add (ColorName.DarkGray, new Color (118, 118, 118));
+		data.Add (ColorName.BrightBlue, new Color (59, 120, 255));
+		data.Add (ColorName.BrightGreen, new Color (22, 198, 12));
+		data.Add (ColorName.BrightCyan, new Color (97, 214, 214));
+		data.Add (ColorName.BrightRed, new Color (231, 72, 86));
+		data.Add (ColorName.BrightMagenta, new Color (180, 0, 158));
+		data.Add (ColorName.BrightYellow, new Color (249, 241, 165));
+		data.Add (ColorName.White, new Color (242, 242, 242));
+		return data;
+	}
+
+	public static TheoryData<FieldInfo, int> Fields_At_Expected_Offsets ()
+	{
+		TheoryData<FieldInfo, int> data = [];
+		data.Add (typeof (Color).GetField ("Argb", BindingFlags.Instance | BindingFlags.Public | BindingFlags.ExactBinding), 0);
+		data.Add (typeof (Color).GetField ("Rgba", BindingFlags.Instance | BindingFlags.Public | BindingFlags.ExactBinding), 0);
+		data.Add (typeof (Color).GetField ("B", BindingFlags.Instance | BindingFlags.Public | BindingFlags.ExactBinding), 0);
+		data.Add (typeof (Color).GetField ("G", BindingFlags.Instance | BindingFlags.Public | BindingFlags.ExactBinding), 1);
+		data.Add (typeof (Color).GetField ("R", BindingFlags.Instance | BindingFlags.Public | BindingFlags.ExactBinding), 2);
+		data.Add (typeof (Color).GetField ("A", BindingFlags.Instance | BindingFlags.Public | BindingFlags.ExactBinding), 3);
+		return data;
+	}
+}

+ 148 - 0
UnitTests/Drawing/ColorTests.ParsingAndFormatting.cs

@@ -0,0 +1,148 @@
+#nullable enable
+using System.Buffers.Binary;
+using System.Globalization;
+
+namespace Terminal.Gui.DrawingTests;
+
+public partial class ColorTests {
+	[Fact]
+	public void Color_ToString_WithNamedColor ()
+	{
+		// Arrange
+		var color = new Color (0, 55, 218); // Blue
+
+		// Act
+		var colorString = color.ToString ();
+
+		// Assert
+		Assert.Equal ("Blue", colorString);
+	}
+
+	[Fact]
+	public void Color_ToString_WithRGBColor ()
+	{
+		// Arrange
+		var color = new Color (1, 64, 32); // Custom RGB color
+
+		// Act
+		var colorString = color.ToString ();
+
+		// Assert
+		Assert.Equal ("#014020", colorString);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void Parse_And_ToString_RoundTrip_For_Known_FormatStrings ([CombinatorialValues (null, "", "g", "G", "d", "D")] string formatString, [CombinatorialValues (0, 64, 255)] byte r, [CombinatorialValues (0, 64, 255)] byte g, [CombinatorialValues (0, 64, 255)] byte b)
+	{
+		Color constructedColor = new (r, g, b, 255);
+
+		// Pre-conditions for the rest of the test to be valid
+		Assert.Equal (r, constructedColor.R);
+		Assert.Equal (g, constructedColor.G);
+		Assert.Equal (b, constructedColor.B);
+		Assert.Equal (255, constructedColor.A);
+
+		//Get the ToString result with the specified format string
+		string formattedColorString = constructedColor.ToString (formatString);
+
+		// Now parse that string
+		Color parsedColor = Color.Parse (formattedColorString);
+
+		// They should have identical underlying values
+		Assert.Equal (constructedColor.Argb, parsedColor.Argb);
+	}
+
+	[Theory]
+	[CombinatorialData]
+	public void ToString_WithInvariantCultureAndNullString_IsSameAsParameterless ([CombinatorialValues (0, 64, 128, 255)] byte r, [CombinatorialValues (0, 64, 128, 255)] byte g, [CombinatorialValues (0, 64, 128, 255)] byte b)
+	{
+		string expected = $"#{r:X2}{g:X2}{b:X2}";
+		Color testColor = new (r, g, b);
+
+		string testStringWithExplicitInvariantCulture = testColor.ToString (null, CultureInfo.InvariantCulture);
+		Assert.Equal (expected, testStringWithExplicitInvariantCulture);
+
+
+		string parameterlessToStringValue = testColor.ToString ();
+		Assert.Equal (parameterlessToStringValue, testStringWithExplicitInvariantCulture);
+	}
+
+	[Theory]
+	[MemberData (nameof (ColorTestsTheoryDataGenerators.TryParse_string_Returns_False_For_Invalid_Inputs), MemberType = typeof (ColorTestsTheoryDataGenerators))]
+	public void TryParse_string_Returns_False_For_Invalid_Inputs (string input)
+	{
+		bool tryParseStatus = Color.TryParse (input, out Color? color);
+		Assert.False (tryParseStatus);
+		Assert.Null (color);
+	}
+
+	[Theory]
+	[MemberData (nameof (ColorTestsTheoryDataGenerators.TryParse_string_Returns_True_For_Valid_Inputs), MemberType = typeof (ColorTestsTheoryDataGenerators))]
+	public void TryParse_string_Returns_True_For_Valid_Inputs (string input, int expectedColorArgb)
+	{
+		bool tryParseStatus = Color.TryParse (input, out Color? color);
+		Assert.True (tryParseStatus);
+		Assert.NotNull (color);
+		Assert.IsType<Color> (color);
+		Assert.Equal (expectedColorArgb, color.Value.Rgba);
+	}
+}
+public static partial class ColorTestsTheoryDataGenerators {
+	public static TheoryData<string?> TryParse_string_Returns_False_For_Invalid_Inputs ()
+	{
+		TheoryData<string?> values = [
+			null
+		];
+		for ( char i = char.MinValue; i < 255; i++ ) {
+			if ( !char.IsAsciiDigit (i) ) {
+				values.Add ($"rgb({i},{i},{i})");
+				values.Add ($"rgba({i},{i},{i})");
+			}
+			if ( !char.IsAsciiHexDigit (i) ) {
+				values.Add ($"#{i}{i}{i}{i}{i}{i}");
+				values.Add ($"#{i}{i}{i}{i}{i}{i}{i}{i}");
+			}
+		}
+		//Also throw in a couple of just badly formatted strings
+		values.Add ("rgbaa(1,2,3,4))");
+		values.Add ("#rgb(1,FF,3,4)");
+		values.Add ("rgb(1,FF,3,4");
+		values.Add ("rgb(1,2,3,4.5))");
+		return values;
+	}
+	public static TheoryData<string?, int> TryParse_string_Returns_True_For_Valid_Inputs ()
+	{
+		TheoryData<string?, int> values = [];
+		for ( byte i = 16; i < 224; i += 32 ) {
+			// Using this so the span only has to be written one way.
+			int expectedRgb = BinaryPrimitives.ReadInt32LittleEndian ([(byte)(i + 16), i, (byte)(i - 16), 255]);
+			int expectedRgba = BinaryPrimitives.ReadInt32LittleEndian ([(byte)(i + 16), i, (byte)(i - 16), i]);
+			values.Add ($"rgb({i - 16:D},{i:D},{i + 16:D})", expectedRgb);
+			values.Add ($"rgb({i - 16:D},{i:D},{i + 16:D},{i:D})", expectedRgba);
+			values.Add ($"rgb({i - 16:D},{i:D},{i + 16:D})", expectedRgb);
+			values.Add ($"rgba({i - 16:D},{i:D},{i + 16:D},{i:D})", expectedRgba);
+			values.Add ($"#{i - 16:X2}{i:X2}{i + 16:X2}", expectedRgb);
+			values.Add ($"#{i:X2}{i - 16:X2}{i:X2}{i + 16:X2}", expectedRgba);
+		}
+		for ( byte i = 1; i < 0xE; i++ ) {
+			values.Add ($"#{i - 1:X0}{i:X0}{i + 1:X0}", BinaryPrimitives.ReadInt32LittleEndian (
+			[
+				// Have to stick the least significant 4 bits in the most significant 4 bits to duplicate the hex values
+				// Breaking this out just so it's easier to see.
+				(byte)(i + 1 | i + 1 << 4),
+				(byte)(i | i << 4),
+				(byte)(i - 1 | i - 1 << 4),
+				255
+			]));
+			values.Add ($"#{i:X0}{i - 1:X0}{i:X0}{i + 1:X0}", BinaryPrimitives.ReadInt32LittleEndian (
+			[
+				(byte)(i + 1 | i + 1 << 4),
+				(byte)(i | i << 4),
+				(byte)(i - 1 | i - 1 << 4),
+				(byte)(i | i << 4)
+			]));
+		}
+		return values;
+	}
+}

+ 135 - 0
UnitTests/Drawing/ColorTests.TypeChecks.cs

@@ -0,0 +1,135 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Terminal.Gui.DrawingTests;
+
+public partial class ColorTests {
+
+	[Fact]
+	[Trait ("Category", "Type Checks")]
+	[Trait ("Category", "Change Control")]
+	public void ColorName_Has_Exactly_16_Defined_Values () => Assert.Equal (16, Enum.GetValues<ColorName> ().DistinctBy (static cname => (int)cname).Count ());
+
+	[Theory]
+	[Trait ("Category", "Type Checks")]
+	[Trait ("Category", "Change Control")]
+	[MemberData (nameof (ColorTestsTheoryDataGenerators.ColorName_HasCorrectOrdinals), MemberType = typeof (ColorTestsTheoryDataGenerators))]
+	public void ColorName_HasCorrectOrdinals (ColorName cname, int ordinal)
+	{
+		Assert.Equal ((int)cname, ordinal);
+	}
+
+	[Fact]
+	[Trait ("Category", "Type Checks")]
+	[Trait ("Category", "Change Control")]
+	[SkipLocalsInit]
+	public unsafe void Fields_At_Expected_Offsets ()
+	{
+		// Raw write to the stack and read-back as a Color
+		// Byte order is little endian
+		Color* c = stackalloc Color [1];
+		int* rgba = (int*)c;
+		*rgba = 0;
+
+		// Pre-conditions. Ensure everything is zero;
+		Assert.Equal (0, c->Rgba);
+		Assert.Equal (0u, c->Argb);
+		Assert.Equal ((byte)0, c->R);
+		Assert.Equal ((byte)0, c->G);
+		Assert.Equal ((byte)0, c->B);
+		Assert.Equal ((byte)0, c->A);
+
+		byte* bytePointer = (byte*)c;
+		// Write value to first byte and read it back in B
+		*bytePointer = 239;
+		Assert.Equal (239, c->Rgba);
+		Assert.Equal (239u, c->Argb);
+		Assert.Equal ((byte)239, c->B);
+		Assert.Equal ((byte)0, c->G);
+		Assert.Equal ((byte)0, c->R);
+		Assert.Equal ((byte)0, c->A);
+
+		// Move to offset 1, write the next value, and check everything again.
+		bytePointer++;
+		*bytePointer = 190;
+		Assert.Equal (48879, c->Rgba);
+		Assert.Equal (48879u, c->Argb);
+		Assert.Equal ((byte)239, c->B);
+		Assert.Equal ((byte)190, c->G);
+		Assert.Equal ((byte)0, c->R);
+		Assert.Equal ((byte)0, c->A);
+
+		// Move to offset 2, write the next value, and check everything again.
+		bytePointer++;
+		*bytePointer = 173;
+		Assert.Equal (11386607, c->Rgba);
+		Assert.Equal (11386607u, c->Argb);
+		Assert.Equal ((byte)239, c->B);
+		Assert.Equal ((byte)190, c->G);
+		Assert.Equal ((byte)173, c->R);
+		Assert.Equal ((byte)0, c->A);
+
+		// Move to offset 3, write the next value, and check everything again.
+		bytePointer++;
+		*bytePointer = 222;
+		Assert.Equal (-559038737, c->Rgba);
+		Assert.Equal (0x_DEAD_BEEF, c->Argb);
+		Assert.Equal ((byte)239, c->B);
+		Assert.Equal ((byte)190, c->G);
+		Assert.Equal ((byte)173, c->R);
+		Assert.Equal ((byte)222, c->A);
+	}
+
+	[Theory]
+	[Trait ("Category", "Type Checks")]
+	[Trait ("Category", "Change Control")]
+	[CombinatorialData]
+	public void Implements_Expected_Interfaces (
+		[CombinatorialValues (
+			typeof (IEquatable<Color>),
+			typeof (ISpanParsable<Color>),
+			typeof (IUtf8SpanParsable<Color>),
+			typeof (ISpanFormattable),
+			typeof (IUtf8SpanFormattable),
+			typeof (IMinMaxValue<Color>))]
+		Type expectedInterface)
+	{
+		Assert.Contains (expectedInterface, typeof (Color).GetInterfaces ());
+	}
+	[Fact]
+	[Trait ("Category", "Type Checks")]
+	[Trait ("Category", "Change Control")]
+	public void Is_Explicit_LayoutKind ()
+	{
+		Assert.True (typeof (Color).IsExplicitLayout);
+	}
+	[Fact]
+	[Trait ("Category", "Type Checks")]
+	[Trait ("Category", "Change Control")]
+	public void Is_Value_Type () =>
+		// prove that Color is a value type
+		Assert.True (typeof (Color).IsValueType);
+}
+public static partial class ColorTestsTheoryDataGenerators {
+	public static TheoryData<ColorName, int> ColorName_HasCorrectOrdinals ()
+	{
+		TheoryData<ColorName, int> data = [];
+		data.Add (ColorName.Black, 0);
+		data.Add (ColorName.Blue, 1);
+		data.Add (ColorName.Green, 2);
+		data.Add (ColorName.Cyan, 3);
+		data.Add (ColorName.Red, 4);
+		data.Add (ColorName.Magenta, 5);
+		data.Add (ColorName.Yellow, 6);
+		data.Add (ColorName.Gray, 7);
+		data.Add (ColorName.DarkGray, 8);
+		data.Add (ColorName.BrightBlue, 9);
+		data.Add (ColorName.BrightGreen, 10);
+		data.Add (ColorName.BrightCyan, 11);
+		data.Add (ColorName.BrightRed, 12);
+		data.Add (ColorName.BrightMagenta, 13);
+		data.Add (ColorName.BrightYellow, 14);
+		data.Add (ColorName.White, 15);
+		return data;
+	}
+}

+ 43 - 292
UnitTests/Drawing/ColorTests.cs

@@ -1,327 +1,78 @@
-using System;
-using System.Linq;
-using Xunit;
+#nullable enable
 
 namespace Terminal.Gui.DrawingTests;
 
-public class ColorTests {
-	[Fact]
-	public void Color_Is_Value_Type () =>
-		// prove that Color is a value type
-		Assert.True (typeof (Color).IsValueType);
-
-	[Fact]
-	public void TestAllColors ()
-	{
-		var colorNames = Enum.GetValues (typeof (ColorName)).Cast<int> ().Distinct ().ToList ();
-		var attrs = new Attribute [colorNames.Count];
-
-		var idx = 0;
-		foreach (ColorName bg in colorNames) {
-			attrs [idx] = new Attribute (bg, colorNames.Count - 1 - bg);
-			idx++;
-		}
-		Assert.Equal (16, attrs.Length);
-		Assert.Equal (new Attribute (Color.Black, Color.White), attrs [0]);
-		Assert.Equal (new Attribute (Color.Blue, Color.BrightYellow), attrs [1]);
-		Assert.Equal (new Attribute (Color.Green, Color.BrightMagenta), attrs [2]);
-		Assert.Equal (new Attribute (Color.Cyan, Color.BrightRed), attrs [3]);
-		Assert.Equal (new Attribute (Color.Red, Color.BrightCyan), attrs [4]);
-		Assert.Equal (new Attribute (Color.Magenta, Color.BrightGreen), attrs [5]);
-		Assert.Equal (new Attribute (Color.Yellow, Color.BrightBlue), attrs [6]);
-		Assert.Equal (new Attribute (Color.Gray, Color.DarkGray), attrs [7]);
-		Assert.Equal (new Attribute (Color.DarkGray, Color.Gray), attrs [8]);
-		Assert.Equal (new Attribute (Color.BrightBlue, Color.Yellow), attrs [9]);
-		Assert.Equal (new Attribute (Color.BrightGreen, Color.Magenta), attrs [10]);
-		Assert.Equal (new Attribute (Color.BrightCyan, Color.Red), attrs [11]);
-		Assert.Equal (new Attribute (Color.BrightRed, Color.Cyan), attrs [12]);
-		Assert.Equal (new Attribute (Color.BrightMagenta, Color.Green), attrs [13]);
-		Assert.Equal (new Attribute (Color.BrightYellow, Color.Blue), attrs [14]);
-		Assert.Equal (new Attribute (Color.White, Color.Black), attrs [^1]);
-	}
-
-	[Fact]
-	public void ColorNames_HasOnly16DistinctElements () => Assert.Equal (16, Enum.GetValues (typeof (ColorName)).Cast<int> ().Distinct ().Count ());
-
-	[Fact]
-	public void ColorNames_HaveCorrectOrdinals ()
-	{
-		Assert.Equal (0, (int)ColorName.Black);
-		Assert.Equal (1, (int)ColorName.Blue);
-		Assert.Equal (2, (int)ColorName.Green);
-		Assert.Equal (3, (int)ColorName.Cyan);
-		Assert.Equal (4, (int)ColorName.Red);
-		Assert.Equal (5, (int)ColorName.Magenta);
-		Assert.Equal (6, (int)ColorName.Yellow);
-		Assert.Equal (7, (int)ColorName.Gray);
-		Assert.Equal (8, (int)ColorName.DarkGray);
-		Assert.Equal (9, (int)ColorName.BrightBlue);
-		Assert.Equal (10, (int)ColorName.BrightGreen);
-		Assert.Equal (11, (int)ColorName.BrightCyan);
-		Assert.Equal (12, (int)ColorName.BrightRed);
-		Assert.Equal (13, (int)ColorName.BrightMagenta);
-		Assert.Equal (14, (int)ColorName.BrightYellow);
-		Assert.Equal (15, (int)ColorName.White);
-	}
-
-	[Fact]
-	public void Color_Constructor_WithRGBValues ()
-	{
-		// Arrange
-		var expectedR = 255;
-		var expectedG = 0;
-		var expectedB = 128;
-
-		// Act
-		var color = new Color (expectedR, expectedG, expectedB);
-
-		// Assert
-		Assert.Equal (expectedR, color.R);
-		Assert.Equal (expectedG, color.G);
-		Assert.Equal (expectedB, color.B);
-		Assert.Equal (0xFF, color.A); // Alpha should be FF by default
-	}
-
-	[Fact]
-	public void Color_Constructor_WithAlphaAndRGBValues ()
-	{
-		// Arrange
-		var expectedA = 128;
-		var expectedR = 255;
-		var expectedG = 0;
-		var expectedB = 128;
-
-		// Act
-		var color = new Color (expectedR, expectedG, expectedB, expectedA);
-
-		// Assert
-		Assert.Equal (expectedR, color.R);
-		Assert.Equal (expectedG, color.G);
-		Assert.Equal (expectedB, color.B);
-		Assert.Equal (expectedA, color.A);
-	}
-
-	[Fact]
-	public void Color_Constructor_WithRgbaValue ()
-	{
-		// Arrange
-		var expectedRgba = unchecked((int)0xFF804040); // R: 128, G: 64, B: 64, Alpha: 255
-
-		// Act
-		var color = new Color (expectedRgba);
-
-		// Assert
-		Assert.Equal (128, color.R);
-		Assert.Equal (64, color.G);
-		Assert.Equal (64, color.B);
-		Assert.Equal (255, color.A);
-	}
-
-	[Fact]
-	public void Color_Constructor_WithColorName ()
-	{
-		// Arrange
-		var colorName = ColorName.Blue;
-		var expectedColor = new Color (0, 55, 218); // Blue
-
-		// Act
-		var color = new Color (colorName);
-
-		// Assert
-		Assert.Equal (expectedColor, color);
-	}
-
-	[Fact]
-	public void Color_ToString_WithNamedColor ()
-	{
-		// Arrange
-		var color = new Color (0, 55, 218); // Blue
-
-		// Act
-		var colorString = color.ToString ();
-
-		// Assert
-		Assert.Equal ("Blue", colorString);
-	}
-
-	[Fact]
-	public void Color_ToString_WithRGBColor ()
-	{
-		// Arrange
-		var color = new Color (1, 64, 32); // Custom RGB color
-
-		// Act
-		var colorString = color.ToString ();
-
-		// Assert
-		Assert.Equal ("#014020", colorString);
-	}
-
-	[Fact]
-	public void Color_ImplicitOperator_FromInt ()
-	{
-		// Arrange
-		var Rgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255
-		var expectedColor = new Color (128, 64, 32);
-
-		// Act
-		Color color = Rgba;
-
-		// Assert
-		Assert.Equal (expectedColor, color);
-	}
-
-	[Fact]
-	public void Color_ExplicitOperator_ToInt ()
-	{
-		// Arrange
-		var color = new Color (128, 64, 32);
-		var expectedRgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255
-
-		// Act
-		var Rgba = (int)color;
-
-		// Assert
-		Assert.Equal (expectedRgba, Rgba);
-	}
+public partial class ColorTests {
 
-
-	[Fact]
-	public void Color_ImplicitOperator_FromColorNames ()
+	[Theory]
+	[CombinatorialData]
+	public void Argb_Returns_Expected_Value ([CombinatorialValues (0, 255)] byte a, [CombinatorialRange (0, 255, 51)] byte r, [CombinatorialRange (0, 153, 51)] byte g, [CombinatorialRange (0, 128, 32)] byte b)
 	{
-		// Arrange
-		var colorName = ColorName.Blue;
-		var expectedColor = new Color (0, 55, 218); // Blue
-
-		// Act
-		var color = new Color (colorName);
-
-		// Assert
-		Assert.Equal (expectedColor, color);
+		Color color = new (r, g, b, a);
+		// Color.Rgba is expected to be a signed int32 in little endian order (a,b,g,r)
+		ReadOnlySpan<byte> littleEndianBytes = [b, g, r, a];
+		uint expectedArgb = BitConverter.ToUInt32 (littleEndianBytes);
+		Assert.Equal (expectedArgb, color.Argb);
 	}
 
 	[Fact]
-	public void Color_ExplicitOperator_ToColorNames ()
+	public void Color_ColorName_Get_ReturnsClosestColorName ()
 	{
 		// Arrange
-		var color = new Color (0, 0, 0x80); // Blue
-		var expectedColorName = ColorName.Blue;
+		var color = new Color (128, 64, 40); // Custom RGB color, closest to Yellow
+		var expectedColorName = ColorName.Yellow;
 
 		// Act
-		var colorName = (ColorName)color;
+		var colorName = color.GetClosestNamedColor ();
 
 		// Assert
 		Assert.Equal (expectedColorName, colorName);
 	}
-
-
-
 	[Fact]
-	public void Color_EqualityOperator_WithColorAndColor ()
-	{
-		// Arrange
-		var color1 = new Color (255, 128, 64, 32);
-		var color2 = new Color (255, 128, 64, 32);
-
-		// Act & Assert
-		Assert.True (color1 == color2);
-		Assert.False (color1 != color2);
-	}
-
-	[Fact]
-	public void Color_InequalityOperator_WithColorAndColor ()
-	{
-		// Arrange
-		var color1 = new Color (255, 128, 64, 32);
-		var color2 = new Color (128, 64, 32, 16);
-
-		// Act & Assert
-		Assert.False (color1 == color2);
-		Assert.True (color1 != color2);
-	}
-
-	[Fact]
-	public void Color_EqualityOperator_WithColorNamesAndColor ()
+	public void Color_IsClosestToNamedColor_ReturnsExpectedValue ()
 	{
 		// Arrange
 		var color1 = new Color (ColorName.Red);
 		var color2 = new Color (197, 15, 31); // Red in RGB
 
-		// Act & Assert
-		Assert.True (ColorName.Red == color1);
-		Assert.False (ColorName.Red != color1);
+		Assert.True (color1.IsClosestToNamedColor (ColorName.Red));
 
-		Assert.True (color1 == ColorName.Red);
-		Assert.False (color1 != ColorName.Red);
-
-		Assert.True (color2 == ColorName.Red);
-		Assert.False (color2 != ColorName.Red);
+		Assert.True (color2.IsClosestToNamedColor (ColorName.Red));
 	}
 
-	[Fact]
-	public void Color_InequalityOperator_WithColorNamesAndColor ()
+	[Theory]
+	[MemberData (nameof (ColorTestsTheoryDataGenerators.FindClosestColor_ReturnsClosestColor), MemberType = typeof (ColorTestsTheoryDataGenerators))]
+	public void FindClosestColor_ReturnsClosestColor (Color inputColor, ColorName expectedColorName)
 	{
-		// Arrange
-		var color1 = new Color (ColorName.Red);
-		var color2 = new Color (58, 150, 221); // Cyan in RGB
+		var actualColorName = Color.GetClosestNamedColor (inputColor);
 
-		// Act & Assert
-		Assert.False (ColorName.Red == color2);
-		Assert.True (ColorName.Red != color2);
-
-		Assert.False (color2 == ColorName.Red);
-		Assert.True (color2 != ColorName.Red);
+		Assert.Equal (expectedColorName, actualColorName);
 	}
 
-	[Fact]
-	public void Color_FromColorName_ConvertsColorNamesToColor ()
-	{
-		// Arrange
-		var colorName = ColorName.Red;
-		var expectedColor = new Color (197, 15, 31); // Red in RGB
-
-		// Act
-		var convertedColor = new Color (colorName);
 
-		// Assert
-		Assert.Equal (expectedColor, convertedColor);
-	}
-
-	[Fact]
-	public void Color_ColorName_Get_ReturnsClosestColorName ()
+	[Theory]
+	[CombinatorialData]
+	public void Rgba_Returns_Expected_Value ([CombinatorialValues (0, 255)] byte a, [CombinatorialRange (0, 255, 51)] byte r, [CombinatorialRange (0, 153, 51)] byte g, [CombinatorialRange (0, 128, 32)] byte b)
 	{
-		// Arrange
-		var color = new Color (128, 64, 40); // Custom RGB color, closest to Yellow 
-		var expectedColorName = ColorName.Yellow;
-
-		// Act
-		var colorName = color.ColorName;
-
-		// Assert
-		Assert.Equal (expectedColorName, colorName);
+		Color color = new (r, g, b, a);
+		// Color.Rgba is expected to be a signed int32 in little endian order (a,b,g,r)
+		ReadOnlySpan<byte> littleEndianBytes = [b, g, r, a];
+		int expectedRgba = BitConverter.ToInt32 (littleEndianBytes);
+		Assert.Equal (expectedRgba, color.Rgba);
 	}
+}
+public static partial class ColorTestsTheoryDataGenerators {
 
-	[Fact]
-	public void FindClosestColor_ReturnsClosestColor ()
+	public static TheoryData<Color, ColorName> FindClosestColor_ReturnsClosestColor ()
 	{
-		// Test cases with RGB values and expected closest color names
-		var testCases = new [] {
-			(new Color (0, 0, 0), ColorName.Black),
-			(new Color (255, 255, 255), ColorName.White),
-			(new Color (5, 100, 255), ColorName.BrightBlue),
-			(new Color (0, 255, 0), ColorName.BrightGreen),
-			(new Color (255, 70, 8), ColorName.BrightRed),
-			(new Color (0, 128, 128), ColorName.Cyan),
-			(new Color (128, 64, 32), ColorName.Yellow)
-		};
-
-		foreach (var testCase in testCases) {
-			var inputColor = testCase.Item1;
-			var expectedColorName = testCase.Item2;
-
-			var actualColorName = Color.FindClosestColor (inputColor);
-
-			Assert.Equal (expectedColorName, actualColorName);
-		}
+		TheoryData<Color, ColorName> data = [];
+		data.Add (new Color (0, 0), ColorName.Black);
+		data.Add (new Color (255, 255, 255), ColorName.White);
+		data.Add (new Color (5, 100, 255), ColorName.BrightBlue);
+		data.Add (new Color (0, 255), ColorName.BrightGreen);
+		data.Add (new Color (255, 70, 8), ColorName.BrightRed);
+		data.Add (new Color (0, 128, 128), ColorName.Cyan);
+		data.Add (new Color (128, 64, 32), ColorName.Yellow);
+		return data;
 	}
-}
+}

+ 7 - 0
UnitTests/UnitTests.csproj

@@ -26,6 +26,7 @@
     <PackageReference Include="System.Collections" Version="4.3.0" />
     <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="20.0.4" />
     <PackageReference Include="xunit" Version="2.6.6" />
+    <PackageReference Include="Xunit.Combinatorial" Version="1.6.24" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -44,6 +45,10 @@
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </None>
   </ItemGroup>
+  <ItemGroup>
+    <Using Include="Terminal.Gui" />
+    <Using Include="Xunit" />
+  </ItemGroup>
   <PropertyGroup Label="FineCodeCoverage">
     <Enabled>
       False
@@ -62,5 +67,7 @@
     <IncludeTestAssembly>
       False
     </IncludeTestAssembly>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <ImplicitUsings>enable</ImplicitUsings>
   </PropertyGroup>
 </Project>

+ 4 - 4
UnitTests/View/Adornment/BorderTests.cs

@@ -687,8 +687,8 @@ public class BorderTests {
 			Normal = new Attribute (Color.Red, Color.Green),
 			Focus = new Attribute (Color.Green, Color.Red),
 		};
-		Assert.Equal (ColorName.Red, view.Border.GetNormalColor ().Foreground.ColorName);
-		Assert.Equal (ColorName.Green, view.Border.GetFocusColor ().Foreground.ColorName);
+		Assert.Equal (ColorName.Red, view.Border.GetNormalColor ().Foreground.GetClosestNamedColor ());
+		Assert.Equal (ColorName.Green, view.Border.GetFocusColor ().Foreground.GetClosestNamedColor ());
 		Assert.Equal (view.GetNormalColor(), view.Border.GetNormalColor ());
 		Assert.Equal (view.GetFocusColor (), view.Border.GetFocusColor ());
 
@@ -716,8 +716,8 @@ public class BorderTests {
 			Focus = new Attribute (Color.Green, Color.Red),
 		};
 		Assert.NotEqual (view.ColorScheme.Normal.Foreground, view.ColorScheme.Focus.Foreground);
-		Assert.Equal (ColorName.Red, view.Border.GetNormalColor ().Foreground.ColorName);
-		Assert.Equal (ColorName.Green, view.Border.GetFocusColor ().Foreground.ColorName);
+		Assert.Equal (ColorName.Red, view.Border.GetNormalColor ().Foreground.GetClosestNamedColor ());
+		Assert.Equal (ColorName.Green, view.Border.GetFocusColor ().Foreground.GetClosestNamedColor ());
 		Assert.Equal (view.GetFocusColor (), view.Border.GetFocusColor ());
 
 		view.BeginInit ();

+ 4 - 5
UnitTests/View/Adornment/MarginTests.cs

@@ -1,5 +1,4 @@
-using Xunit;
-using Xunit.Abstractions;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.ViewTests;
 public class MarginTests {
@@ -21,15 +20,15 @@ public class MarginTests {
 		view.Margin.Thickness = new Thickness (1);
 
 		var superView = new View ();
-		
+
 		superView.ColorScheme = new ColorScheme () {
 			Normal = new Attribute (Color.Red, Color.Green),
 			Focus = new Attribute (Color.Green, Color.Red),
 		};
 
 		superView.Add (view);
-		Assert.Equal (ColorName.Red, view.Margin.GetNormalColor ().Foreground.ColorName);
-		Assert.Equal (ColorName.Red, superView.GetNormalColor ().Foreground.ColorName);
+		Assert.Equal (ColorName.Red, view.Margin.GetNormalColor ().Foreground.GetClosestNamedColor ());
+		Assert.Equal (ColorName.Red, superView.GetNormalColor ().Foreground.GetClosestNamedColor ());
 		Assert.Equal (superView.GetNormalColor (), view.Margin.GetNormalColor ());
 		Assert.Equal (superView.GetFocusColor (), view.Margin.GetFocusColor ());
 

+ 2 - 2
UnitTests/View/Adornment/PaddingTests.cs

@@ -24,8 +24,8 @@ public class PaddingTests {
 			Normal = new Attribute (Color.Red, Color.Green),
 			Focus = new Attribute (Color.Green, Color.Red),
 		};
-		
-		Assert.Equal (ColorName.Red, view.Padding.GetNormalColor ().Foreground.ColorName);
+
+		Assert.Equal (ColorName.Red, view.Padding.GetNormalColor ().Foreground.GetClosestNamedColor ());
 		Assert.Equal (view.GetNormalColor (), view.Padding.GetNormalColor ());
 
 		view.BeginInit ();

+ 74 - 0
UnitTests/Views/DateFieldTests.cs

@@ -170,4 +170,78 @@ public class DateFieldTests {
 		Assert.Equal (4, df.CursorPosition);
 		CultureInfo.CurrentCulture = cultureBackup;
 	}
+
+	[Fact]
+	public void Using_All_Culture_StandardizeDateFormat ()
+	{
+		CultureInfo cultureBackup = CultureInfo.CurrentCulture;
+
+		DateTime date = DateTime.Parse ("1/1/1971");
+		foreach (var culture in CultureInfo.GetCultures (CultureTypes.AllCultures)) {
+			CultureInfo.CurrentCulture = culture;
+			var separator = culture.DateTimeFormat.DateSeparator.Trim ();
+			if (separator.Length > 1 && separator.Contains ('\u200f')) {
+				separator = separator.Replace ("\u200f", "");
+			}
+			var format = culture.DateTimeFormat.ShortDatePattern;
+			DateField df = new DateField (date);
+			if ((!culture.TextInfo.IsRightToLeft || (culture.TextInfo.IsRightToLeft && !df.Text.Contains ('\u200f')))
+				&& (format.StartsWith ('d') || format.StartsWith ('M'))) {
+
+				switch (culture.Name) {
+				case "ar-SA":
+					Assert.Equal ($" 04{separator}11{separator}1390", df.Text);
+					break;
+				case "th":
+				case "th-TH":
+					Assert.Equal ($" 01{separator}01{separator}2514", df.Text);
+					break;
+				default:
+					Assert.Equal ($" 01{separator}01{separator}1971", df.Text);
+					break;
+				}
+			} else if (culture.TextInfo.IsRightToLeft) {
+				if (df.Text.Contains ('\u200f')) {
+					// It's a Unicode Character (U+200F) - Right-to-Left Mark (RLM)
+					Assert.True (df.Text.Contains ('\u200f'));
+					switch (culture.Name) {
+					case "ar-SA":
+						Assert.Equal ($" 04‏{separator}11‏{separator}1390", df.Text);
+						break;
+					default:
+						Assert.Equal ($" 01‏{separator}01‏{separator}1971", df.Text);
+						break;
+					}
+				} else {
+					switch (culture.Name) {
+					case "ckb-IR":
+					case "fa":
+					case "fa-AF":
+					case "fa-IR":
+					case "lrc":
+					case "lrc-IR":
+					case "mzn":
+					case "mzn-IR":
+					case "ps":
+					case "ps-AF":
+					case "uz-Arab":
+					case "uz-Arab-AF":
+						Assert.Equal ($" 1349{separator}10{separator}11", df.Text);
+						break;
+					default:
+						Assert.Equal ($" 1971{separator}01{separator}01", df.Text);
+						break;
+					}
+				}
+			} else {
+				switch (culture.Name) {
+				default:
+					Assert.Equal ($" 1971{separator}01{separator}01", df.Text);
+					break;
+				}
+			}
+		}
+
+		CultureInfo.CurrentCulture = cultureBackup;
+	}
 }

+ 60 - 18
UnitTests/Views/DatePickerTests.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Globalization;
-using Terminal.Gui;
 using Xunit;
 
 namespace Terminal.Gui.ViewsTests;
@@ -8,26 +7,38 @@ namespace Terminal.Gui.ViewsTests;
 public class DatePickerTests {
 
 	[Fact]
-	public void DatePicker_SetFormat_ShouldChangeFormat ()
+	public void DatePicker_ChangingCultureChangesFormat ()
 	{
-		var datePicker = new DatePicker {
-			Format = "dd/MM/yyyy"
-		};
-		Assert.Equal ("dd/MM/yyyy", datePicker.Format);
+		var date = new DateTime (2000, 7, 23);
+		var datePicker = new DatePicker (date);
+
+		datePicker.Culture = CultureInfo.GetCultureInfo ("en-GB");
+		Assert.Equal ("23/07/2000", datePicker.Text);
+
+		datePicker.Culture = CultureInfo.GetCultureInfo ("pl-PL");
+		Assert.Equal ("23.07.2000", datePicker.Text);
+
+		// Deafult date format for en-US is M/d/yyyy but we are using StandardizeDateFormat method
+		// to convert it to the format that has 2 digits for month and day.
+		datePicker.Culture = CultureInfo.GetCultureInfo ("en-US");
+		Assert.Equal ("07/23/2000", datePicker.Text);
 	}
 
 	[Fact]
 	public void DatePicker_Initialize_ShouldSetCurrentDate ()
 	{
 		var datePicker = new DatePicker ();
-		var format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
-		Assert.Equal (DateTime.Now.ToString (format), datePicker.Text);
+		Assert.Equal (DateTime.Now.Date.Day, datePicker.Date.Day);
+		Assert.Equal (DateTime.Now.Date.Month, datePicker.Date.Month);
+		Assert.Equal (DateTime.Now.Date.Year, datePicker.Date.Year);
 	}
 
 	[Fact]
 	public void DatePicker_SetDate_ShouldChangeText ()
 	{
-		var datePicker = new DatePicker ();
+		var datePicker = new DatePicker () {
+			Culture = CultureInfo.GetCultureInfo ("en-GB")
+		};
 		var newDate = new DateTime (2024, 1, 15);
 		var format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
 
@@ -36,19 +47,50 @@ public class DatePickerTests {
 	}
 
 	[Fact]
-	public void DatePicker_ShowDatePickerDialog_ShouldChangeDate ()
+	[AutoInitShutdown]
+	public void DatePicker_ShouldNot_SetDateOutOfRange_UsingPreviousMonthButton ()
 	{
-		var datePicker = new DatePicker ();
-		var format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
-		var originalDate = datePicker.Date;
+		var date = new DateTime (1, 2, 15);
+		var datePicker = new DatePicker (date);
 
-		datePicker.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Clicked, X = 4, Y = 1 });
+		// Move focus to previous month button
+		Application.Top.Add (datePicker);
+		Application.Begin (Application.Top);
 
-		var newDate = new DateTime (2024, 2, 20);
-		datePicker.Date = newDate;
+		// set focus to the previous month button
+		datePicker.FocusNext ();
+		datePicker.FocusNext ();
 
-		Assert.Equal (newDate.ToString (format), datePicker.Text);
+		// Change month to January 
+		Assert.True (datePicker.NewKeyDownEvent (new (KeyCode.Enter)));
+		Assert.Equal (1, datePicker.Date.Month);
+
+		// Date should not change as previous month button is disabled
+		Assert.False (datePicker.NewKeyDownEvent (new (KeyCode.Enter)));
+		Assert.Equal (1, datePicker.Date.Month);
+	}
+
+	[Fact]
+	[AutoInitShutdown]
+	public void DatePicker_ShouldNot_SetDateOutOfRange_UsingNextMonthButton ()
+	{
+		var date = new DateTime (9999, 11, 15);
+		var datePicker = new DatePicker (date);
+
+		Application.Top.Add (datePicker);
+		Application.Begin (Application.Top);
+
+		// Set focus to next month button
+		datePicker.FocusNext ();
+		datePicker.FocusNext ();
+		datePicker.FocusNext ();
+
+		// Change month to December
+		Assert.True (datePicker.NewKeyDownEvent (new (KeyCode.Enter)));
+		Assert.Equal (12, datePicker.Date.Month);
 
-		datePicker.Date = originalDate;
+		// Date should not change as next month button is disabled
+		Assert.False (datePicker.NewKeyDownEvent (new (KeyCode.Enter)));
+		Assert.Equal (12, datePicker.Date.Month);
 	}
 }

+ 1 - 1
UnitTests/Views/RuneCellTests.cs

@@ -65,7 +65,7 @@ namespace Terminal.Gui.ViewsTests {
 				ColorScheme = new ColorScheme () { Normal = new Attribute (Color.Red) }
 			};
 			Assert.Equal ("U+0000 '\0'; null", rc1.ToString ());
-			Assert.Equal ("U+0061 'a'; Normal: Red,Red; Focus: White,Black; HotNormal: White,Black; HotFocus: White,Black; Disabled: White,Black", rc2.ToString ());
+			Assert.Equal ("U+0061 'a'; Normal: [Red,Red]; Focus: [White,Black]; HotNormal: [White,Black]; HotFocus: [White,Black]; Disabled: [White,Black]", rc2.ToString ());
 		}
 
 

+ 1 - 1
UnitTests/Views/TimeFieldTests.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using Xunit;
 
 namespace Terminal.Gui.ViewsTests;

+ 4 - 2
docfx/docfx.json

@@ -9,7 +9,8 @@
           ]
         }
       ],
-      "dest": "api"
+      "dest": "api",
+      "memberLayout": "separatePages"
     },
     {
       "src": [
@@ -20,7 +21,8 @@
           ]
         }
       ],
-      "dest": "api/UICatalog"
+      "dest": "api/UICatalog",
+      "memberLayout": "separatePages"
     }
   ],
   "build": {