From fe30bd0d9bad7c4d8dad8be370aa58e315b811d9 Mon Sep 17 00:00:00 2001
From: Tonttu <15074459+TheTonttu@users.noreply.github.com>
Date: Wed, 19 Mar 2025 22:48:20 +0200
Subject: [PATCH 01/16] Add W3C color enum with the RGB as numeric value
---
Terminal.Gui/Drawing/Color/W3cColor.cs | 777 +++++++++++++++++++++++++
1 file changed, 777 insertions(+)
create mode 100644 Terminal.Gui/Drawing/Color/W3cColor.cs
diff --git a/Terminal.Gui/Drawing/Color/W3cColor.cs b/Terminal.Gui/Drawing/Color/W3cColor.cs
new file mode 100644
index 0000000000..a2cc07dabd
--- /dev/null
+++ b/Terminal.Gui/Drawing/Color/W3cColor.cs
@@ -0,0 +1,777 @@
+namespace Terminal.Gui;
+
+///
+/// Represents the W3C color names with their RGB values.
+///
+///
+/// Based on https://www.w3schools.com/colors/color_tryit.asp page.
+///
+public enum W3cColor
+{
+ ///
+ /// Alice blue RGB(240, 248, 255).
+ ///
+ AliceBlue = 0xF0F8FF,
+
+ ///
+ /// Antique white RGB(250, 235, 215).
+ ///
+ AntiqueWhite = 0xFAEBD7,
+
+ ///
+ /// Aqua RGB(0, 255, 255).
+ ///
+ Aqua = 0x00FFFF,
+
+ ///
+ /// Aquamarine RGB(127, 255, 212).
+ ///
+ Aquamarine = 0x7FFFD4,
+
+ ///
+ /// Azure RGB(240, 255, 255).
+ ///
+ Azure = 0xF0FFFF,
+
+ ///
+ /// Beige RGB(245, 245, 220).
+ ///
+ Beige = 0xF5F5DC,
+
+ ///
+ /// Bisque RGB(255, 228, 196).
+ ///
+ Bisque = 0xFFE4C4,
+
+ ///
+ /// Black RGB(0, 0, 0).
+ ///
+ Black = 0x000000,
+
+ ///
+ /// Blanched almond RGB(255, 235, 205).
+ ///
+ BlanchedAlmond = 0xFFEBCD,
+
+ ///
+ /// Blue RGB(0, 0, 255).
+ ///
+ Blue = 0x0000FF,
+
+ ///
+ /// Blue violet RGB(138, 43, 226).
+ ///
+ BlueViolet = 0x8A2BE2,
+
+ ///
+ /// Brown RGB(165, 42, 42).
+ ///
+ Brown = 0xA52A2A,
+
+ ///
+ /// Burly wood RGB(222, 184, 135).
+ ///
+ BurlyWood = 0xDEB887,
+
+ ///
+ /// Cadet blue RGB(95, 158, 160).
+ ///
+ CadetBlue = 0x5F9EA0,
+
+ ///
+ /// Chartreuse RGB(127, 255, 0).
+ ///
+ Chartreuse = 0x7FFF00,
+
+ ///
+ /// Chocolate RGB(210, 105, 30).
+ ///
+ Chocolate = 0xD2691E,
+
+ ///
+ /// Coral RGB(255, 127, 80).
+ ///
+ Coral = 0xFF7F50,
+
+ ///
+ /// Cornflower blue RGB(100, 149, 237).
+ ///
+ CornflowerBlue = 0x6495ED,
+
+ ///
+ /// Cornsilk RGB(255, 248, 220).
+ ///
+ Cornsilk = 0xFFF8DC,
+
+ ///
+ /// Crimson RGB(220, 20, 60).
+ ///
+ Crimson = 0xDC143C,
+
+ ///
+ /// Cyan RGB(0, 255, 255).
+ ///
+ ///
+ /// Same as .
+ ///
+ Cyan = Aqua,
+
+ ///
+ /// Dark blue RGB(0, 0, 139).
+ ///
+ DarkBlue = 0x00008B,
+
+ ///
+ /// Dark cyan RGB(0, 139, 139).
+ ///
+ DarkCyan = 0x008B8B,
+
+ ///
+ /// Dark goldenrod RGB(184, 134, 11).
+ ///
+ DarkGoldenrod = 0xB8860B,
+
+ ///
+ /// Dark gray RGB(169, 169, 169).
+ ///
+ DarkGray = 0xA9A9A9,
+
+ ///
+ /// Dark green RGB(0, 100, 0).
+ ///
+ DarkGreen = 0x006400,
+
+ ///
+ /// Dark grey RGB(169, 169, 169).
+ ///
+ ///
+ /// Same as .
+ ///
+ DarkGrey = DarkGray,
+
+ ///
+ /// Dark khaki RGB(189, 183, 107).
+ ///
+ DarkKhaki = 0xBDB76B,
+
+ ///
+ /// Dark magenta RGB(139, 0, 139).
+ ///
+ DarkMagenta = 0x8B008B,
+
+ ///
+ /// Dark olive green RGB(85, 107, 47).
+ ///
+ DarkOliveGreen = 0x556B2F,
+
+ ///
+ /// Dark orange RGB(255, 140, 0).
+ ///
+ DarkOrange = 0xFF8C00,
+
+ ///
+ /// Dark orchid RGB(153, 50, 204).
+ ///
+ DarkOrchid = 0x9932CC,
+
+ ///
+ /// Dark red RGB(139, 0, 0).
+ ///
+ DarkRed = 0x8B0000,
+
+ ///
+ /// Dark salmon RGB(233, 150, 122).
+ ///
+ DarkSalmon = 0xE9967A,
+
+ ///
+ /// Dark sea green RGB(143, 188, 143).
+ ///
+ DarkSeaGreen = 0x8FBC8F,
+
+ ///
+ /// Dark slate blue RGB(72, 61, 139).
+ ///
+ DarkSlateBlue = 0x483D8B,
+
+ ///
+ /// Dark slate gray RGB(47, 79, 79).
+ ///
+ DarkSlateGray = 0x2F4F4F,
+
+ ///
+ /// Dark slate grey RGB(47, 79, 79).
+ ///
+ ///
+ /// Same as .
+ ///
+ DarkSlateGrey = DarkSlateGray,
+
+ ///
+ /// Dark turquoise RGB(0, 206, 209).
+ ///
+ DarkTurquoise = 0x00CED1,
+
+ ///
+ /// Dark violet RGB(148, 0, 211).
+ ///
+ DarkViolet = 0x9400D3,
+
+ ///
+ /// Deep pink RGB(255, 20, 147).
+ ///
+ DeepPink = 0xFF1493,
+
+ ///
+ /// Deep sky blue RGB(0, 191, 255).
+ ///
+ DeepSkyBlue = 0x00BFFF,
+
+ ///
+ /// Dim gray RGB(105, 105, 105).
+ ///
+ DimGray = 0x696969,
+
+ ///
+ /// Dim grey RGB(105, 105, 105).
+ ///
+ ///
+ /// Same as .
+ ///
+ DimGrey = DimGray,
+
+ ///
+ /// Dodger blue RGB(30, 144, 255).
+ ///
+ DodgerBlue = 0x1E90FF,
+
+ ///
+ /// Fire brick RGB(178, 34, 34).
+ ///
+ FireBrick = 0xB22222,
+
+ ///
+ /// Floral white RGB(255, 250, 240).
+ ///
+ FloralWhite = 0xFFFAF0,
+
+ ///
+ /// Forest green RGB(34, 139, 34).
+ ///
+ ForestGreen = 0x228B22,
+
+ ///
+ /// Fuchsia RGB(255, 0, 255).
+ ///
+ ///
+ /// Same as .
+ ///
+ Fuchsia = Magenta,
+
+ ///
+ /// Gainsboro RGB(220, 220, 220).
+ ///
+ Gainsboro = 0xDCDCDC,
+
+ ///
+ /// Ghost white RGB(248, 248, 255).
+ ///
+ GhostWhite = 0xF8F8FF,
+
+ ///
+ /// Gold RGB(255, 215, 0).
+ ///
+ Gold = 0xFFD700,
+
+ ///
+ /// Goldenrod RGB(218, 165, 32).
+ ///
+ Goldenrod = 0xDAA520,
+
+ ///
+ /// Gray RGB(128, 128, 128).
+ ///
+ Gray = 0x808080,
+
+ ///
+ /// Green RGB(0, 128, 0).
+ ///
+ Green = 0x008000,
+
+ ///
+ /// Green yellow RGB(173, 255, 47).
+ ///
+ GreenYellow = 0xADFF2F,
+
+ ///
+ /// Grey RGB(128, 128, 128).
+ ///
+ ///
+ /// Same as .
+ ///
+ Grey = Gray,
+
+ ///
+ /// Honey dew RGB(240, 255, 240).
+ ///
+ HoneyDew = 0xF0FFF0,
+
+ ///
+ /// Hot pink RGB(255, 105, 180).
+ ///
+ HotPink = 0xFF69B4,
+
+ ///
+ /// Indian red RGB(205, 92, 92).
+ ///
+ IndianRed = 0xCD5C5C,
+
+ ///
+ /// Indigo RGB(75, 0, 130).
+ ///
+ Indigo = 0x4B0082,
+
+ ///
+ /// Ivory RGB(255, 255, 240).
+ ///
+ Ivory = 0xFFFFF0,
+
+ ///
+ /// Khaki RGB(240, 230, 140).
+ ///
+ Khaki = 0xF0E68C,
+
+ ///
+ /// Lavender RGB(230, 230, 250).
+ ///
+ Lavender = 0xE6E6FA,
+
+ ///
+ /// Lavender blush RGB(255, 240, 245).
+ ///
+ LavenderBlush = 0xFFF0F5,
+
+ ///
+ /// Lawn green RGB(124, 252, 0).
+ ///
+ LawnGreen = 0x7CFC00,
+
+ ///
+ /// Lemon chiffon RGB(255, 250, 205).
+ ///
+ LemonChiffon = 0xFFFACD,
+
+ ///
+ /// Light blue RGB(173, 216, 230).
+ ///
+ LightBlue = 0xADD8E6,
+
+ ///
+ /// Light coral RGB(240, 128, 128).
+ ///
+ LightCoral = 0xF08080,
+
+ ///
+ /// Light cyan RGB(224, 255, 255).
+ ///
+ LightCyan = 0xE0FFFF,
+
+ ///
+ /// Light goldenrod yellow RGB(250, 250, 210).
+ ///
+ LightGoldenrodYellow = 0xFAFAD2,
+
+ ///
+ /// Light gray RGB(211, 211, 211).
+ ///
+ LightGray = 0xD3D3D3,
+
+ ///
+ /// Light green RGB(144, 238, 144).
+ ///
+ LightGreen = 0x90EE90,
+
+ ///
+ /// Light grey RGB(211, 211, 211).
+ ///
+ ///
+ /// Same as .
+ ///
+ LightGrey = LightGray,
+
+ ///
+ /// Light pink RGB(255, 182, 193).
+ ///
+ LightPink = 0xFFB6C1,
+
+ ///
+ /// Light salmon RGB(255, 160, 122).
+ ///
+ LightSalmon = 0xFFA07A,
+
+ ///
+ /// Light sea green RGB(32, 178, 170).
+ ///
+ LightSeaGreen = 0x20B2AA,
+
+ ///
+ /// Light sky blue RGB(135, 206, 250).
+ ///
+ LightSkyBlue = 0x87CEFA,
+
+ ///
+ /// Light slate gray RGB(119, 136, 153).
+ ///
+ LightSlateGray = 0x778899,
+
+ ///
+ /// Light slate grey RGB(119, 136, 153).
+ ///
+ ///
+ /// Same as .
+ ///
+ LightSlateGrey = LightSlateGray,
+
+ ///
+ /// Light steel blue RGB(176, 196, 222).
+ ///
+ LightSteelBlue = 0xB0C4DE,
+
+ ///
+ /// Light yellow RGB(255, 255, 224).
+ ///
+ LightYellow = 0xFFFFE0,
+
+ ///
+ /// Lime RGB(0, 255, 0).
+ ///
+ Lime = 0x00FF00,
+
+ ///
+ /// Lime green RGB(50, 205, 50).
+ ///
+ LimeGreen = 0x32CD32,
+
+ ///
+ /// Linen RGB(250, 240, 230).
+ ///
+ Linen = 0xFAF0E6,
+
+ ///
+ /// Magenta RGB(255, 0, 255).
+ ///
+ Magenta = 0xFF00FF,
+
+ ///
+ /// Maroon RGB(128, 0, 0).
+ ///
+ Maroon = 0x800000,
+
+ ///
+ /// Medium aqua marine RGB(102, 205, 170).
+ ///
+ MediumAquaMarine = 0x66CDAA,
+
+ ///
+ /// Medium blue RGB(0, 0, 205).
+ ///
+ MediumBlue = 0x0000CD,
+
+ ///
+ /// Medium orchid RGB(186, 85, 211).
+ ///
+ MediumOrchid = 0xBA55D3,
+
+ ///
+ /// Medium purple RGB(147, 112, 219).
+ ///
+ MediumPurple = 0x9370DB,
+
+ ///
+ /// Medium sea green RGB(60, 179, 113).
+ ///
+ MediumSeaGreen = 0x3CB371,
+
+ ///
+ /// Medium slate blue RGB(123, 104, 238).
+ ///
+ MediumSlateBlue = 0x7B68EE,
+
+ ///
+ /// Medium spring green RGB(0, 250, 154).
+ ///
+ MediumSpringGreen = 0x00FA9A,
+
+ ///
+ /// Medium turquoise RGB(72, 209, 204).
+ ///
+ MediumTurquoise = 0x48D1CC,
+
+ ///
+ /// Medium violet red RGB(199, 21, 133).
+ ///
+ MediumVioletRed = 0xC71585,
+
+ ///
+ /// Midnight blue RGB(25, 25, 112).
+ ///
+ MidnightBlue = 0x191970,
+
+ ///
+ /// Mint cream RGB(245, 255, 250).
+ ///
+ MintCream = 0xF5FFFA,
+
+ ///
+ /// Misty rose RGB(255, 228, 225).
+ ///
+ MistyRose = 0xFFE4E1,
+
+ ///
+ /// Moccasin RGB(255, 228, 181).
+ ///
+ Moccasin = 0xFFE4B5,
+
+ ///
+ /// Navajo white RGB(255, 222, 173).
+ ///
+ NavajoWhite = 0xFFDEAD,
+
+ ///
+ /// Navy RGB(0, 0, 128).
+ ///
+ Navy = 0x000080,
+
+ ///
+ /// Old lace RGB(253, 245, 230).
+ ///
+ OldLace = 0xFDF5E6,
+
+ ///
+ /// Olive RGB(128, 128, 0).
+ ///
+ Olive = 0x808000,
+
+ ///
+ /// Olive drab RGB(107, 142, 35).
+ ///
+ OliveDrab = 0x6B8E23,
+
+ ///
+ /// Orange RGB(255, 165, 0).
+ ///
+ Orange = 0xFFA500,
+
+ ///
+ /// Orange red RGB(255, 69, 0).
+ ///
+ OrangeRed = 0xFF4500,
+
+ ///
+ /// Orchid RGB(218, 112, 214).
+ ///
+ Orchid = 0xDA70D6,
+
+ ///
+ /// Pale goldenrod RGB(238, 232, 170).
+ ///
+ PaleGoldenrod = 0xEEE8AA,
+
+ ///
+ /// Pale green RGB(152, 251, 152).
+ ///
+ PaleGreen = 0x98FB98,
+
+ ///
+ /// Pale turquoise RGB(175, 238, 238).
+ ///
+ PaleTurquoise = 0xAFEEEE,
+
+ ///
+ /// Pale violet red RGB(219, 112, 147).
+ ///
+ PaleVioletRed = 0xDB7093,
+
+ ///
+ /// Papaya whip RGB(255, 239, 213).
+ ///
+ PapayaWhip = 0xFFEFD5,
+
+ ///
+ /// Peach puff RGB(255, 218, 185).
+ ///
+ PeachPuff = 0xFFDAB9,
+
+ ///
+ /// Peru RGB(205, 133, 63).
+ ///
+ Peru = 0xCD853F,
+
+ ///
+ /// Pink RGB(255, 192, 203).
+ ///
+ Pink = 0xFFC0CB,
+
+ ///
+ /// Plum RGB(221, 160, 221).
+ ///
+ Plum = 0xDDA0DD,
+
+ ///
+ /// Powder blue RGB(176, 224, 230).
+ ///
+ PowderBlue = 0xB0E0E6,
+
+ ///
+ /// Purple RGB(128, 0, 128).
+ ///
+ Purple = 0x800080,
+
+ ///
+ /// Rebecca purple RGB(102, 51, 153).
+ ///
+ RebeccaPurple = 0x663399,
+
+ ///
+ /// Red RGB(255, 0, 0).
+ ///
+ Red = 0xFF0000,
+
+ ///
+ /// Rosy brown RGB(188, 143, 143).
+ ///
+ RosyBrown = 0xBC8F8F,
+
+ ///
+ /// Royal blue RGB(65, 105, 225).
+ ///
+ RoyalBlue = 0x4169E1,
+
+ ///
+ /// Saddle brown RGB(139, 69, 19).
+ ///
+ SaddleBrown = 0x8B4513,
+
+ ///
+ /// Salmon RGB(250, 128, 114).
+ ///
+ Salmon = 0xFA8072,
+
+ ///
+ /// Sandy brown RGB(244, 164, 96).
+ ///
+ SandyBrown = 0xF4A460,
+
+ ///
+ /// Sea green RGB(46, 139, 87).
+ ///
+ SeaGreen = 0x2E8B57,
+
+ ///
+ /// Sea shell RGB(255, 245, 238).
+ ///
+ SeaShell = 0xFFF5EE,
+
+ ///
+ /// Sienna RGB(160, 82, 45).
+ ///
+ Sienna = 0xA0522D,
+
+ ///
+ /// Silver RGB(192, 192, 192).
+ ///
+ Silver = 0xC0C0C0,
+
+ ///
+ /// Sky blue RGB(135, 206, 235).
+ ///
+ SkyBlue = 0x87CEEB,
+
+ ///
+ /// Slate blue RGB(106, 90, 205).
+ ///
+ SlateBlue = 0x6A5ACD,
+
+ ///
+ /// Slate gray RGB(112, 128, 144).
+ ///
+ SlateGray = 0x708090,
+
+ ///
+ /// Slate grey RGB(112, 128, 144).
+ ///
+ ///
+ /// Same as .
+ ///
+ SlateGrey = SlateGray,
+
+ ///
+ /// Snow RGB(255, 250, 250).
+ ///
+ Snow = 0xFFFAFA,
+
+ ///
+ /// Spring green RGB(0, 255, 127).
+ ///
+ SpringGreen = 0x00FF7F,
+
+ ///
+ /// Steel blue RGB(70, 130, 180).
+ ///
+ SteelBlue = 0x4682B4,
+
+ ///
+ /// Tan RGB(210, 180, 140).
+ ///
+ Tan = 0xD2B48C,
+
+ ///
+ /// Teal RGB(0, 128, 128).
+ ///
+ Teal = 0x008080,
+
+ ///
+ /// Thistle RGB(216, 191, 216).
+ ///
+ Thistle = 0xD8BFD8,
+
+ ///
+ /// Tomato RGB(255, 99, 71).
+ ///
+ Tomato = 0xFF6347,
+
+ ///
+ /// Turquoise RGB(64, 224, 208).
+ ///
+ Turquoise = 0x40E0D0,
+
+ ///
+ /// Violet RGB(238, 130, 238).
+ ///
+ Violet = 0xEE82EE,
+
+ ///
+ /// Wheat RGB(245, 222, 179).
+ ///
+ Wheat = 0xF5DEB3,
+
+ ///
+ /// White RGB(255, 255, 255).
+ ///
+ White = 0xFFFFFF,
+
+ ///
+ /// White smoke RGB(245, 245, 245).
+ ///
+ WhiteSmoke = 0xF5F5F5,
+
+ ///
+ /// Yellow RGB(255, 255, 0).
+ ///
+ Yellow = 0xFFFF00,
+
+ ///
+ /// Yellow green RGB(154, 205, 50).
+ ///
+ YellowGreen = 0x9ACD32
+}
From 6d64a9f96f87ba4e2bc990eec8bf43463dbcae68 Mon Sep 17 00:00:00 2001
From: Tonttu <15074459+TheTonttu@users.noreply.github.com>
Date: Sat, 22 Mar 2025 09:51:08 +0200
Subject: [PATCH 02/16] Add transform helper class for W3cColor enum
For the sake of backwards compatibility prioritize parsing 16 color mode color names over the W3C colors because the previous resource-based color names/values had a mix of W3C and 16 color mode RGB values.
Mechanism for choosing/prioritizing one color scheme over the other is currently only available at higher application/driver/output level.
---
.../Configuration/ColorJsonConverter.cs | 12 +-
.../Drawing/Color/Color.Formatting.cs | 155 +++++++++---------
Terminal.Gui/Drawing/Color/Color.cs | 10 +-
Terminal.Gui/Drawing/Color/ColorStrings.cs | 98 +++++++----
Terminal.Gui/Drawing/Color/W3CColors.cs | 105 +++++++++++-
.../Drawing/Color/W3cColorsTests.cs | 86 ++++++++++
6 files changed, 351 insertions(+), 115 deletions(-)
create mode 100644 Tests/UnitTestsParallelizable/Drawing/Color/W3cColorsTests.cs
diff --git a/Terminal.Gui/Configuration/ColorJsonConverter.cs b/Terminal.Gui/Configuration/ColorJsonConverter.cs
index 6da4490d91..388d1fc673 100644
--- a/Terminal.Gui/Configuration/ColorJsonConverter.cs
+++ b/Terminal.Gui/Configuration/ColorJsonConverter.cs
@@ -1,6 +1,5 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-using ColorHelper;
namespace Terminal.Gui;
@@ -40,11 +39,18 @@ public override Color Read (ref Utf8JsonReader reader, Type typeToConvert, JsonS
// Get the color string
ReadOnlySpan colorString = reader.GetString ();
+ // TODO: Mechanism to choose/prioritize between ANSI and W3C colors.
+ // Backwards compatibility: Color 16 RGB values were previously used as main colors.
+ if (ColorStrings.TryParseColor16(colorString, out Color color16))
+ {
+ return new (color16);
+ }
+
// Check if the color string is a color name
- if (ColorStrings.TryParseW3CColorName (colorString.ToString (), out Color color1))
+ if (ColorStrings.TryParseW3CColorName (colorString, out Color w3cColor))
{
// Return the parsed color
- return new (color1);
+ return new (w3cColor);
}
if (Color.TryParse (colorString, null, out Color parsedColor))
diff --git a/Terminal.Gui/Drawing/Color/Color.Formatting.cs b/Terminal.Gui/Drawing/Color/Color.Formatting.cs
index dd775fe049..5dbd378bc0 100644
--- a/Terminal.Gui/Drawing/Color/Color.Formatting.cs
+++ b/Terminal.Gui/Drawing/Color/Color.Formatting.cs
@@ -267,90 +267,82 @@ public static Color Parse (ReadOnlySpan text, IFormatProvider? formatProvi
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
- ),
+ { 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
- ),
+ { 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 < 3 } => 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 ColorStrings resources
- { } when char.IsLetter (text [0]) && ColorStrings.TryParseW3CColorName (text.ToString (), out Color color) =>
- new Color (color),
+ { Length: > 0 and < 3 } =>
+ 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 named colors
+ // TODO: Mechanism to choose/prioritize between ANSI and W3C colors.
+ // Backwards compatibility: Color 16 RGB values were previously used as main colors.
+ { } when char.IsLetter (text [0]) && ColorStrings.TryParseColor16 (text, out Color color16) => color16,
+ { } when char.IsLetter (text [0]) && ColorStrings.TryParseW3CColorName (text, out Color w3cColor) => w3cColor,
// Any other input
_ => throw new ColorParseException (in text, "Text did not match any expected format.", in text, [])
};
@@ -585,11 +577,16 @@ public static bool TryParse (ReadOnlySpan utf8Text, IFormatProvider? provi
[SkipLocalsInit]
public override string ToString ()
{
- string? name = ColorStrings.GetW3CColorName (this);
+ // TODO: Mechanism to choose/prioritize between ANSI and W3C colors.
+ // Backwards compatibility: Color 16 RGB values were previously used as main colors.
+ if (ColorStrings.GetANSIColor16Name (this) is string ansiName)
+ {
+ return ansiName;
+ }
- if (name is { })
+ if (ColorStrings.GetW3CColorName (this) is string w3cName)
{
- return name;
+ return w3cName;
}
return $"#{R:X2}{G:X2}{B:X2}";
diff --git a/Terminal.Gui/Drawing/Color/Color.cs b/Terminal.Gui/Drawing/Color/Color.cs
index d81254c73a..643cb048d7 100644
--- a/Terminal.Gui/Drawing/Color/Color.cs
+++ b/Terminal.Gui/Drawing/Color/Color.cs
@@ -1,7 +1,5 @@
#nullable enable
using System.Collections.Frozen;
-using System.Diagnostics.Contracts;
-using System.Drawing;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -236,6 +234,14 @@ internal static ColorName16 GetClosestNamedColor16 (Color inputColor)
return ColorExtensions.ColorToName16Map.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
}
+ /// Gets the exact named color to this value.
+ ///
+ ///
+ internal static bool TryGetExactNamedColor16 (Color inputColor, out ColorName16 colorName16)
+ {
+ return ColorExtensions.ColorToName16Map.TryGetValue (inputColor, out colorName16);
+ }
+
[SkipLocalsInit]
private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); }
diff --git a/Terminal.Gui/Drawing/Color/ColorStrings.cs b/Terminal.Gui/Drawing/Color/ColorStrings.cs
index 79ca9f357e..0baeccbcea 100644
--- a/Terminal.Gui/Drawing/Color/ColorStrings.cs
+++ b/Terminal.Gui/Drawing/Color/ColorStrings.cs
@@ -1,8 +1,5 @@
#nullable enable
-using System.Collections;
using System.Globalization;
-using System.Resources;
-using Terminal.Gui.Resources;
namespace Terminal.Gui;
@@ -11,8 +8,6 @@ namespace Terminal.Gui;
///
public static class ColorStrings
{
- // PERFORMANCE: See https://stackoverflow.com/a/15521524/297526 for why GlobalResources.GetString is fast.
-
///
/// Gets the W3C standard string for .
///
@@ -20,28 +15,58 @@ public static class ColorStrings
/// if there is no standard color name for the specified color.
public static string? GetW3CColorName (Color color)
{
- return GlobalResources.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentUICulture);
+ if (W3cColors.TryNameColor (color, out string? name))
+ {
+ return name;
+ }
+ return null;
}
///
- /// Returns the list of W3C standard color names.
+ /// Gets the ANSI 4-bit (16) color name for .
///
- ///
- public static IEnumerable GetW3CColorNames ()
+ /// The color.
+ /// if there is no standard color name for the specified color.
+ public static string? GetANSIColor16Name (Color color)
{
- ResourceSet? resourceSet = GlobalResources.GetResourceSet (CultureInfo.CurrentUICulture, true, true);
- if (resourceSet == null)
+ if (Color.TryGetExactNamedColor16 (color, out ColorName16 color16))
{
- yield break;
+ return Color16Name (color16);
}
+ return null;
+ }
- foreach (DictionaryEntry entry in resourceSet)
+ private static string Color16Name (ColorName16 color16)
+ {
+ return color16 switch
{
- if (entry is { Value: string colorName, Key: string key } && key.StartsWith ('#'))
- {
- yield return colorName;
- }
- }
+ ColorName16.Black => nameof (ColorName16.Black),
+ ColorName16.Blue => nameof (ColorName16.Blue),
+ ColorName16.Green => nameof (ColorName16.Green),
+ ColorName16.Cyan => nameof (ColorName16.Cyan),
+ ColorName16.Red => nameof (ColorName16.Red),
+ ColorName16.Magenta => nameof (ColorName16.Magenta),
+ ColorName16.Yellow => nameof (ColorName16.Yellow),
+ ColorName16.Gray => nameof (ColorName16.Gray),
+ ColorName16.DarkGray => nameof (ColorName16.DarkGray),
+ ColorName16.BrightBlue => nameof (ColorName16.BrightBlue),
+ ColorName16.BrightGreen => nameof (ColorName16.BrightGreen),
+ ColorName16.BrightCyan => nameof (ColorName16.BrightCyan),
+ ColorName16.BrightRed => nameof (ColorName16.BrightRed),
+ ColorName16.BrightMagenta => nameof (ColorName16.BrightMagenta),
+ ColorName16.BrightYellow => nameof (ColorName16.BrightYellow),
+ ColorName16.White => nameof (ColorName16.White),
+ _ => throw new NotSupportedException ($"ColorName16 '{color16}' is not supported.")
+ };
+ }
+
+ ///
+ /// Returns the list of W3C standard color names.
+ ///
+ ///
+ public static IEnumerable GetW3CColorNames ()
+ {
+ return W3cColors.GetColorNames ();
}
///
@@ -50,25 +75,22 @@ public static IEnumerable GetW3CColorNames ()
/// The name to parse.
/// If successful, the color.
/// if was parsed successfully.
- public static bool TryParseW3CColorName (string name, out Color color)
+ public static bool TryParseW3CColorName (ReadOnlySpan name, out Color color)
{
- foreach (DictionaryEntry entry in GlobalResources.GetResourceSet (CultureInfo.CurrentUICulture, true, true)!)
+ if (W3cColors.TryParseColor (name, out color))
{
- if (entry.Value is string colorName && colorName.Equals (name, StringComparison.OrdinalIgnoreCase))
- {
- return TryParseColorKey (entry.Key.ToString (), out color);
- }
+ return true;
}
return TryParseColorKey (name, out color);
- bool TryParseColorKey (string? key, out Color color)
+ static bool TryParseColorKey (ReadOnlySpan key, out Color color)
{
- if (key != null && key.StartsWith ('#') && key.Length == 7)
+ if (!key.IsEmpty && key [0] == '#' && key.Length == 7)
{
- if (int.TryParse (key.AsSpan (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) &&
- int.TryParse (key.AsSpan (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) &&
- int.TryParse (key.AsSpan (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b))
+ if (int.TryParse (key.Slice (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) &&
+ int.TryParse (key.Slice (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) &&
+ int.TryParse (key.Slice (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b))
{
color = new Color (r, g, b);
return true;
@@ -79,4 +101,22 @@ bool TryParseColorKey (string? key, out Color color)
return false;
}
}
+
+ ///
+ /// Parses and returns if name is a ANSI 4-bit standard named color.
+ ///
+ /// The name to parse.
+ /// If successful, the color.
+ /// if was parsed successfully.
+ public static bool TryParseColor16 (ReadOnlySpan name, out Color color)
+ {
+ if (Enum.TryParse (name, ignoreCase: true, out ColorName16 color16))
+ {
+ color = new Color (color16);
+ return true;
+ }
+
+ color = default;
+ return false;
+ }
}
diff --git a/Terminal.Gui/Drawing/Color/W3CColors.cs b/Terminal.Gui/Drawing/Color/W3CColors.cs
index baa1a1101e..c594930ae7 100644
--- a/Terminal.Gui/Drawing/Color/W3CColors.cs
+++ b/Terminal.Gui/Drawing/Color/W3CColors.cs
@@ -1,12 +1,20 @@
-namespace Terminal.Gui;
+#nullable enable
+
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
///
/// Helper class that resolves w3c color names to their hex values
/// Based on https://www.w3schools.com/colors/color_tryit.asp
///
+[Obsolete ("Superseded by W3cColors")]
public class W3CColors : IColorNameResolver
{
///
+ [Obsolete ("Prefer W3cColors.GetColorNames()")]
public IEnumerable GetColorNames () { return ColorStrings.GetW3CColorNames (); }
///
@@ -15,10 +23,103 @@ public class W3CColors : IColorNameResolver
///
public bool TryNameColor (Color color, out string name)
{
- string answer = ColorStrings.GetW3CColorName (color);
+ string? answer = ColorStrings.GetW3CColorName (color);
name = answer ?? string.Empty;
return answer != null;
}
}
+
+///
+/// Helper class for transforming to and from enum.
+///
+public static class W3cColors
+{
+ private static readonly ImmutableArray Names;
+ private static readonly FrozenDictionary RgbNameMap;
+
+ static W3cColors ()
+ {
+ // Populate based on names because enums with same name are not otherwise distinguishable from each other.
+ string[] w3cNames = Enum.GetNames ().Order().ToArray();
+
+ Dictionary map = new(w3cNames.Length);
+ foreach (string name in w3cNames)
+ {
+ W3cColor w3c = Enum.Parse(name);
+ int rgb = (int)w3c;
+ // TODO: Collect aliases?
+ _ = map.TryAdd (rgb, name);
+ }
+
+ Names = ImmutableArray.Create (w3cNames);
+ RgbNameMap = map.ToFrozenDictionary ();
+ }
+
+ ///
+ /// Gets read-only list of the W3C colors in alphabetical order.
+ ///
+ public static IReadOnlyList GetColorNames ()
+ {
+ return Names;
+ }
+
+ ///
+ /// Tries to parse W3C color from the given name.
+ ///
+ ///
+ /// Contains the successfully parsed value.
+ /// True if parsed successfully; otherwise false.
+ public static bool TryParseColor (ReadOnlySpan name, out Color color)
+ {
+ if (!Enum.TryParse (name, ignoreCase: true, out W3cColor w3cColor))
+ {
+ color = default;
+ return false;
+ }
+
+ (byte red, byte green, byte blue) = GetRgbComponents (w3cColor);
+ color = new Color (red, green, blue);
+ return true;
+ }
+
+ ///
+ /// Tries to match the given color RGB value to a W3C color and returns the name.
+ ///
+ /// Color to match W3C RGB value.
+ /// Contains name of matching W3C color.
+ /// True if match; otherwise false.
+ public static bool TryNameColor (Color color, [NotNullWhen (true)] out string? name)
+ {
+ int rgb = GetRgb (color.R, color.G, color.B);
+ if (RgbNameMap.TryGetValue (rgb, out name))
+ {
+ return true;
+ }
+
+ name = null;
+ return false;
+ }
+
+ private const int RgbRedShift = 16;
+ private const int RgbGreenShift = 8;
+ private const int RgbBlueShift = 0;
+ private const int RgbRedMask = 0xFF << RgbRedShift;
+ private const int RgbGreenMask = 0xFF << RgbGreenShift;
+ private const int RgbBlueMask = 0xFF << RgbBlueShift;
+
+ private static (byte Red, byte Green, byte Blue) GetRgbComponents (W3cColor w3cColor)
+ {
+ int rgb = (int)w3cColor;
+ byte red = (byte)((rgb & RgbRedMask) >> RgbRedShift);
+ byte green = (byte)((rgb & RgbGreenMask) >> RgbGreenShift);
+ byte blue = (byte)((rgb & RgbBlueMask) >> RgbBlueShift);
+ return (red, green, blue);
+ }
+
+ private static int GetRgb (byte red, byte green, byte blue) =>
+ red << RgbRedShift |
+ green << RgbGreenShift |
+ blue << RgbBlueShift;
+}
diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/W3cColorsTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/W3cColorsTests.cs
new file mode 100644
index 0000000000..cc695012e0
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Drawing/Color/W3cColorsTests.cs
@@ -0,0 +1,86 @@
+#nullable enable
+
+namespace Terminal.Gui.DrawingTests;
+
+public class W3cColorsTests
+{
+ [Fact]
+ public void GetColorNames_NamesAreInAlphabeticalOrder ()
+ {
+ List alphabeticallyOrderedNames = Enum.GetNames ().Order ().ToList ();
+
+ Assert.Equal (alphabeticallyOrderedNames, W3cColors.GetColorNames ());
+ }
+
+ [Theory]
+ // TODO: Solve conflicts with ColorName16
+ [InlineData (nameof (W3cColor.Aqua))]
+ [InlineData (nameof (W3cColor.Cyan))]
+ [InlineData (nameof (W3cColor.DarkGray))]
+ [InlineData (nameof (W3cColor.DarkGrey))]
+ [InlineData (nameof (W3cColor.DarkSlateGray))]
+ [InlineData (nameof (W3cColor.DarkSlateGrey))]
+ [InlineData (nameof (W3cColor.DimGray))]
+ [InlineData (nameof (W3cColor.DimGrey))]
+ [InlineData (nameof (W3cColor.Fuchsia))]
+ [InlineData (nameof (W3cColor.LightGray))]
+ [InlineData (nameof (W3cColor.LightGrey))]
+ [InlineData (nameof (W3cColor.LightSlateGray))]
+ [InlineData (nameof (W3cColor.LightSlateGrey))]
+ [InlineData (nameof (W3cColor.Magenta))]
+ [InlineData (nameof (W3cColor.SlateGray))]
+ [InlineData (nameof (W3cColor.SlateGrey))]
+ public void GetColorNames_IncludesNamesWithSameValues (string name)
+ {
+ IReadOnlyList names = W3cColors.GetColorNames ();
+
+ Assert.True (names.Contains (name), $"W3C color names is missing '{name}'.");
+ }
+
+ [Theory]
+ [InlineData (240, 248, 255, true, nameof(W3cColor.AliceBlue))]
+ [InlineData (0, 255, 255, true, nameof(W3cColor.Aqua))]
+ [InlineData (255, 0, 0, true, nameof(W3cColor.Red))]
+ [InlineData (0, 128, 0, true, nameof(W3cColor.Green))]
+ [InlineData (0, 0, 255, true, nameof(W3cColor.Blue))]
+ [InlineData (0, 255, 0, true, nameof(W3cColor.Lime))]
+ [InlineData (0, 0, 0, true, nameof(W3cColor.Black))]
+ [InlineData (255, 255, 255, true, nameof(W3cColor.White))]
+ [InlineData (154, 205, 50, true, nameof(W3cColor.YellowGreen))]
+ [InlineData (1, 2, 3, false, null)]
+ public void TryNameColor_ReturnsExpectedColorName(int r, int g, int b, bool expectedSuccess, string? expectedName)
+ {
+ Color inputColor = new(r, g, b);
+ bool actualSuccess = W3cColors.TryNameColor (inputColor, out string? actualName);
+
+ Assert.Equal ((expectedSuccess, expectedName), (actualSuccess, actualName));
+ }
+
+ [Theory]
+ [InlineData ("Red", true, 255, 0, 0)]
+ [InlineData ("red", true, 255, 0, 0)]
+ [InlineData ("RED", true, 255, 0, 0)]
+ [InlineData ("Green", true, 0, 128, 0)]
+ [InlineData ("green", true, 0, 128, 0)]
+ [InlineData ("GREEN", true, 0, 128, 0)]
+ [InlineData ("Blue", true, 0, 0, 255)]
+ [InlineData ("blue", true, 0, 0, 255)]
+ [InlineData ("BLUE", true, 0, 0, 255)]
+ [InlineData ("Nada", false, 0, 0, 0)]
+ // Aliases also work
+ // TODO: Solve conflicts with ColorName16
+ [InlineData (nameof(W3cColor.Aqua), true, 0, 255, 255)]
+ [InlineData (nameof(W3cColor.Cyan), true, 0, 255, 255)]
+ [InlineData (nameof(W3cColor.DarkGray), true, 169, 169, 169)]
+ [InlineData (nameof(W3cColor.DarkGrey), true, 169, 169, 169)]
+ public void TryParseColor_ReturnsExpectedColor(string inputName, bool expectedSuccess, int r, int g, int b)
+ {
+ Color expectedColor = expectedSuccess
+ ? new (r, g, b)
+ : default;
+
+ bool actualSuccess = W3cColors.TryParseColor (inputName, out Color actualColor);
+
+ Assert.Equal ((expectedSuccess, expectedColor), (actualSuccess, actualColor));
+ }
+}
From d81495b575febf08016825ab0374c38612f27b74 Mon Sep 17 00:00:00 2001
From: Tonttu <15074459+TheTonttu@users.noreply.github.com>
Date: Sat, 22 Mar 2025 15:44:59 +0200
Subject: [PATCH 03/16] IColorNameResolver enable null analysis
---
Terminal.Gui/Drawing/Color/IColorNameResolver.cs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs
index 5af243ee4a..f5c0fd9ba0 100644
--- a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs
+++ b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs
@@ -1,4 +1,8 @@
-namespace Terminal.Gui;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Terminal.Gui;
+
+#nullable enable
///
/// When implemented by a class, allows mapping to
@@ -20,7 +24,7 @@ public interface IColorNameResolver
///
///
///
- bool TryNameColor (Color color, out string name);
+ bool TryNameColor (Color color, [NotNullWhen(true)]out string? name);
///
/// Returns if is a recognized
From 7ba196e75fbf5d90fd299dd064020ec9bbda3f9b Mon Sep 17 00:00:00 2001
From: Tonttu <15074459+TheTonttu@users.noreply.github.com>
Date: Sun, 23 Mar 2025 14:33:47 +0200
Subject: [PATCH 04/16] Remove obsolete color name related ResourceManagerTests
---
.../Resources/ResourceManagerTests.cs | 49 -------------------
1 file changed, 49 deletions(-)
diff --git a/Tests/UnitTests/Resources/ResourceManagerTests.cs b/Tests/UnitTests/Resources/ResourceManagerTests.cs
index 1c49b3447f..15ebb0bdda 100644
--- a/Tests/UnitTests/Resources/ResourceManagerTests.cs
+++ b/Tests/UnitTests/Resources/ResourceManagerTests.cs
@@ -9,10 +9,6 @@ namespace Terminal.Gui.ResourcesTests;
public class ResourceManagerTests
{
- private const string DODGER_BLUE_COLOR_KEY = "DodgerBlue";
- private const string DODGER_BLUE_COLOR_NAME = "DodgerBlue";
- private const string NO_NAMED_COLOR_KEY = "#1E80FF";
- private const string NO_NAMED_COLOR_NAME = "#1E80FF";
private const string EXISTENT_CULTURE = "pt-PT";
private const string NO_EXISTENT_CULTURE = "de-DE";
private const string NO_EXISTENT_KEY = "blabla";
@@ -50,51 +46,6 @@ public void GetObject_FallBack_To_Default_For_Not_Translated_Existent_Culture_Fi
RestoreCurrentCultures ();
}
- [Fact]
- public void GetResourceSet_FallBack_To_Default_For_No_Existent_Culture_File ()
- {
- CultureInfo.CurrentCulture = new (NO_EXISTENT_CULTURE);
- CultureInfo.CurrentUICulture = new (NO_EXISTENT_CULTURE);
-
- // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames
- string [] colorNames = new W3CColors ().GetColorNames ().ToArray ();
- Assert.Contains (DODGER_BLUE_COLOR_NAME, colorNames);
- Assert.DoesNotContain (NO_TRANSLATED_VALUE, colorNames);
-
- RestoreCurrentCultures ();
- }
-
- [Fact]
- public void GetResourceSet_FallBack_To_Default_For_Not_Translated_Existent_Culture_File ()
- {
- CultureInfo.CurrentCulture = new (EXISTENT_CULTURE);
- CultureInfo.CurrentUICulture = new (EXISTENT_CULTURE);
-
- // These aren't already translated
- // ColorStrings.GetW3CColorNames method uses GetResourceSet method to retrieve color names
- IEnumerable colorNames = ColorStrings.GetW3CColorNames ();
- Assert.NotEmpty (colorNames);
-
- // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames
- colorNames = new W3CColors ().GetColorNames ().ToArray ();
- Assert.Contains (DODGER_BLUE_COLOR_NAME, colorNames);
- Assert.DoesNotContain (NO_TRANSLATED_VALUE, colorNames);
-
- // ColorStrings.TryParseW3CColorName method uses GetResourceSet method to retrieve a color value
- Assert.True (ColorStrings.TryParseW3CColorName (DODGER_BLUE_COLOR_NAME, out Color color));
- Assert.Equal (DODGER_BLUE_COLOR_KEY, color.ToString ());
-
- // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames for no-named colors
- colorNames = new W3CColors ().GetColorNames ().ToArray ();
- Assert.DoesNotContain (NO_NAMED_COLOR_NAME, colorNames);
-
- // ColorStrings.TryParseW3CColorName method uses GetResourceSet method to retrieve a color value for no-named colors
- Assert.True (ColorStrings.TryParseW3CColorName (NO_NAMED_COLOR_NAME, out color));
- Assert.Equal (NO_NAMED_COLOR_KEY, color.ToString ());
-
- RestoreCurrentCultures ();
- }
-
[Fact]
public void GetResourceSet_With_Filter_Does_Not_Overflows_If_Key_Does_Not_Exist ()
{
From cb109f333972297457974dfdfd94960f456af4ec Mon Sep 17 00:00:00 2001
From: Tonttu <15074459+TheTonttu@users.noreply.github.com>
Date: Sun, 23 Mar 2025 15:16:42 +0200
Subject: [PATCH 05/16] Replace remains of W3CColors with direct W3C color name
resolver
Temporarily breaks backwards compatibility and tests even further.
---
.../Drawing/Color/IColorNameResolver.cs | 2 +-
.../{W3CColors.cs => W3cColorNameResolver.cs} | 49 +++++++++----------
Terminal.Gui/Views/ColorPicker.cs | 12 ++---
Tests/UnitTests/Views/ColorPickerTests.cs | 4 +-
4 files changed, 32 insertions(+), 35 deletions(-)
rename Terminal.Gui/Drawing/Color/{W3CColors.cs => W3cColorNameResolver.cs} (67%)
diff --git a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs
index f5c0fd9ba0..ac5b2d18e3 100644
--- a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs
+++ b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs
@@ -34,5 +34,5 @@ public interface IColorNameResolver
///
///
///
- bool TryParseColor (string name, out Color color);
+ bool TryParseColor (ReadOnlySpan name, out Color color);
}
diff --git a/Terminal.Gui/Drawing/Color/W3CColors.cs b/Terminal.Gui/Drawing/Color/W3cColorNameResolver.cs
similarity index 67%
rename from Terminal.Gui/Drawing/Color/W3CColors.cs
rename to Terminal.Gui/Drawing/Color/W3cColorNameResolver.cs
index c594930ae7..f22e3f6295 100644
--- a/Terminal.Gui/Drawing/Color/W3CColors.cs
+++ b/Terminal.Gui/Drawing/Color/W3cColorNameResolver.cs
@@ -7,28 +7,24 @@
namespace Terminal.Gui;
///
-/// Helper class that resolves w3c color names to their hex values
-/// Based on https://www.w3schools.com/colors/color_tryit.asp
+/// W3C color name resolver.
///
-[Obsolete ("Superseded by W3cColors")]
-public class W3CColors : IColorNameResolver
+///
+/// Color name resolver interface wrapper for .
+///
+public class W3cColorNameResolver : IColorNameResolver
{
///
- [Obsolete ("Prefer W3cColors.GetColorNames()")]
- public IEnumerable GetColorNames () { return ColorStrings.GetW3CColorNames (); }
+ public IEnumerable GetColorNames () =>
+ W3cColors.GetColorNames ();
///
- public bool TryParseColor (string name, out Color color) { return ColorStrings.TryParseW3CColorName (name, out color); }
+ public bool TryParseColor (ReadOnlySpan name, out Color color) =>
+ W3cColors.TryParseColor (name, out color);
///
- public bool TryNameColor (Color color, out string name)
- {
- string? answer = ColorStrings.GetW3CColorName (color);
-
- name = answer ?? string.Empty;
-
- return answer != null;
- }
+ public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name) =>
+ W3cColors.TryNameColor (color, out name);
}
///
@@ -41,7 +37,8 @@ public static class W3cColors
static W3cColors ()
{
- // Populate based on names because enums with same name are not otherwise distinguishable from each other.
+ // Populate based on names because enums with same numerical value
+ // are not otherwise distinguishable from each other.
string[] w3cNames = Enum.GetNames ().Order().ToArray();
Dictionary map = new(w3cNames.Length);
@@ -66,14 +63,16 @@ public static IReadOnlyList GetColorNames ()
}
///
- /// Tries to parse W3C color from the given name.
+ /// Converts the given W3C color name to equivalent color value.
///
- ///
- /// Contains the successfully parsed value.
- /// True if parsed successfully; otherwise false.
+ /// W3C color name.
+ /// The successfully converted W3C color value.
+ /// True if the conversion succeeded; otherwise false.
public static bool TryParseColor (ReadOnlySpan name, out Color color)
{
- if (!Enum.TryParse (name, ignoreCase: true, out W3cColor w3cColor))
+ if (!Enum.TryParse (name, ignoreCase: true, out W3cColor w3cColor) ||
+ // Any numerical value converts to undefined enum value.
+ !Enum.IsDefined(w3cColor))
{
color = default;
return false;
@@ -85,11 +84,11 @@ public static bool TryParseColor (ReadOnlySpan name, out Color color)
}
///
- /// Tries to match the given color RGB value to a W3C color and returns the name.
+ /// Converts the given color value to a W3C color name.
///
- /// Color to match W3C RGB value.
- /// Contains name of matching W3C color.
- /// True if match; otherwise false.
+ /// Color value to match W3C color.
+ /// The successfully converted W3C color name.
+ /// True if conversion succeeded; otherwise false.
public static bool TryNameColor (Color color, [NotNullWhen (true)] out string? name)
{
int rgb = GetRgb (color.R, color.G, color.B);
diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs
index 415fc1acd1..75889359d4 100644
--- a/Terminal.Gui/Views/ColorPicker.cs
+++ b/Terminal.Gui/Views/ColorPicker.cs
@@ -1,7 +1,5 @@
#nullable enable
-using System;
-
namespace Terminal.Gui;
///
@@ -34,7 +32,7 @@ public ColorPicker ()
private Color _selectedColor = Color.Black;
// TODO: Add interface
- private readonly IColorNameResolver _colorNameResolver = new W3CColors ();
+ private readonly IColorNameResolver _colorNameResolver = new W3cColorNameResolver ();
private List _bars = new ();
@@ -64,7 +62,7 @@ public void ApplyStyleChanges ()
Width = textFieldWidth
};
tfValue.HasFocusChanged += UpdateSingleBarValueFromTextField;
- tfValue.Accepting += (s, _)=>UpdateSingleBarValueFromTextField(s);
+ tfValue.Accepting += (s, _) => UpdateSingleBarValueFromTextField (s);
_textFields.Add (bar, tfValue);
}
@@ -182,7 +180,7 @@ private void CreateTextField ()
Add (_tfHex);
_tfHex.HasFocusChanged += UpdateValueFromTextField;
- _tfHex.Accepting += (_,_)=> UpdateValueFromTextField();
+ _tfHex.Accepting += (_, _) => UpdateValueFromTextField ();
}
private void DisposeOldViews ()
@@ -266,7 +264,7 @@ private void SyncSubViewValues (bool syncBars)
if (_tfName != null)
{
- _tfName.Text = _colorNameResolver.TryNameColor (_selectedColor, out string name) ? name : string.Empty;
+ _tfName.Text = _colorNameResolver.TryNameColor (_selectedColor, out string? name) ? name : string.Empty;
}
if (_tfHex != null)
@@ -312,7 +310,7 @@ private void UpdateValueFromName (object? sender, HasFocusEventArgs e)
}
// it is a leave event so update
- UpdateValueFromName();
+ UpdateValueFromName ();
}
private void UpdateValueFromName ()
{
diff --git a/Tests/UnitTests/Views/ColorPickerTests.cs b/Tests/UnitTests/Views/ColorPickerTests.cs
index 121696ff1d..3c06ab5a50 100644
--- a/Tests/UnitTests/Views/ColorPickerTests.cs
+++ b/Tests/UnitTests/Views/ColorPickerTests.cs
@@ -820,7 +820,7 @@ public static IEnumerable
public static class ColorStrings
{
+ private static readonly AnsiColorNameResolver Ansi = new();
+ private static readonly W3cColorNameResolver W3c = new();
+ private static readonly MultiStandardColorNameResolver Multi = new();
+
///
/// Gets the W3C standard string for .
///
@@ -15,7 +19,7 @@ public static class ColorStrings
/// if there is no standard color name for the specified color.
public static string? GetW3CColorName (Color color)
{
- if (W3cColors.TryNameColor (color, out string? name))
+ if (W3c.TryNameColor (color, out string? name))
{
return name;
}
@@ -29,35 +33,25 @@ public static class ColorStrings
/// if there is no standard color name for the specified color.
public static string? GetANSIColor16Name (Color color)
{
- if (Color.TryGetExactNamedColor16 (color, out ColorName16 color16))
+ if (Ansi.TryNameColor (color, out string? name))
{
- return Color16Name (color16);
+ return name;
}
return null;
}
- private static string Color16Name (ColorName16 color16)
+ ///
+ /// Gets backwards compatible color name for .
+ ///
+ /// The color.
+ /// Standard color name for the specified color; otherwise .
+ public static string? GetColorName (Color color)
{
- return color16 switch
+ if (Multi.TryNameColor (color, out string? name))
{
- ColorName16.Black => nameof (ColorName16.Black),
- ColorName16.Blue => nameof (ColorName16.Blue),
- ColorName16.Green => nameof (ColorName16.Green),
- ColorName16.Cyan => nameof (ColorName16.Cyan),
- ColorName16.Red => nameof (ColorName16.Red),
- ColorName16.Magenta => nameof (ColorName16.Magenta),
- ColorName16.Yellow => nameof (ColorName16.Yellow),
- ColorName16.Gray => nameof (ColorName16.Gray),
- ColorName16.DarkGray => nameof (ColorName16.DarkGray),
- ColorName16.BrightBlue => nameof (ColorName16.BrightBlue),
- ColorName16.BrightGreen => nameof (ColorName16.BrightGreen),
- ColorName16.BrightCyan => nameof (ColorName16.BrightCyan),
- ColorName16.BrightRed => nameof (ColorName16.BrightRed),
- ColorName16.BrightMagenta => nameof (ColorName16.BrightMagenta),
- ColorName16.BrightYellow => nameof (ColorName16.BrightYellow),
- ColorName16.White => nameof (ColorName16.White),
- _ => throw new NotSupportedException ($"ColorName16 '{color16}' is not supported.")
- };
+ return name;
+ }
+ return null;
}
///
@@ -66,7 +60,7 @@ private static string Color16Name (ColorName16 color16)
///
public static IEnumerable GetW3CColorNames ()
{
- return W3cColors.GetColorNames ();
+ return W3c.GetColorNames ();
}
///
@@ -77,44 +71,64 @@ public static IEnumerable GetW3CColorNames ()
/// if was parsed successfully.
public static bool TryParseW3CColorName (ReadOnlySpan name, out Color color)
{
- if (W3cColors.TryParseColor (name, out color))
+ if (W3c.TryParseColor (name, out color))
{
return true;
}
+ // Backwards compatibility: Also parse #RRGGBB.
+ return TryParseHexColor (name, out color);
+ }
- return TryParseColorKey (name, out color);
-
- static bool TryParseColorKey (ReadOnlySpan key, out Color color)
+ ///
+ /// Parses and returns if name is a ANSI 4-bit standard named color.
+ ///
+ /// The name to parse.
+ /// If successful, the color.
+ /// if was parsed successfully.
+ public static bool TryParseColor16 (ReadOnlySpan name, out Color color)
+ {
+ if (Ansi.TryParseColor (name, out color))
{
- if (!key.IsEmpty && key [0] == '#' && key.Length == 7)
- {
- if (int.TryParse (key.Slice (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) &&
- int.TryParse (key.Slice (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) &&
- int.TryParse (key.Slice (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b))
- {
- color = new Color (r, g, b);
- return true;
- }
- }
-
- color = default (Color);
- return false;
+ return true;
}
+ color = default;
+ return false;
}
///
- /// Parses and returns if name is a ANSI 4-bit standard named color.
+ /// Parses and returns if name is either ANSI 4-bit or W3C standard named color.
///
/// The name to parse.
/// If successful, the color.
/// if was parsed successfully.
- public static bool TryParseColor16 (ReadOnlySpan name, out Color color)
+ public static bool TryParseNamedColor (ReadOnlySpan name, out Color color)
{
- if (Enum.TryParse (name, ignoreCase: true, out ColorName16 color16))
+ if (Multi.TryParseColor (name, out color))
{
- color = new Color (color16);
return true;
}
+ // Backwards compatibility: Also parse #RRGGBB.
+ if (TryParseHexColor (name, out color))
+ {
+ return true;
+ }
+
+ color = default;
+ return false;
+ }
+
+ private static bool TryParseHexColor (ReadOnlySpan name, out Color color)
+ {
+ if (name.Length == 7 && name [0] == '#')
+ {
+ if (int.TryParse (name.Slice (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) &&
+ int.TryParse (name.Slice (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) &&
+ int.TryParse (name.Slice (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b))
+ {
+ color = new Color (r, g, b);
+ return true;
+ }
+ }
color = default;
return false;
diff --git a/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs b/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs
new file mode 100644
index 0000000000..b6db824fb7
--- /dev/null
+++ b/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs
@@ -0,0 +1,174 @@
+using System.Collections.Frozen;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+
+#nullable enable
+
+namespace Terminal.Gui;
+
+///
+/// Backwards compatible(-ish) color name resolver prioritizing ANSI 4-bit (16) colors with fallback to W3C colors.
+///
+public class MultiStandardColorNameResolver : IColorNameResolver
+{
+ private static readonly AnsiColorNameResolver Ansi = new();
+ private static readonly W3cColorNameResolver W3c = new();
+ private static readonly FrozenDictionary W3cBlockedColorNameHashMap;
+ private static readonly FrozenSet W3cBlockedColors;
+ private static readonly ImmutableArray CombinedColorNames;
+
+ static MultiStandardColorNameResolver ()
+ {
+ HashSet combinedNames = new(Ansi.GetColorNames());
+
+ HashSet w3cInconsistentColorNames = new(StringComparer.OrdinalIgnoreCase);
+ HashSet w3cInconsistentColors = new();
+
+ IEnumerable enumerableW3cNames = W3c.GetColorNames ();
+ IReadOnlyList w3cNames = enumerableW3cNames is IReadOnlyList alreadyReadOnlyList
+ ? alreadyReadOnlyList
+ : [.. enumerableW3cNames];
+
+ Dictionary> w3cColorsWithAlternativeNames = w3cNames
+ .GroupBy(w3cName =>
+ {
+ if (!W3c.TryParseColor(w3cName, out Color w3cColor))
+ {
+ throw new InvalidOperationException ($"W3C color name '{w3cName}' does not resolve to any W3C color.");
+ }
+ return w3cColor;
+ })
+ .Where(g => g.Count() > 1)
+ .ToDictionary(g => g.Key, g => g.ToHashSet());
+
+ // Gather inconsistencies between ANSI and W3C, filter out problematic W3C names and
+ // create additional blocklists for W3C names and colors.
+ // Blocking and filtering is only applied to W3C because this resolver prioritizes ANSI for backwards compatibility.
+ // It would be a lot simpler to just prioritize W3C colors and names.
+ foreach (string w3cName in w3cNames)
+ {
+ if (w3cInconsistentColorNames.Contains (w3cName))
+ {
+ // Already blocked, most likely through alternative name.
+ continue;
+ }
+
+ if (!W3c.TryParseColor (w3cName, out Color w3cColor))
+ {
+ // This condition is just inverted to reduce indentation.
+ // This should practically never happen if the W3C color name resolver is properly implemented.
+ throw new InvalidOperationException ($"W3C color name '{w3cName}' does not resolve to any color.");
+ }
+
+ if (w3cColorsWithAlternativeNames.TryGetValue (w3cColor, out var names))
+ {
+ bool blocked = false;
+ // Alternative names cause issues with ColorPicker etc. when combined with ANSI and prioritizing ANSI resolver.
+ // For example Aqua is not in ColorName16 but the actual color value resolves to ANSI Cyan
+ // so autocomplete for Aqua suddenly changes to Cyan because they happen to have same color value in both color scheme.
+ // Also DarkGrey would cause inconsistencies because the alternative DarkGray exists in ANSI and has different color value.
+ foreach (string name in names)
+ {
+ if (Ansi.TryParseColor (name, out _))
+ {
+ w3cInconsistentColors.Add (w3cColor);
+ // Block all if one is inconsistent.
+ foreach (string inconsistentName in names)
+ {
+ w3cInconsistentColorNames.Add (inconsistentName);
+ }
+ blocked = true;
+ break;
+ }
+ }
+
+ if (blocked)
+ {
+ // Already blocked continue to next W3C color name.
+ continue;
+ }
+ }
+
+ // Just in case check.
+ // Same name, different ANSI value.
+ // For example both #767676 (ColorName16) and #A9A9A9 (W3C) resolve to DarkGray,
+ // although a bad example because it is already filtered due to also having alternative names.
+ if (Ansi.TryParseColor (w3cName, out Color ansiColor) && w3cColor != ansiColor)
+ {
+ w3cInconsistentColorNames.Add (w3cName);
+ w3cInconsistentColors.Add (w3cColor);
+ continue;
+ }
+
+ combinedNames.Add (w3cName);
+ }
+
+ // TODO: Utilize .NET 9 and later alternative lookup for matching ReadOnlySpan with string.
+ W3cBlockedColorNameHashMap = w3cInconsistentColorNames.ToFrozenDictionary (
+ // Workaround for alternative lookup not being available in .NET 8 by matching ReadOnlySpan hash code to string hash code.
+ keySelector: x => string.GetHashCode (x, StringComparison.OrdinalIgnoreCase),
+ // String element is just for verifying hash code collision, e.g. same hash code but the name was different.
+ // Quite unlikely due to small data set, but still possible.
+ elementSelector: x => x);
+ W3cBlockedColors = w3cInconsistentColors.ToFrozenSet ();
+ CombinedColorNames = combinedNames.Order ().ToImmutableArray ();
+ }
+
+ ///
+ public IEnumerable GetColorNames ()
+ {
+ return CombinedColorNames;
+ }
+
+ ///
+ public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name)
+ {
+ if (Ansi.TryNameColor (color, out string? ansiName))
+ {
+ name = ansiName;
+ return true;
+ }
+
+ if (!IsBlockedW3cColor (color) &&
+ W3c.TryNameColor (color, out string? w3cName) &&
+ !IsBlockedW3cName (w3cName))
+ {
+ name = w3cName;
+ return true;
+ }
+
+ name = null;
+ return false;
+ }
+
+ ///
+ public bool TryParseColor (ReadOnlySpan name, out Color color)
+ {
+ if (Ansi.TryParseColor (name, out color))
+ {
+ return true;
+ }
+
+ if (!IsBlockedW3cName (name) &&
+ W3c.TryParseColor (name, out color) &&
+ !IsBlockedW3cColor (color))
+ {
+ return true;
+ }
+
+ color = default;
+ return false;
+ }
+
+ private bool IsBlockedW3cName (ReadOnlySpan name)
+ {
+ int nameHashCode = string.GetHashCode(name, StringComparison.OrdinalIgnoreCase);
+ return W3cBlockedColorNameHashMap.TryGetValue (nameHashCode, out string? inconsistentColorName) &&
+ name.Equals (inconsistentColorName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool IsBlockedW3cColor (Color color)
+ {
+ return W3cBlockedColors.Contains (color);
+ }
+}
diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs
index 75889359d4..2bd102085c 100644
--- a/Terminal.Gui/Views/ColorPicker.cs
+++ b/Terminal.Gui/Views/ColorPicker.cs
@@ -32,7 +32,7 @@ public ColorPicker ()
private Color _selectedColor = Color.Black;
// TODO: Add interface
- private readonly IColorNameResolver _colorNameResolver = new W3cColorNameResolver ();
+ private readonly IColorNameResolver _colorNameResolver = new MultiStandardColorNameResolver ();
private List _bars = new ();
diff --git a/Tests/UnitTests/Views/ColorPickerTests.cs b/Tests/UnitTests/Views/ColorPickerTests.cs
index 3c06ab5a50..38a09ac23f 100644
--- a/Tests/UnitTests/Views/ColorPickerTests.cs
+++ b/Tests/UnitTests/Views/ColorPickerTests.cs
@@ -817,14 +817,6 @@ public static IEnumerable