Color.cs 19 KB

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