Color.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. namespace Terminal.Gui;
  10. /// <summary>Represents a 24-bit color encoded in ARGB32 format.
  11. /// <para />
  12. /// </summary>
  13. /// <seealso cref="Attribute" />
  14. /// <seealso cref="ColorExtensions" />
  15. /// <seealso cref="ColorName" />
  16. [JsonConverter (typeof (ColorJsonConverter))]
  17. [StructLayout (LayoutKind.Explicit)]
  18. public readonly partial record struct Color : ISpanParsable<Color>, IUtf8SpanParsable<Color>, ISpanFormattable, IUtf8SpanFormattable, IMinMaxValue<Color> {
  19. /// <summary>The value of the alpha channel component</summary>
  20. /// <remarks>
  21. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
  22. /// </remarks>
  23. [JsonIgnore]
  24. [field: FieldOffset (3)]
  25. public readonly byte A;
  26. /// <summary>
  27. /// The value of this <see cref="Color" /> as a <see langword="uint" /> in ARGB32 format.
  28. /// </summary>
  29. /// <remarks>
  30. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
  31. /// </remarks>
  32. [JsonIgnore]
  33. [field: FieldOffset (0)]
  34. public readonly uint Argb;
  35. /// <summary>The value of the blue color component.</summary>
  36. [JsonIgnore]
  37. [field: FieldOffset (0)]
  38. public readonly byte B;
  39. /// <summary>The value of the green color component.</summary>
  40. [JsonIgnore]
  41. [field: FieldOffset (1)]
  42. public readonly byte G;
  43. /// <summary>The value of the red color component.</summary>
  44. [JsonIgnore]
  45. [field: FieldOffset (2)]
  46. public readonly byte R;
  47. /// <summary>
  48. /// The value of this <see cref="Color" /> encoded as a signed 32-bit integer in ARGB32 format.
  49. /// </summary>
  50. [JsonIgnore]
  51. [field: FieldOffset (0)]
  52. public readonly int Rgba;
  53. /// <summary>
  54. /// Initializes a new instance of the <see cref="Color" /> <see langword="struct" /> using the supplied component values.
  55. /// </summary>
  56. /// <param name="red">The red 8-bits.</param>
  57. /// <param name="green">The green 8-bits.</param>
  58. /// <param name="blue">The blue 8-bits.</param>
  59. /// <param name="alpha">Optional; defaults to 0xFF. The Alpha channel is not supported by Terminal.Gui.</param>
  60. /// <remarks>Alpha channel is not currently supported by Terminal.Gui.</remarks>
  61. /// <exception cref="OverflowException">If the value of any parameter is greater than <see cref="byte.MaxValue" />.</exception>
  62. /// <exception cref="ArgumentOutOfRangeException">If the value of any parameter is negative.</exception>
  63. public Color (int red = 0, int green = 0, int blue = 0, int alpha = byte.MaxValue)
  64. {
  65. ArgumentOutOfRangeException.ThrowIfNegative (red, nameof (red));
  66. ArgumentOutOfRangeException.ThrowIfNegative (green, nameof (green));
  67. ArgumentOutOfRangeException.ThrowIfNegative (blue, nameof (blue));
  68. ArgumentOutOfRangeException.ThrowIfNegative (alpha, nameof (alpha));
  69. A = Convert.ToByte (alpha);
  70. R = Convert.ToByte (red);
  71. G = Convert.ToByte (green);
  72. B = Convert.ToByte (blue);
  73. }
  74. /// <summary>
  75. /// Initializes a new instance of the <see cref="Color" /> class with an encoded signed 32-bit color value in ARGB32 format.
  76. /// </summary>
  77. /// <param name="rgba">The encoded 32-bit color value (see <see cref="Rgba" />).</param>
  78. /// <remarks>
  79. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
  80. /// </remarks>
  81. public Color (int rgba)
  82. {
  83. Rgba = rgba;
  84. }
  85. /// <summary>
  86. /// Initializes a new instance of the <see cref="Color" /> class with an encoded unsigned 32-bit color value in ARGB32 format.
  87. /// </summary>
  88. /// <param name="argb">The encoded unsigned 32-bit color value (see <see cref="Argb" />).</param>
  89. /// <remarks>
  90. /// The alpha channel is not currently supported, so the value of the alpha channel bits will not affect rendering.
  91. /// </remarks>
  92. public Color (uint argb)
  93. {
  94. Argb = argb;
  95. }
  96. /// <summary>
  97. /// Initializes a new instance of the <see cref="Color" /> color from a legacy 16-color named value.
  98. /// </summary>
  99. /// <param name="colorName">The 16-color value.</param>
  100. public Color (in ColorName colorName)
  101. {
  102. this = ColorExtensions.ColorNameToColorMap [colorName];
  103. }
  104. /// <summary>
  105. /// Initializes a new instance of the <see cref="Color" /> color from string. See <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 characters.
  111. /// </exception>
  112. /// <exception cref="ColorParseException">If thrown by <see cref="Parse(string?,System.IFormatProvider?)" /></exception>
  113. public Color (string colorString)
  114. {
  115. ArgumentException.ThrowIfNullOrWhiteSpace (colorString, nameof (colorString));
  116. this = Parse (colorString, CultureInfo.InvariantCulture);
  117. }
  118. /// <summary>
  119. /// Initializes a new instance of the <see cref="Color" /> with all channels set to 0.
  120. /// </summary>
  121. public Color ()
  122. {
  123. Argb = 0u;
  124. }
  125. /// <summary>
  126. /// Gets or sets the 3-byte/6-character hexadecimal value for each of the legacy 16-color values.
  127. /// </summary>
  128. [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
  129. public static Dictionary<ColorName, string> Colors {
  130. get =>
  131. // Transform _colorToNameMap into a Dictionary<ColorNames,string>
  132. ColorExtensions.ColorToNameMap.ToDictionary (static kvp => kvp.Value, static kvp => kvp.Key.ToString ("g"));
  133. set {
  134. // Transform Dictionary<ColorNames,string> into _colorToNameMap
  135. ColorExtensions.ColorToNameMap = value.ToFrozenDictionary (GetColorToNameMapKey, GetColorToNameMapValue);
  136. return;
  137. static Color GetColorToNameMapKey (KeyValuePair<ColorName, string> kvp) => new Color (kvp.Value);
  138. static ColorName GetColorToNameMapValue (KeyValuePair<ColorName, string> kvp) => Enum.TryParse<ColorName> (kvp.Key.ToString (), true, out var colorName) ? colorName : throw new ArgumentException ($"Invalid color name: {kvp.Key}");
  139. }
  140. }
  141. /// <summary>
  142. /// Gets the <see cref="Color" /> using a legacy 16-color <see cref="ColorName" /> value. <see langword="get" /> will return the closest 16
  143. /// color match to the true color when no exact value is found.
  144. /// </summary>
  145. /// <remarks>
  146. /// Get returns the <see cref="GetClosestNamedColor" /> of the closest 24-bit color value. Set sets the RGB value using a hard-coded map.
  147. /// </remarks>
  148. public AnsiColorCode GetAnsiColorCode () => ColorExtensions.ColorNameToAnsiColorMap [GetClosestNamedColor ()];
  149. /// <summary>
  150. /// Gets the <see cref="Color" /> using a legacy 16-color <see cref="Gui.ColorName" /> value. <see langword="get" /> will return the closest
  151. /// 16 color match to the true color when no exact value is found.
  152. /// </summary>
  153. /// <remarks>
  154. /// Get returns the <see cref="GetClosestNamedColor" /> of the closest 24-bit color value. Set sets the RGB value using a hard-coded map.
  155. /// </remarks>
  156. public ColorName GetClosestNamedColor () => GetClosestNamedColor (this);
  157. /// <summary>
  158. /// Determines if the closest named <see cref="Color" /> to <see langword="this" /> is the provided <paramref name="namedColor" />.
  159. /// </summary>
  160. /// <param name="namedColor">
  161. /// The <see cref="GetClosestNamedColor" /> to check if this <see cref="Color" /> is closer to than any other configured named color.
  162. /// </param>
  163. /// <returns>
  164. /// <see langword="true" /> if the closest named color is the provided value.
  165. /// <br />
  166. /// <see langword="false" /> if any other named color is closer to this <see cref="Color" /> than <paramref name="namedColor" />.
  167. /// </returns>
  168. /// <remarks>
  169. /// If <see langword="this" /> is equidistant from two named colors, the result of this method is not guaranteed to be determinate.
  170. /// </remarks>
  171. [Pure]
  172. [MethodImpl (MethodImplOptions.AggressiveInlining)]
  173. public bool IsClosestToNamedColor (in ColorName namedColor) => GetClosestNamedColor () == namedColor;
  174. /// <summary>
  175. /// Determines if the closest named <see cref="Color" /> to <paramref name="color" />/> is the provided <paramref name="namedColor" />.
  176. /// </summary>
  177. /// <param name="color">
  178. /// The color to test against the <see cref="GetClosestNamedColor" /> value in <paramref name="namedColor" />.
  179. /// </param>
  180. /// <param name="namedColor">
  181. /// The <see cref="GetClosestNamedColor" /> to check if this <see cref="Color" /> is closer to than any other configured named color.
  182. /// </param>
  183. /// <returns>
  184. /// <see langword="true" /> if the closest named color to <paramref name="color" /> is the provided value.
  185. /// <br />
  186. /// <see langword="false" /> if any other named color is closer to <paramref name="color" /> than <paramref name="namedColor" />.
  187. /// </returns>
  188. /// <remarks>
  189. /// If <paramref name="color" /> is equidistant from two named colors, the result of this method is not guaranteed to be determinate.
  190. /// </remarks>
  191. [Pure]
  192. [MethodImpl (MethodImplOptions.AggressiveInlining)]
  193. public static bool IsColorClosestToNamedColor (in Color color, in ColorName namedColor)
  194. {
  195. return color.IsClosestToNamedColor (in namedColor);
  196. }
  197. /// <summary>Gets the "closest" named color to this <see cref="Color" /> value.</summary>
  198. /// <param name="inputColor"></param>
  199. /// <remarks>
  200. /// Distance is defined here as the Euclidean distance between each color interpreted as a <see cref="Vector3" />.
  201. /// <para />
  202. /// The order of the values in the passed Vector3 must be
  203. /// </remarks>
  204. /// <returns></returns>
  205. [SkipLocalsInit]
  206. internal static ColorName GetClosestNamedColor (Color inputColor) => ColorExtensions.ColorToNameMap.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value;
  207. [SkipLocalsInit]
  208. static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) => Vector4.Distance (color1, color2);
  209. #region Legacy Color Names
  210. /// <summary>The black color.</summary>
  211. public const ColorName Black = ColorName.Black;
  212. /// <summary>The blue color.</summary>
  213. public const ColorName Blue = ColorName.Blue;
  214. /// <summary>The green color.</summary>
  215. public const ColorName Green = ColorName.Green;
  216. /// <summary>The cyan color.</summary>
  217. public const ColorName Cyan = ColorName.Cyan;
  218. /// <summary>The red color.</summary>
  219. public const ColorName Red = ColorName.Red;
  220. /// <summary>The magenta color.</summary>
  221. public const ColorName Magenta = ColorName.Magenta;
  222. /// <summary>The yellow color.</summary>
  223. public const ColorName Yellow = ColorName.Yellow;
  224. /// <summary>The gray color.</summary>
  225. public const ColorName Gray = ColorName.Gray;
  226. /// <summary>The dark gray color.</summary>
  227. public const ColorName DarkGray = ColorName.DarkGray;
  228. /// <summary>The bright bBlue color.</summary>
  229. public const ColorName BrightBlue = ColorName.BrightBlue;
  230. /// <summary>The bright green color.</summary>
  231. public const ColorName BrightGreen = ColorName.BrightGreen;
  232. /// <summary>The bright cyan color.</summary>
  233. public const ColorName BrightCyan = ColorName.BrightCyan;
  234. /// <summary>The bright red color.</summary>
  235. public const ColorName BrightRed = ColorName.BrightRed;
  236. /// <summary>The bright magenta color.</summary>
  237. public const ColorName BrightMagenta = ColorName.BrightMagenta;
  238. /// <summary>The bright yellow color.</summary>
  239. public const ColorName BrightYellow = ColorName.BrightYellow;
  240. /// <summary>The White color.</summary>
  241. public const ColorName White = ColorName.White;
  242. #endregion
  243. }