Color.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. using System.Collections.Frozen;
  2. using System.Globalization;
  3. using System.Numerics;
  4. using System.Runtime.CompilerServices;
  5. using System.Runtime.InteropServices;
  6. using System.Text.Json.Serialization;
  7. using ColorHelper;
  8. using ColorConverter = ColorHelper.ColorConverter;
  9. namespace Terminal.Gui.Drawing;
  10. /// <summary>
  11. /// Represents a 24-bit color encoded in ARGB32 format.
  12. /// <para>
  13. /// The RGB components define the color identity (what color it is), while the alpha channel defines
  14. /// rendering intent (how transparent it should be when drawn).
  15. /// </para>
  16. /// </summary>
  17. /// <remarks>
  18. /// <para>
  19. /// When matching colors to standard color names (e.g., via <see cref="ColorStrings.GetColorName"/>),
  20. /// the alpha channel is ignored. This means colors with the same RGB values but different alpha values
  21. /// will resolve to the same color name. This design supports transparency features while maintaining
  22. /// semantic color identity.
  23. /// </para>
  24. /// <para>
  25. /// While Terminal.Gui does not currently support alpha blending during rendering, the alpha channel
  26. /// is used to indicate rendering intent:
  27. /// <list type="bullet">
  28. /// <item><description>Alpha = 0: Fully transparent (don't render)</description></item>
  29. /// <item><description>Alpha = 255: Fully opaque (normal rendering)</description></item>
  30. /// <item><description>Other values: Reserved for future alpha blending support</description></item>
  31. /// </list>
  32. /// </para>
  33. /// </remarks>
  34. /// <seealso cref="Attribute"/>
  35. /// <seealso cref="ColorExtensions"/>
  36. /// <seealso cref="ColorName16"/>
  37. [JsonConverter (typeof (ColorJsonConverter))]
  38. [StructLayout (LayoutKind.Explicit)]
  39. public readonly partial record struct Color : ISpanParsable<Color>, IUtf8SpanParsable<Color>, ISpanFormattable,
  40. IUtf8SpanFormattable, IMinMaxValue<Color>
  41. {
  42. /// <summary>The value of the alpha channel component</summary>
  43. /// <remarks>
  44. /// <para>
  45. /// The alpha channel represents rendering intent (transparency) rather than color identity.
  46. /// Terminal.Gui does not currently perform alpha blending, but uses this value to determine
  47. /// whether to render the color at all (alpha = 0 means don't render).
  48. /// </para>
  49. /// <para>
  50. /// When matching colors to standard color names, the alpha channel is ignored. For example,
  51. /// <c>new Color(255, 0, 0, 255)</c> and <c>new Color(255, 0, 0, 128)</c> will both be
  52. /// identified as "Red".
  53. /// </para>
  54. /// </remarks>
  55. [JsonIgnore]
  56. [field: FieldOffset (3)]
  57. public readonly byte A;
  58. /// <summary>The value of this <see cref="Color"/> as a <see langword="uint"/> in ARGB32 format.</summary>
  59. /// <remarks>
  60. /// The alpha channel in the ARGB value represents rendering intent (transparency), not color identity.
  61. /// When matching to standard color names, only the RGB components are considered.
  62. /// </remarks>
  63. [JsonIgnore]
  64. [field: FieldOffset (0)]
  65. public readonly uint Argb;
  66. /// <summary>The value of the blue color component.</summary>
  67. [JsonIgnore]
  68. [field: FieldOffset (0)]
  69. public readonly byte B;
  70. /// <summary>The value of the green color component.</summary>
  71. [JsonIgnore]
  72. [field: FieldOffset (1)]
  73. public readonly byte G;
  74. /// <summary>The value of the red color component.</summary>
  75. [JsonIgnore]
  76. [field: FieldOffset (2)]
  77. public readonly byte R;
  78. /// <summary>The value of this <see cref="Color"/> encoded as a signed 32-bit integer in ARGB32 format.</summary>
  79. [JsonIgnore]
  80. [field: FieldOffset (0)]
  81. public readonly int Rgba;
  82. /// <summary>
  83. /// Initializes a new instance of the <see cref="Color"/> <see langword="struct"/> using the supplied component
  84. /// values.
  85. /// </summary>
  86. /// <param name="red">The red 8-bits.</param>
  87. /// <param name="green">The green 8-bits.</param>
  88. /// <param name="blue">The blue 8-bits.</param>
  89. /// <param name="alpha">Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui.</param>
  90. /// <remarks>Alpha channel is not currently supported by Terminal.Gui.</remarks>
  91. /// <exception cref="OverflowException">If the value of any parameter is greater than <see cref="byte.MaxValue"/>.</exception>
  92. /// <exception cref="ArgumentOutOfRangeException">If the value of any parameter is negative.</exception>
  93. public Color (int red = 0, int green = 0, int blue = 0, int alpha = byte.MaxValue)
  94. {
  95. ArgumentOutOfRangeException.ThrowIfNegative (red, nameof (red));
  96. ArgumentOutOfRangeException.ThrowIfNegative (green, nameof (green));
  97. ArgumentOutOfRangeException.ThrowIfNegative (blue, nameof (blue));
  98. ArgumentOutOfRangeException.ThrowIfNegative (alpha, nameof (alpha));
  99. A = Convert.ToByte (alpha);
  100. R = Convert.ToByte (red);
  101. G = Convert.ToByte (green);
  102. B = Convert.ToByte (blue);
  103. }
  104. /// <summary>
  105. /// Initializes a new instance of the <see cref="Color"/> class with an encoded signed 32-bit color value in
  106. /// ARGB32 format.
  107. /// </summary>
  108. /// <param name="rgba">The encoded 32-bit color value (see <see cref="Rgba"/>).</param>
  109. /// <remarks>
  110. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
  111. /// rendering.
  112. /// </remarks>
  113. public Color (int rgba) { Rgba = rgba; }
  114. /// <summary>
  115. /// Initializes a new instance of the <see cref="Color"/> class with an encoded unsigned 32-bit color value in
  116. /// ARGB32 format.
  117. /// </summary>
  118. /// <param name="argb">The encoded unsigned 32-bit color value (see <see cref="Argb"/>).</param>
  119. /// <remarks>
  120. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
  121. /// rendering.
  122. /// </remarks>
  123. public Color (uint argb) { Argb = argb; }
  124. /// <summary>Initializes a new instance of the <see cref="Color"/> color from a legacy 16-color named value.</summary>
  125. /// <param name="colorName">The 16-color value.</param>
  126. public Color (in ColorName16 colorName) { this = ColorExtensions.ColorName16ToColorMap! [colorName]; }
  127. /// <summary>Initializes a new instance of the <see cref="Color"/> color from a value in the <see cref="StandardColor"/> enum.</summary>
  128. /// <param name="colorName">The 16-color value.</param>
  129. public Color (in StandardColor colorName) : this (StandardColors.GetArgb (colorName)) { }
  130. /// <summary>
  131. /// Initializes a new instance of the <see cref="Color"/> color from string. See
  132. /// <see cref="TryParse(string, out Color?)"/> for details.
  133. /// </summary>
  134. /// <param name="colorString"></param>
  135. /// <exception cref="ArgumentNullException">If <paramref name="colorString"/> is <see langword="null"/>.</exception>
  136. /// <exception cref="ArgumentException">
  137. /// If <paramref name="colorString"/> is an empty string or consists of only whitespace
  138. /// characters.
  139. /// </exception>
  140. /// <exception cref="ColorParseException">If thrown by <see cref="Parse(string?,System.IFormatProvider?)"/></exception>
  141. public Color (string colorString)
  142. {
  143. ArgumentException.ThrowIfNullOrWhiteSpace (colorString, nameof (colorString));
  144. this = Parse (colorString, CultureInfo.InvariantCulture);
  145. }
  146. /// <summary>Initializes a new instance of the <see cref="Color"/> with all channels set to 0.</summary>
  147. public Color () { Argb = 0u; }
  148. /// <summary>Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values.</summary>
  149. [ConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  150. public static Dictionary<ColorName16, string> Colors16
  151. {
  152. get =>
  153. // Transform _colorToNameMap into a Dictionary<ColorNames,string>
  154. ColorExtensions.ColorToName16Map!.ToDictionary (static kvp => kvp.Value, static kvp => kvp.Key.ToString ("g"));
  155. set
  156. {
  157. // Transform Dictionary<ColorNames,string> into _colorToNameMap
  158. ColorExtensions.ColorToName16Map = value.ToFrozenDictionary (GetColorToNameMapKey, GetColorToNameMapValue);
  159. return;
  160. static Color GetColorToNameMapKey (KeyValuePair<ColorName16, string> kvp) { return new (kvp.Value); }
  161. static ColorName16 GetColorToNameMapValue (KeyValuePair<ColorName16, string> kvp)
  162. {
  163. return Enum.TryParse (kvp.Key.ToString (), true, out ColorName16 colorName)
  164. ? colorName
  165. : throw new ArgumentException ($"Invalid color name: {kvp.Key}");
  166. }
  167. }
  168. }
  169. /// <summary>
  170. /// Gets the <see cref="Color"/> using a legacy 16-color <see cref="ColorName16"/> value. <see langword="get"/> will
  171. /// return the closest 16 color match to the true color when no exact value is found.
  172. /// </summary>
  173. /// <remarks>
  174. /// Get returns the <see cref="GetClosestNamedColor16(Color)"/> of the closest 24-bit color value. Set sets the RGB
  175. /// value using a hard-coded map.
  176. /// </remarks>
  177. public AnsiColorCode GetAnsiColorCode () { return ColorExtensions.ColorName16ToAnsiColorMap [GetClosestNamedColor16 ()]; }
  178. /// <summary>
  179. /// Gets the <see cref="Color"/> using a legacy 16-color <see cref="ColorName16"/> value. <see langword="get"/>
  180. /// will return the closest 16 color match to the true color when no exact value is found.
  181. /// </summary>
  182. /// <remarks>
  183. /// Get returns the <see cref="GetClosestNamedColor16(Color)"/> of the closest 24-bit color value. Set
  184. /// sets the RGB
  185. /// value using a hard-coded map.
  186. /// </remarks>
  187. public ColorName16 GetClosestNamedColor16 () { return GetClosestNamedColor16 (this); }
  188. /// <summary>
  189. /// Determines if the closest named <see cref="Color"/> to <see langword="this"/> is the provided
  190. /// <paramref name="namedColor"/>.
  191. /// </summary>
  192. /// <param name="namedColor">
  193. /// The <see cref="GetClosestNamedColor16(Color)"/> to check if this <see cref="Color"/> is closer
  194. /// to than any other configured named color.
  195. /// </param>
  196. /// <returns>
  197. /// <see langword="true"/> if the closest named color is the provided value. <br/> <see langword="false"/> if any
  198. /// other named color is closer to this <see cref="Color"/> than <paramref name="namedColor"/>.
  199. /// </returns>
  200. /// <remarks>
  201. /// If <see langword="this"/> is equidistant from two named colors, the result of this method is not guaranteed to
  202. /// be determinate.
  203. /// </remarks>
  204. [Pure]
  205. [MethodImpl (MethodImplOptions.AggressiveInlining)]
  206. public bool IsClosestToNamedColor16 (in ColorName16 namedColor) { return GetClosestNamedColor16 () == namedColor; }
  207. /// <summary>Gets the "closest" named color to this <see cref="Color"/> value.</summary>
  208. /// <param name="inputColor"></param>
  209. /// <remarks>
  210. /// Distance is defined here as the Euclidean distance between each color interpreted as a <see cref="Vector3"/>.
  211. /// </remarks>
  212. /// <returns></returns>
  213. [SkipLocalsInit]
  214. internal static ColorName16 GetClosestNamedColor16 (Color inputColor)
  215. {
  216. return ColorExtensions.ColorToName16Map!.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
  217. }
  218. [SkipLocalsInit]
  219. private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); }
  220. /// <summary>
  221. /// Returns a color with the same hue and saturation as this color, but with a significantly different lightness,
  222. /// making it suitable for use as a highlight or contrast color in UI elements.
  223. /// </summary>
  224. /// <remarks>
  225. /// <para>
  226. /// This method brightens the color if it is dark, or darkens it if it is light, ensuring the result is visually
  227. /// distinct
  228. /// from the original. The algorithm works in HSL color space and adjusts the lightness channel:
  229. /// <list type="bullet">
  230. /// <item>
  231. /// <description>If the color is dark (lightness &lt; 0.5), the lightness is increased (brightened).</description>
  232. /// </item>
  233. /// <item>
  234. /// <description>If the color is light (lightness &gt;= 0.5), the lightness is decreased (darkened).</description>
  235. /// </item>
  236. /// <item>
  237. /// <description>
  238. /// If the adjustment resulted in a color too close to the original, a larger adjustment is
  239. /// made.
  240. /// </description>
  241. /// </item>
  242. /// </list>
  243. /// This ensures the returned color is always visually distinct and suitable for highlighting or selection states.
  244. /// </para>
  245. /// <para>
  246. /// The returned color will always have the same hue and saturation as the original, but a different lightness.
  247. /// </para>
  248. /// </remarks>
  249. /// <param name="brightenAmount">The percent amount to brighten the color by. The default is <c>20%</c>.</param>
  250. /// <returns>
  251. /// A <see cref="Color"/> instance with the same hue and saturation as this color, but with a contrasting lightness.
  252. /// </returns>
  253. /// <example>
  254. /// <code>
  255. /// var baseColor = new Color(100, 100, 100);
  256. /// var highlight = baseColor.GetHighlightColor();
  257. /// // highlight will be a lighter or darker version of baseColor, depending on its original lightness.
  258. /// </code>
  259. /// </example>
  260. public Color GetBrighterColor (double brightenAmount = 0.2)
  261. {
  262. HSL? hsl = ColorConverter.RgbToHsl (new (R, G, B));
  263. double lNorm = hsl.L / 255.0;
  264. double newL = lNorm < 0.5 ? Math.Min (1.0, lNorm + brightenAmount) : Math.Max (0.0, lNorm - brightenAmount);
  265. if (Math.Abs (newL - lNorm) < 0.1)
  266. {
  267. newL = lNorm < 0.5 ? Math.Min (1.0, lNorm + 2 * brightenAmount) : Math.Max (0.0, lNorm - 2 * brightenAmount);
  268. }
  269. var newHsl = new HSL (hsl.H, hsl.S, (byte)(newL * 255));
  270. RGB? rgb = ColorConverter.HslToRgb (newHsl);
  271. return new (rgb.R, rgb.G, rgb.B);
  272. }
  273. /// <summary>
  274. /// Returns a color with the same hue and saturation as this color, but with a significantly lower lightness,
  275. /// making it suitable for use as a shadow or background contrast color in UI elements.
  276. /// </summary>
  277. /// <remarks>
  278. /// <para>
  279. /// This method darkens the color by reducing its lightness in HSL color space:
  280. /// <list type="bullet">
  281. /// <item>
  282. /// <description>If the color is already very dark, returns <see cref="ColorName16.DarkGray"/>.</description>
  283. /// </item>
  284. /// <item>
  285. /// <description>Otherwise, reduces the lightness by a fixed amount (default 30%).</description>
  286. /// </item>
  287. /// <item>
  288. /// <description>
  289. /// If the adjustment resulted in a color too close to the original, a larger adjustment is
  290. /// made.
  291. /// </description>
  292. /// </item>
  293. /// </list>
  294. /// This ensures the returned color is always visually distinct and suitable for shadowing or de-emphasis.
  295. /// </para>
  296. /// </remarks>
  297. /// <param name="dimAmount">The percent amount to dim the color by. The default is <c>20%</c>.</param>
  298. /// <returns>
  299. /// A <see cref="Color"/> instance with the same hue and saturation as this color, but with a much lower lightness.
  300. /// </returns>
  301. public Color GetDimColor (double dimAmount = 0.2)
  302. {
  303. HSL hsl = ColorConverter.RgbToHsl (new (R, G, B));
  304. double lNorm = hsl.L / 255.0;
  305. double newL = Math.Max (0.0, lNorm - dimAmount);
  306. // If the color is already very dark, return a standard dark gray for visibility
  307. if (lNorm <= 0.1)
  308. {
  309. return new (ColorName16.DarkGray);
  310. }
  311. // If the new lightness is too close to the original, force a bigger change
  312. if (Math.Abs (newL - lNorm) < 0.1)
  313. {
  314. newL = Math.Max (0.0, lNorm - 2 * dimAmount);
  315. }
  316. var newHsl = new HSL (hsl.H, hsl.S, (byte)(newL * 255));
  317. RGB rgb = ColorConverter.HslToRgb (newHsl);
  318. return new (rgb.R, rgb.G, rgb.B);
  319. }
  320. #region Legacy Color Names
  321. // ReSharper disable InconsistentNaming
  322. /// <summary>The black color.</summary>
  323. public const ColorName16 Black = ColorName16.Black;
  324. /// <summary>The blue color.</summary>
  325. public const ColorName16 Blue = ColorName16.Blue;
  326. /// <summary>The green color.</summary>
  327. public const ColorName16 Green = ColorName16.Green;
  328. /// <summary>The cyan color.</summary>
  329. public const ColorName16 Cyan = ColorName16.Cyan;
  330. /// <summary>The red color.</summary>
  331. public const ColorName16 Red = ColorName16.Red;
  332. /// <summary>The magenta color.</summary>
  333. public const ColorName16 Magenta = ColorName16.Magenta;
  334. /// <summary>The yellow color.</summary>
  335. public const ColorName16 Yellow = ColorName16.Yellow;
  336. /// <summary>The gray color.</summary>
  337. public const ColorName16 Gray = ColorName16.Gray;
  338. /// <summary>The dark gray color.</summary>
  339. public const ColorName16 DarkGray = ColorName16.DarkGray;
  340. /// <summary>The bright bBlue color.</summary>
  341. public const ColorName16 BrightBlue = ColorName16.BrightBlue;
  342. /// <summary>The bright green color.</summary>
  343. public const ColorName16 BrightGreen = ColorName16.BrightGreen;
  344. /// <summary>The bright cyan color.</summary>
  345. public const ColorName16 BrightCyan = ColorName16.BrightCyan;
  346. /// <summary>The bright red color.</summary>
  347. public const ColorName16 BrightRed = ColorName16.BrightRed;
  348. /// <summary>The bright magenta color.</summary>
  349. public const ColorName16 BrightMagenta = ColorName16.BrightMagenta;
  350. /// <summary>The bright yellow color.</summary>
  351. public const ColorName16 BrightYellow = ColorName16.BrightYellow;
  352. /// <summary>The White color.</summary>
  353. public const ColorName16 White = ColorName16.White;
  354. #endregion
  355. }