Color.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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. namespace Terminal.Gui;
  10. /// <summary>
  11. /// Represents a 24-bit color encoded in ARGB32 format.
  12. /// <para/>
  13. /// </summary>
  14. /// <seealso cref="Attribute"/>
  15. /// <seealso cref="ColorExtensions"/>
  16. /// <seealso cref="ColorName16"/>
  17. [JsonConverter (typeof (ColorJsonConverter))]
  18. [StructLayout (LayoutKind.Explicit)]
  19. public readonly partial record struct Color : ISpanParsable<Color>, IUtf8SpanParsable<Color>, ISpanFormattable,
  20. IUtf8SpanFormattable, IMinMaxValue<Color>
  21. {
  22. /// <summary>The value of the alpha channel component</summary>
  23. /// <remarks>
  24. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
  25. /// rendering.
  26. /// </remarks>
  27. [JsonIgnore]
  28. [field: FieldOffset (3)]
  29. public readonly byte A;
  30. /// <summary>The value of this <see cref="Color"/> as a <see langword="uint"/> in ARGB32 format.</summary>
  31. /// <remarks>
  32. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
  33. /// rendering.
  34. /// </remarks>
  35. [JsonIgnore]
  36. [field: FieldOffset (0)]
  37. public readonly uint Argb;
  38. /// <summary>The value of the blue color component.</summary>
  39. [JsonIgnore]
  40. [field: FieldOffset (0)]
  41. public readonly byte B;
  42. /// <summary>The value of the green color component.</summary>
  43. [JsonIgnore]
  44. [field: FieldOffset (1)]
  45. public readonly byte G;
  46. /// <summary>The value of the red color component.</summary>
  47. [JsonIgnore]
  48. [field: FieldOffset (2)]
  49. public readonly byte R;
  50. /// <summary>The value of this <see cref="Color"/> encoded as a signed 32-bit integer in ARGB32 format.</summary>
  51. [JsonIgnore]
  52. [field: FieldOffset (0)]
  53. public readonly int Rgba;
  54. /// <summary>
  55. /// Initializes a new instance of the <see cref="Color"/> <see langword="struct"/> using the supplied component
  56. /// values.
  57. /// </summary>
  58. /// <param name="red">The red 8-bits.</param>
  59. /// <param name="green">The green 8-bits.</param>
  60. /// <param name="blue">The blue 8-bits.</param>
  61. /// <param name="alpha">Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui.</param>
  62. /// <remarks>Alpha channel is not currently supported by Terminal.Gui.</remarks>
  63. /// <exception cref="OverflowException">If the value of any parameter is greater than <see cref="byte.MaxValue"/>.</exception>
  64. /// <exception cref="ArgumentOutOfRangeException">If the value of any parameter is negative.</exception>
  65. public Color (int red = 0, int green = 0, int blue = 0, int alpha = byte.MaxValue)
  66. {
  67. ArgumentOutOfRangeException.ThrowIfNegative (red, nameof (red));
  68. ArgumentOutOfRangeException.ThrowIfNegative (green, nameof (green));
  69. ArgumentOutOfRangeException.ThrowIfNegative (blue, nameof (blue));
  70. ArgumentOutOfRangeException.ThrowIfNegative (alpha, nameof (alpha));
  71. A = Convert.ToByte (alpha);
  72. R = Convert.ToByte (red);
  73. G = Convert.ToByte (green);
  74. B = Convert.ToByte (blue);
  75. }
  76. /// <summary>
  77. /// Initializes a new instance of the <see cref="Color"/> class with an encoded signed 32-bit color value in
  78. /// ARGB32 format.
  79. /// </summary>
  80. /// <param name="rgba">The encoded 32-bit color value (see <see cref="Rgba"/>).</param>
  81. /// <remarks>
  82. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
  83. /// rendering.
  84. /// </remarks>
  85. public Color (int rgba) { Rgba = rgba; }
  86. /// <summary>
  87. /// Initializes a new instance of the <see cref="Color"/> class with an encoded unsigned 32-bit color value in
  88. /// ARGB32 format.
  89. /// </summary>
  90. /// <param name="argb">The encoded unsigned 32-bit color value (see <see cref="Argb"/>).</param>
  91. /// <remarks>
  92. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect
  93. /// rendering.
  94. /// </remarks>
  95. public Color (uint argb) { Argb = argb; }
  96. /// <summary>Initializes a new instance of the <see cref="Color"/> color from a legacy 16-color named value.</summary>
  97. /// <param name="colorName">The 16-color value.</param>
  98. public Color (in ColorName16 colorName) { this = ColorExtensions.ColorName16ToColorMap [colorName]; }
  99. /// <summary>
  100. /// Initializes a new instance of the <see cref="Color"/> color from string. See
  101. /// <see cref="TryParse(string, out Color?)"/> for details.
  102. /// </summary>
  103. /// <param name="colorString"></param>
  104. /// <exception cref="ArgumentNullException">If <paramref name="colorString"/> is <see langword="null"/>.</exception>
  105. /// <exception cref="ArgumentException">
  106. /// If <paramref name="colorString"/> is an empty string or consists of only whitespace
  107. /// characters.
  108. /// </exception>
  109. /// <exception cref="ColorParseException">If thrown by <see cref="Parse(string?,System.IFormatProvider?)"/></exception>
  110. public Color (string colorString)
  111. {
  112. ArgumentException.ThrowIfNullOrWhiteSpace (colorString, nameof (colorString));
  113. this = Parse (colorString, CultureInfo.InvariantCulture);
  114. }
  115. /// <summary>Initializes a new instance of the <see cref="Color"/> with all channels set to 0.</summary>
  116. public Color () { Argb = 0u; }
  117. // TODO: ColorName and AnsiColorCode are only needed when a driver is in Force16Color mode and we
  118. // TODO: should be able to remove these from any non-Driver-specific usages.
  119. /// <summary>Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values.</summary>
  120. [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  121. public static Dictionary<ColorName16, string> Colors16
  122. {
  123. get =>
  124. // Transform _colorToNameMap into a Dictionary<ColorNames,string>
  125. ColorExtensions.ColorToName16Map.ToDictionary (static kvp => kvp.Value, static kvp => kvp.Key.ToString ("g"));
  126. set
  127. {
  128. // Transform Dictionary<ColorNames,string> into _colorToNameMap
  129. ColorExtensions.ColorToName16Map = value.ToFrozenDictionary (GetColorToNameMapKey, GetColorToNameMapValue);
  130. return;
  131. static Color GetColorToNameMapKey (KeyValuePair<ColorName16, string> kvp) { return new Color (kvp.Value); }
  132. static ColorName16 GetColorToNameMapValue (KeyValuePair<ColorName16, string> kvp)
  133. {
  134. return Enum.TryParse (kvp.Key.ToString (), true, out ColorName16 colorName)
  135. ? colorName
  136. : throw new ArgumentException ($"Invalid color name: {kvp.Key}");
  137. }
  138. }
  139. }
  140. /// <summary>
  141. /// Gets the <see cref="Color"/> using a legacy 16-color <see cref="ColorName16"/> value. <see langword="get"/> will
  142. /// return the closest 16 color match to the true color when no exact value is found.
  143. /// </summary>
  144. /// <remarks>
  145. /// Get returns the <see cref="GetClosestNamedColor16(Color)"/> of the closest 24-bit color value. Set sets the RGB
  146. /// value using a hard-coded map.
  147. /// </remarks>
  148. public AnsiColorCode GetAnsiColorCode () { return ColorExtensions.ColorName16ToAnsiColorMap [GetClosestNamedColor16 ()]; }
  149. /// <summary>
  150. /// Gets the <see cref="Color"/> using a legacy 16-color <see cref="ColorName16"/> value. <see langword="get"/>
  151. /// will return the closest 16 color match to the true color when no exact value is found.
  152. /// </summary>
  153. /// <remarks>
  154. /// Get returns the <see cref="GetClosestNamedColor16(Terminal.Gui.Color)"/> of the closest 24-bit color value. Set sets the RGB
  155. /// value using a hard-coded map.
  156. /// </remarks>
  157. public ColorName16 GetClosestNamedColor16 () { return GetClosestNamedColor16 (this); }
  158. /// <summary>
  159. /// Determines if the closest named <see cref="Color"/> to <see langword="this"/> is the provided
  160. /// <paramref name="namedColor"/>.
  161. /// </summary>
  162. /// <param name="namedColor">
  163. /// The <see cref="GetClosestNamedColor16(Terminal.Gui.Color)"/> to check if this <see cref="Color"/> is closer
  164. /// to than any other configured named color.
  165. /// </param>
  166. /// <returns>
  167. /// <see langword="true"/> if the closest named color is the provided value. <br/> <see langword="false"/> if any
  168. /// other named color is closer to this <see cref="Color"/> than <paramref name="namedColor"/>.
  169. /// </returns>
  170. /// <remarks>
  171. /// If <see langword="this"/> is equidistant from two named colors, the result of this method is not guaranteed to
  172. /// be determinate.
  173. /// </remarks>
  174. [Pure]
  175. [MethodImpl (MethodImplOptions.AggressiveInlining)]
  176. public bool IsClosestToNamedColor16 (in ColorName16 namedColor) { return GetClosestNamedColor16 () == namedColor; }
  177. /// <summary>
  178. /// Determines if the closest named <see cref="Color"/> to <paramref name="color"/>/> is the provided
  179. /// <paramref name="namedColor"/>.
  180. /// </summary>
  181. /// <param name="color">
  182. /// The color to test against the <see cref="GetClosestNamedColor16(Terminal.Gui.Color)"/> value in
  183. /// <paramref name="namedColor"/>.
  184. /// </param>
  185. /// <param name="namedColor">
  186. /// The <see cref="GetClosestNamedColor16(Terminal.Gui.Color)"/> to check if this <see cref="Color"/> is closer
  187. /// to than any other configured named color.
  188. /// </param>
  189. /// <returns>
  190. /// <see langword="true"/> if the closest named color to <paramref name="color"/> is the provided value. <br/>
  191. /// <see langword="false"/> if any other named color is closer to <paramref name="color"/> than
  192. /// <paramref name="namedColor"/>.
  193. /// </returns>
  194. /// <remarks>
  195. /// If <paramref name="color"/> is equidistant from two named colors, the result of this method is not guaranteed
  196. /// to be determinate.
  197. /// </remarks>
  198. [Pure]
  199. [MethodImpl (MethodImplOptions.AggressiveInlining)]
  200. public static bool IsColorClosestToNamedColor16 (in Color color, in ColorName16 namedColor) { return color.IsClosestToNamedColor16 (in namedColor); }
  201. /// <summary>Gets the "closest" named color to this <see cref="Color"/> value.</summary>
  202. /// <param name="inputColor"></param>
  203. /// <remarks>
  204. /// Distance is defined here as the Euclidean distance between each color interpreted as a <see cref="Vector3"/>.
  205. /// </remarks>
  206. /// <returns></returns>
  207. [SkipLocalsInit]
  208. internal static ColorName16 GetClosestNamedColor16 (Color inputColor)
  209. {
  210. return ColorExtensions.ColorToName16Map.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
  211. }
  212. /// <summary>Converts the given color value to exact named color represented by <see cref="ColorName16"/>.</summary>
  213. /// <param name="inputColor"></param>
  214. /// <param name="colorName16">Successfully converted named color.</param>
  215. /// <returns>True if conversion succeeded; otherwise false.</returns>
  216. internal static bool TryGetExactNamedColor16 (Color inputColor, out ColorName16 colorName16)
  217. {
  218. return ColorExtensions.ColorToName16Map.TryGetValue (inputColor, out colorName16);
  219. }
  220. [SkipLocalsInit]
  221. private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); }
  222. /// <summary>
  223. /// Gets a color that is the same hue as the current color, but with a different lightness.
  224. /// </summary>
  225. /// <returns></returns>
  226. public Color GetHighlightColor ()
  227. {
  228. // TODO: This is a temporary implementation; just enough to show how it could work.
  229. var hsl = ColorHelper.ColorConverter.RgbToHsl (new RGB (R, G, B));
  230. var amount = .7;
  231. if (hsl.L <= 5)
  232. {
  233. return DarkGray;
  234. }
  235. hsl.L = (byte)(hsl.L * amount);
  236. var rgb = ColorHelper.ColorConverter.HslToRgb (hsl);
  237. return new (rgb.R, rgb.G, rgb.B);
  238. }
  239. /// <summary>
  240. /// Gets a color that is the same hue as the current color, but with a different lightness.
  241. /// </summary>
  242. /// <returns></returns>
  243. public Color GetDarkerColor ()
  244. {
  245. // TODO: This is a temporary implementation; just enough to show how it could work.
  246. var hsl = ColorHelper.ColorConverter.RgbToHsl (new RGB (R, G, B));
  247. var amount = .3;
  248. if (hsl.L <= 5)
  249. {
  250. return DarkGray;
  251. }
  252. hsl.L = (byte)(hsl.L * amount);
  253. var rgb = ColorHelper.ColorConverter.HslToRgb (hsl);
  254. return new (rgb.R, rgb.G, rgb.B);
  255. }
  256. #region Legacy Color Names
  257. /// <summary>The black color.</summary>
  258. public const ColorName16 Black = ColorName16.Black;
  259. /// <summary>The blue color.</summary>
  260. public const ColorName16 Blue = ColorName16.Blue;
  261. /// <summary>The green color.</summary>
  262. public const ColorName16 Green = ColorName16.Green;
  263. /// <summary>The cyan color.</summary>
  264. public const ColorName16 Cyan = ColorName16.Cyan;
  265. /// <summary>The red color.</summary>
  266. public const ColorName16 Red = ColorName16.Red;
  267. /// <summary>The magenta color.</summary>
  268. public const ColorName16 Magenta = ColorName16.Magenta;
  269. /// <summary>The yellow color.</summary>
  270. public const ColorName16 Yellow = ColorName16.Yellow;
  271. /// <summary>The gray color.</summary>
  272. public const ColorName16 Gray = ColorName16.Gray;
  273. /// <summary>The dark gray color.</summary>
  274. public const ColorName16 DarkGray = ColorName16.DarkGray;
  275. /// <summary>The bright bBlue color.</summary>
  276. public const ColorName16 BrightBlue = ColorName16.BrightBlue;
  277. /// <summary>The bright green color.</summary>
  278. public const ColorName16 BrightGreen = ColorName16.BrightGreen;
  279. /// <summary>The bright cyan color.</summary>
  280. public const ColorName16 BrightCyan = ColorName16.BrightCyan;
  281. /// <summary>The bright red color.</summary>
  282. public const ColorName16 BrightRed = ColorName16.BrightRed;
  283. /// <summary>The bright magenta color.</summary>
  284. public const ColorName16 BrightMagenta = ColorName16.BrightMagenta;
  285. /// <summary>The bright yellow color.</summary>
  286. public const ColorName16 BrightYellow = ColorName16.BrightYellow;
  287. /// <summary>The White color.</summary>
  288. public const ColorName16 White = ColorName16.White;
  289. #endregion
  290. }