2
0

Color.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. #nullable enable
  2. using System.Collections.Frozen;
  3. using System.Diagnostics.Contracts;
  4. using System.Globalization;
  5. using System.Numerics;
  6. using System.Runtime.CompilerServices;
  7. using System.Runtime.InteropServices;
  8. using System.Text.Json.Serialization;
  9. using ColorHelper;
  10. namespace Terminal.Gui;
  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="ColorName"/>
  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 ColorName colorName) { this = ColorExtensions.ColorNameToColorMap [colorName]; }
  100. /// <summary>
  101. /// Initializes a new instance of the <see cref="Color"/> color from string. See
  102. /// <see cref="TryParse(string, out Color?)"/> for details.
  103. /// </summary>
  104. /// <param name="colorString"></param>
  105. /// <exception cref="ArgumentNullException">If <paramref name="colorString"/> is <see langword="null"/>.</exception>
  106. /// <exception cref="ArgumentException">
  107. /// If <paramref name="colorString"/> is an empty string or consists of only whitespace
  108. /// characters.
  109. /// </exception>
  110. /// <exception cref="ColorParseException">If thrown by <see cref="Parse(string?,System.IFormatProvider?)"/></exception>
  111. public Color (string colorString)
  112. {
  113. ArgumentException.ThrowIfNullOrWhiteSpace (colorString, nameof (colorString));
  114. this = Parse (colorString, CultureInfo.InvariantCulture);
  115. }
  116. /// <summary>Initializes a new instance of the <see cref="Color"/> with all channels set to 0.</summary>
  117. public Color () { Argb = 0u; }
  118. /// <summary>Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values.</summary>
  119. [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  120. public static Dictionary<ColorName, string> Colors
  121. {
  122. get =>
  123. // Transform _colorToNameMap into a Dictionary<ColorNames,string>
  124. ColorExtensions.ColorToNameMap.ToDictionary (static kvp => kvp.Value, static kvp => kvp.Key.ToString ("g"));
  125. set
  126. {
  127. // Transform Dictionary<ColorNames,string> into _colorToNameMap
  128. ColorExtensions.ColorToNameMap = value.ToFrozenDictionary (GetColorToNameMapKey, GetColorToNameMapValue);
  129. return;
  130. static Color GetColorToNameMapKey (KeyValuePair<ColorName, string> kvp) { return new Color (kvp.Value); }
  131. static ColorName GetColorToNameMapValue (KeyValuePair<ColorName, string> kvp)
  132. {
  133. return Enum.TryParse (kvp.Key.ToString (), true, out ColorName colorName)
  134. ? colorName
  135. : throw new ArgumentException ($"Invalid color name: {kvp.Key}");
  136. }
  137. }
  138. }
  139. /// <summary>
  140. /// Gets the <see cref="Color"/> using a legacy 16-color <see cref="ColorName"/> value. <see langword="get"/> will
  141. /// return the closest 16 color match to the true color when no exact value is found.
  142. /// </summary>
  143. /// <remarks>
  144. /// Get returns the <see cref="GetClosestNamedColor (Color)"/> of the closest 24-bit color value. Set sets the RGB
  145. /// value using a hard-coded map.
  146. /// </remarks>
  147. public AnsiColorCode GetAnsiColorCode () { return ColorExtensions.ColorNameToAnsiColorMap [GetClosestNamedColor ()]; }
  148. /// <summary>
  149. /// Gets the <see cref="Color"/> using a legacy 16-color <see cref="Gui.ColorName"/> value. <see langword="get"/>
  150. /// will return the closest 16 color match to the true color when no exact value is found.
  151. /// </summary>
  152. /// <remarks>
  153. /// Get returns the <see cref="GetClosestNamedColor (Color)"/> of the closest 24-bit color value. Set sets the RGB
  154. /// value using a hard-coded map.
  155. /// </remarks>
  156. public ColorName GetClosestNamedColor () { return GetClosestNamedColor (this); }
  157. /// <summary>
  158. /// Determines if the closest named <see cref="Color"/> to <see langword="this"/> is the provided
  159. /// <paramref name="namedColor"/>.
  160. /// </summary>
  161. /// <param name="namedColor">
  162. /// The <see cref="GetClosestNamedColor (Color)"/> to check if this <see cref="Color"/> is closer
  163. /// to than any other configured named color.
  164. /// </param>
  165. /// <returns>
  166. /// <see langword="true"/> if the closest named color is the provided value. <br/> <see langword="false"/> if any
  167. /// other named color is closer to this <see cref="Color"/> than <paramref name="namedColor"/>.
  168. /// </returns>
  169. /// <remarks>
  170. /// If <see langword="this"/> is equidistant from two named colors, the result of this method is not guaranteed to
  171. /// be determinate.
  172. /// </remarks>
  173. [Pure]
  174. [MethodImpl (MethodImplOptions.AggressiveInlining)]
  175. public bool IsClosestToNamedColor (in ColorName namedColor) { return GetClosestNamedColor () == namedColor; }
  176. /// <summary>
  177. /// Determines if the closest named <see cref="Color"/> to <paramref name="color"/>/> is the provided
  178. /// <paramref name="namedColor"/>.
  179. /// </summary>
  180. /// <param name="color">
  181. /// The color to test against the <see cref="GetClosestNamedColor (Color)"/> value in
  182. /// <paramref name="namedColor"/>.
  183. /// </param>
  184. /// <param name="namedColor">
  185. /// The <see cref="GetClosestNamedColor (Color)"/> to check if this <see cref="Color"/> is closer
  186. /// to than any other configured named color.
  187. /// </param>
  188. /// <returns>
  189. /// <see langword="true"/> if the closest named color to <paramref name="color"/> is the provided value. <br/>
  190. /// <see langword="false"/> if any other named color is closer to <paramref name="color"/> than
  191. /// <paramref name="namedColor"/>.
  192. /// </returns>
  193. /// <remarks>
  194. /// If <paramref name="color"/> is equidistant from two named colors, the result of this method is not guaranteed
  195. /// to be determinate.
  196. /// </remarks>
  197. [Pure]
  198. [MethodImpl (MethodImplOptions.AggressiveInlining)]
  199. public static bool IsColorClosestToNamedColor (in Color color, in ColorName namedColor) { return color.IsClosestToNamedColor (in namedColor); }
  200. /// <summary>Gets the "closest" named color to this <see cref="Color"/> value.</summary>
  201. /// <param name="inputColor"></param>
  202. /// <remarks>
  203. /// Distance is defined here as the Euclidean distance between each color interpreted as a <see cref="Vector3"/>.
  204. /// <para/>
  205. /// The order of the values in the passed Vector3 must be
  206. /// </remarks>
  207. /// <returns></returns>
  208. [SkipLocalsInit]
  209. internal static ColorName GetClosestNamedColor (Color inputColor)
  210. {
  211. return ColorExtensions.ColorToNameMap.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
  212. }
  213. [SkipLocalsInit]
  214. private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); }
  215. /// <summary>
  216. /// Gets a color that is the same hue as the current color, but with a different lightness.
  217. /// </summary>
  218. /// <returns></returns>
  219. public Color GetHighlightColor ()
  220. {
  221. // TODO: This is a temporary implementation; just enough to show how it could work.
  222. var hsl = ColorHelper.ColorConverter.RgbToHsl(new RGB (R, G, B));
  223. var amount = .7;
  224. if (hsl.L <= 5)
  225. {
  226. return DarkGray;
  227. }
  228. hsl.L = (byte)(hsl.L * amount);
  229. var rgb = ColorHelper.ColorConverter.HslToRgb (hsl);
  230. return new (rgb.R, rgb.G, rgb.B);
  231. }
  232. /// <summary>
  233. /// Gets a color that is the same hue as the current color, but with a different lightness.
  234. /// </summary>
  235. /// <returns></returns>
  236. public Color GetDarkerColor ()
  237. {
  238. // TODO: This is a temporary implementation; just enough to show how it could work.
  239. var hsl = ColorHelper.ColorConverter.RgbToHsl (new RGB (R, G, B));
  240. var amount = .3;
  241. if (hsl.L <= 5)
  242. {
  243. return DarkGray;
  244. }
  245. hsl.L = (byte)(hsl.L * amount);
  246. var rgb = ColorHelper.ColorConverter.HslToRgb (hsl);
  247. return new (rgb.R, rgb.G, rgb.B);
  248. }
  249. #region Legacy Color Names
  250. /// <summary>The black color.</summary>
  251. public const ColorName Black = ColorName.Black;
  252. /// <summary>The blue color.</summary>
  253. public const ColorName Blue = ColorName.Blue;
  254. /// <summary>The green color.</summary>
  255. public const ColorName Green = ColorName.Green;
  256. /// <summary>The cyan color.</summary>
  257. public const ColorName Cyan = ColorName.Cyan;
  258. /// <summary>The red color.</summary>
  259. public const ColorName Red = ColorName.Red;
  260. /// <summary>The magenta color.</summary>
  261. public const ColorName Magenta = ColorName.Magenta;
  262. /// <summary>The yellow color.</summary>
  263. public const ColorName Yellow = ColorName.Yellow;
  264. /// <summary>The gray color.</summary>
  265. public const ColorName Gray = ColorName.Gray;
  266. /// <summary>The dark gray color.</summary>
  267. public const ColorName DarkGray = ColorName.DarkGray;
  268. /// <summary>The bright bBlue color.</summary>
  269. public const ColorName BrightBlue = ColorName.BrightBlue;
  270. /// <summary>The bright green color.</summary>
  271. public const ColorName BrightGreen = ColorName.BrightGreen;
  272. /// <summary>The bright cyan color.</summary>
  273. public const ColorName BrightCyan = ColorName.BrightCyan;
  274. /// <summary>The bright red color.</summary>
  275. public const ColorName BrightRed = ColorName.BrightRed;
  276. /// <summary>The bright magenta color.</summary>
  277. public const ColorName BrightMagenta = ColorName.BrightMagenta;
  278. /// <summary>The bright yellow color.</summary>
  279. public const ColorName BrightYellow = ColorName.BrightYellow;
  280. /// <summary>The White color.</summary>
  281. public const ColorName White = ColorName.White;
  282. #endregion
  283. }