Color.Formatting.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. #nullable enable
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Diagnostics.Contracts;
  4. using System.Globalization;
  5. using System.Runtime.CompilerServices;
  6. namespace Terminal.Gui;
  7. public readonly partial record struct Color
  8. {
  9. /// <inheritdoc cref="object.ToString"/>
  10. /// <summary>
  11. /// Returns a <see langword="string"/> representation of the current <see cref="Color"/> value, according to the
  12. /// provided <paramref name="formatString"/> and optional <paramref name="formatProvider"/>.
  13. /// </summary>
  14. /// <param name="formatString">
  15. /// A format string that will be passed to
  16. /// <see cref="string.Format(System.IFormatProvider?,string,object?[])"/>.
  17. /// <para/>
  18. /// See remarks for parameters passed to that method.
  19. /// </param>
  20. /// <param name="formatProvider">
  21. /// An optional <see cref="IFormatProvider"/> to use when formatting the <see cref="Color"/>
  22. /// using custom format strings not specified for this method. Provides this instance as <see cref="Argb"/>. <br/> If
  23. /// this parameter is not null, the specified <see cref="IFormatProvider"/> will be used instead of the custom
  24. /// formatting provided by the <see cref="Color"/> type.
  25. /// <para/>
  26. /// See remarks for defined format strings.
  27. /// </param>
  28. /// <remarks>
  29. /// Pre-defined format strings for this method, if a custom <paramref name="formatProvider"/> is not supplied are:
  30. /// <list type="bullet">
  31. /// <listheader>
  32. /// <term>Value</term> <description>Result</description>
  33. /// </listheader>
  34. /// <item>
  35. /// <term>g or null or empty string</term>
  36. /// <description>
  37. /// General/default format - Returns a named <see cref="Color"/> if there is a match, or a
  38. /// 24-bit/3-byte/6-hex digit string in "#RRGGBB" format.
  39. /// </description>
  40. /// </item>
  41. /// <item>
  42. /// <term>G</term>
  43. /// <description>
  44. /// Extended general format - Returns a named <see cref="Color"/> if there is a match, or a
  45. /// 32-bit/4-byte/8-hex digit string in "#AARRGGBB" format.
  46. /// </description>
  47. /// </item>
  48. /// <item>
  49. /// <term>d</term>
  50. /// <description>
  51. /// Decimal format - Returns a 3-component decimal representation of the <see cref="Color"/> in
  52. /// "rgb(R,G,B)" format.
  53. /// </description>
  54. /// </item>
  55. /// <item>
  56. /// <term>D</term>
  57. /// <description>
  58. /// Extended decimal format - Returns a 4-component decimal representation of the
  59. /// <see cref="Color"/> in "rgba(R,G,B,A)" format.
  60. /// </description>
  61. /// </item>
  62. /// </list>
  63. /// <para>
  64. /// If <paramref name="formatProvider"/> is provided and is a non-null <see cref="ICustomColorFormatter"/>, the
  65. /// following behaviors are available, for the specified values of <paramref name="formatString"/>:
  66. /// <list type="bullet">
  67. /// <listheader>
  68. /// <term>Value</term> <description>Result</description>
  69. /// </listheader>
  70. /// <item>
  71. /// <term>null or empty string</term>
  72. /// <description>
  73. /// Calls <see cref="ICustomColorFormatter.Format(string?,byte,byte,byte,byte)"/> on the
  74. /// provided <paramref name="formatProvider"/> with the null string, and <see cref="R"/>,
  75. /// <see cref="G"/>, <see cref="B"/>, and <see cref="A"/> as typed arguments of type <see cref="Byte"/>
  76. /// .
  77. /// </description>
  78. /// </item>
  79. /// <item>
  80. /// <term>All other values</term>
  81. /// <description>
  82. /// Calls <see cref="string.Format{TArg0}"/> with the provided
  83. /// <paramref name="formatProvider"/> and <paramref name="formatString"/> (parsed as a
  84. /// <see cref="CompositeFormat"/>), with the value of <see cref="Argb"/> as the sole
  85. /// <see langword="uint"/>-typed argument.
  86. /// </description>
  87. /// </item>
  88. /// </list>
  89. /// </para>
  90. /// </remarks>
  91. [SkipLocalsInit]
  92. public string ToString (
  93. [StringSyntax (StringSyntaxAttribute.CompositeFormat)] string? formatString,
  94. IFormatProvider? formatProvider = null
  95. )
  96. {
  97. return (formatString, formatProvider) switch
  98. {
  99. // Null or empty string and null formatProvider - Revert to 'g' case behavior
  100. (null or { Length: 0 }, null) => ToString (),
  101. // Null or empty string and formatProvider is an ICustomColorFormatter - Output according to the given ICustomColorFormatted, with R, G, B, and A as typed arguments
  102. (null or { Length: 0 }, ICustomColorFormatter ccf) => ccf.Format (null, R, G, B, A),
  103. // Null or empty string and formatProvider is otherwise non-null but not the invariant culture - Output according to string.Format with the given IFormatProvider and R, G, B, and A as boxed arguments, with string.Empty as the format string
  104. (null or { Length: 0 }, { }) when !Equals (formatProvider, CultureInfo.InvariantCulture) =>
  105. string.Format (formatProvider, formatString ?? string.Empty, R, G, B, A),
  106. // Null or empty string and formatProvider is the invariant culture - Output according to string.Format with the given IFormatProvider and R, G, B, and A as boxed arguments, with string.Empty as the format string
  107. (null or { Length: 0 }, { }) when Equals (formatProvider, CultureInfo.InvariantCulture) =>
  108. $"#{R:X2}{G:X2}{B:X2}",
  109. // Non-null string and non-null formatProvider - let formatProvider handle it and give it R, G, B, and A
  110. ({ }, { }) => string.Format (formatProvider, CompositeFormat.Parse (formatString), R, G, B, A),
  111. // g format string and null formatProvider - Output as 24-bit hex according to invariant culture rules from R, G, and B
  112. (['g'], null) => ToString (),
  113. // G format string and null formatProvider - Output as 32-bit hex according to invariant culture rules from Argb
  114. (['G'], null) => $"#{A:X2}{R:X2}{G:X2}{B:X2}",
  115. // d format string and null formatProvider - Output as 24-bit decimal rgb(r,g,b) according to invariant culture rules from R, G, and B
  116. (['d'], null) => $"rgb({R:D},{G:D},{B:D})",
  117. // D format string and null formatProvider - Output as 32-bit decimal rgba(r,g,b,a) according to invariant culture rules from R, G, B, and A. Non-standard: a is a decimal byte value.
  118. (['D'], null) => $"rgba({R:D},{G:D},{B:D},{A:D})",
  119. // All other cases (formatString is not null here) - Delegate to formatProvider, first, and otherwise to invariant culture, and try to format the provided string from the channels
  120. ({ }, _) => string.Format (
  121. formatProvider ?? CultureInfo.InvariantCulture,
  122. CompositeFormat.Parse (formatString),
  123. R,
  124. G,
  125. B,
  126. A
  127. ),
  128. _ => throw new InvalidOperationException (
  129. $"Unable to create string from Color with value {Argb}, using format string {formatString}"
  130. )
  131. }
  132. ?? throw new InvalidOperationException (
  133. $"Unable to create string from Color with value {Argb}, using format string {formatString}"
  134. );
  135. }
  136. /// <inheritdoc/>
  137. /// <remarks>
  138. /// <para>
  139. /// This method should be used only when absolutely necessary, because it <b>always</b> has more overhead than
  140. /// <see cref="ToString(string?,System.IFormatProvider?)"/>, as this method results in an intermediate allocation
  141. /// of one or more instances of <see langword="string"/> and a copy of that string to
  142. /// <paramref name="destination"/> if formatting was successful. <br/> When possible, use
  143. /// <see cref="ToString(string?,System.IFormatProvider?)"/>, which attempts to avoid intermediate allocations.
  144. /// </para>
  145. /// <para>
  146. /// This method only returns <see langword="true"/> and with its output written to <paramref name="destination"/>
  147. /// if the formatted string, <i>in its entirety</i>, will fit in <paramref name="destination"/>. If the resulting
  148. /// formatted string is too large to fit in <paramref name="destination"/>, the result will be false and
  149. /// <paramref name="destination"/> will be unaltered.
  150. /// </para>
  151. /// <para>
  152. /// The resulting formatted string may be <b>shorter</b> than <paramref name="destination"/>. When this method
  153. /// returns <see langword="true"/>, use <paramref name="charsWritten"/> when handling the value of
  154. /// <paramref name="destination"/>.
  155. /// </para>
  156. /// </remarks>
  157. [Pure]
  158. [SkipLocalsInit]
  159. public bool TryFormat (
  160. Span<char> destination,
  161. out int charsWritten,
  162. ReadOnlySpan<char> format,
  163. IFormatProvider? provider
  164. )
  165. {
  166. // TODO: This can probably avoid a string allocation with a little more work
  167. try
  168. {
  169. string formattedString = ToString (format.ToString (), provider);
  170. if (formattedString.Length <= destination.Length)
  171. {
  172. formattedString.CopyTo (destination);
  173. charsWritten = formattedString.Length;
  174. return true;
  175. }
  176. }
  177. catch
  178. {
  179. destination.Clear ();
  180. charsWritten = 0;
  181. return false;
  182. }
  183. destination.Clear ();
  184. charsWritten = 0;
  185. return false;
  186. }
  187. /// <summary>Converts the provided <see langword="string"/> to a new <see cref="Color"/> value.</summary>
  188. /// <param name="text">
  189. /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
  190. /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="Gui.ColorName"/> string values.
  191. /// </param>
  192. /// <param name="formatProvider">
  193. /// If specified and not <see langword="null"/>, will be passed to
  194. /// <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)"/>.
  195. /// </param>
  196. /// <returns>A <see cref="Color"/> value equivalent to <paramref name="text"/>, if parsing was successful.</returns>
  197. /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
  198. /// <exception cref="ArgumentNullException">If <paramref name="text"/> is <see langword="null"/>.</exception>
  199. /// <exception cref="ArgumentException">
  200. /// If <paramref name="text"/> is an empty string or consists of only whitespace
  201. /// characters.
  202. /// </exception>
  203. /// <exception cref="ColorParseException">
  204. /// If thrown by
  205. /// <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)"/>.
  206. /// </exception>
  207. [Pure]
  208. [SkipLocalsInit]
  209. public static Color Parse (string? text, IFormatProvider? formatProvider = null)
  210. {
  211. ArgumentException.ThrowIfNullOrWhiteSpace (text, nameof (text));
  212. if (text is { Length: < 3 } && formatProvider is null)
  213. {
  214. throw new ColorParseException (
  215. text,
  216. reason: "Provided text is too short to be any known color format.",
  217. badValue: text
  218. );
  219. }
  220. return Parse (text.AsSpan (), formatProvider ?? CultureInfo.InvariantCulture);
  221. }
  222. /// <summary>
  223. /// Converts the provided <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to a new <see cref="Color"/>
  224. /// value.
  225. /// </summary>
  226. /// <param name="text">
  227. /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#RGBA", "#AARRGGBB", "rgb(r,g,b)",
  228. /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="Gui.ColorName"/> string values.
  229. /// </param>
  230. /// <param name="formatProvider">
  231. /// Optional <see cref="IFormatProvider"/> to provide parsing services for the input text.
  232. /// <br/> Defaults to <see cref="CultureInfo.InvariantCulture"/> if <see langword="null"/>. <br/> If not null, must
  233. /// implement <see cref="ICustomColorFormatter"/> or will be ignored and <see cref="CultureInfo.InvariantCulture"/>
  234. /// will be used.
  235. /// </param>
  236. /// <returns>A <see cref="Color"/> value equivalent to <paramref name="text"/>, if parsing was successful.</returns>
  237. /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
  238. /// <exception cref="ArgumentException">
  239. /// with an inner <see cref="FormatException"/> if <paramref name="text"/> was unable
  240. /// to be successfully parsed as a <see cref="Color"/>, for any reason.
  241. /// </exception>
  242. [Pure]
  243. [SkipLocalsInit]
  244. public static Color Parse (ReadOnlySpan<char> text, IFormatProvider? formatProvider = null)
  245. {
  246. return text switch
  247. {
  248. // Null string or empty span provided
  249. { IsEmpty: true } when formatProvider is null => throw new ColorParseException (
  250. in text,
  251. "The text provided was null or empty.",
  252. in text
  253. ),
  254. // A valid ICustomColorFormatter was specified and the text wasn't null or empty
  255. { IsEmpty: false } when formatProvider is ICustomColorFormatter f => f.Parse (text),
  256. // Input string is only whitespace
  257. { Length: > 0 } when text.IsWhiteSpace () => throw new ColorParseException (
  258. in text,
  259. "The text provided consisted of only whitespace characters.",
  260. in text
  261. ),
  262. // Any string too short to possibly be any supported format.
  263. { Length: > 0 and < 3 } => throw new ColorParseException (
  264. in text,
  265. "Text was too short to be any possible supported format.",
  266. in text
  267. ),
  268. // The various hexadecimal cases
  269. ['#', ..] hexString => hexString switch
  270. {
  271. // #RGB
  272. ['#', var rChar, var gChar, var bChar] chars when chars [1..]
  273. .IsAllAsciiHexDigits () =>
  274. new Color (
  275. byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
  276. byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
  277. byte.Parse ([bChar, bChar], NumberStyles.HexNumber)
  278. ),
  279. // #ARGB
  280. ['#', var aChar, var rChar, var gChar, var bChar] chars when chars [1..]
  281. .IsAllAsciiHexDigits () =>
  282. new Color (
  283. byte.Parse ([rChar, rChar], NumberStyles.HexNumber),
  284. byte.Parse ([gChar, gChar], NumberStyles.HexNumber),
  285. byte.Parse ([bChar, bChar], NumberStyles.HexNumber),
  286. byte.Parse ([aChar, aChar], NumberStyles.HexNumber)
  287. ),
  288. // #RRGGBB
  289. [
  290. '#', var r1Char, var r2Char, var g1Char, var g2Char, var b1Char,
  291. var b2Char
  292. ] chars when chars [1..].IsAllAsciiHexDigits () =>
  293. new Color (
  294. byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
  295. byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
  296. byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber)
  297. ),
  298. // #AARRGGBB
  299. [
  300. '#', var a1Char, var a2Char, var r1Char, var r2Char, var g1Char,
  301. var g2Char, var b1Char, var b2Char
  302. ] chars when chars [1..].IsAllAsciiHexDigits () =>
  303. new Color (
  304. byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber),
  305. byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber),
  306. byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber),
  307. byte.Parse ([a1Char, a2Char], NumberStyles.HexNumber)
  308. ),
  309. _ => throw new ColorParseException (
  310. in hexString,
  311. $"Color hex string {hexString} was not in a supported format",
  312. in hexString
  313. )
  314. },
  315. // rgb(r,g,b) or rgb(r,g,b,a)
  316. ['r', 'g', 'b', '(', .., ')'] => ParseRgbaFormat (in text, 4),
  317. // rgba(r,g,b,a) or rgba(r,g,b)
  318. ['r', 'g', 'b', 'a', '(', .., ')'] => ParseRgbaFormat (in text, 5),
  319. // Attempt to parse as a named color from the ColorName enum
  320. { } when char.IsLetter (text [0]) && Enum.TryParse (text, true, out ColorName colorName) =>
  321. new Color (colorName),
  322. // Any other input
  323. _ => throw new ColorParseException (in text, "Text did not match any expected format.", in text, [])
  324. };
  325. [Pure]
  326. [SkipLocalsInit]
  327. static Color ParseRgbaFormat (in ReadOnlySpan<char> originalString, in int startIndex)
  328. {
  329. ReadOnlySpan<char> valuesSubstring = originalString [startIndex..^1];
  330. Span<Range> valueRanges = stackalloc Range [4];
  331. int rangeCount = valuesSubstring.Split (
  332. valueRanges,
  333. ',',
  334. StringSplitOptions.RemoveEmptyEntries
  335. | StringSplitOptions.TrimEntries
  336. );
  337. switch (rangeCount)
  338. {
  339. case 3:
  340. {
  341. // rgba(r,g,b)
  342. ParseRgbValues (
  343. in valuesSubstring,
  344. in valueRanges,
  345. in originalString,
  346. out ReadOnlySpan<char> rSpan,
  347. out ReadOnlySpan<char> gSpan,
  348. out ReadOnlySpan<char> bSpan
  349. );
  350. return new Color (int.Parse (rSpan), int.Parse (gSpan), int.Parse (bSpan));
  351. }
  352. case 4:
  353. {
  354. // rgba(r,g,b,a)
  355. ParseRgbValues (
  356. in valuesSubstring,
  357. in valueRanges,
  358. in originalString,
  359. out ReadOnlySpan<char> rSpan,
  360. out ReadOnlySpan<char> gSpan,
  361. out ReadOnlySpan<char> bSpan
  362. );
  363. ReadOnlySpan<char> aSpan = valuesSubstring [valueRanges [3]];
  364. if (!aSpan.IsAllAsciiDigits ())
  365. {
  366. throw new ColorParseException (
  367. in originalString,
  368. "Value was not composed entirely of decimal digits.",
  369. in aSpan,
  370. nameof (A)
  371. );
  372. }
  373. return new Color (int.Parse (rSpan), int.Parse (gSpan), int.Parse (bSpan), int.Parse (aSpan));
  374. }
  375. default:
  376. throw new ColorParseException (
  377. in originalString,
  378. $"Wrong number of values. Expected 3 or 4 decimal integers. Got {rangeCount}.",
  379. in originalString
  380. );
  381. }
  382. [Pure]
  383. [SkipLocalsInit]
  384. static void ParseRgbValues (
  385. in ReadOnlySpan<char> valuesString,
  386. in Span<Range> valueComponentRanges,
  387. in ReadOnlySpan<char> originalString,
  388. out ReadOnlySpan<char> rSpan,
  389. out ReadOnlySpan<char> gSpan,
  390. out ReadOnlySpan<char> bSpan
  391. )
  392. {
  393. rSpan = valuesString [valueComponentRanges [0]];
  394. if (!rSpan.IsAllAsciiDigits ())
  395. {
  396. throw new ColorParseException (
  397. in originalString,
  398. "Value was not composed entirely of decimal digits.",
  399. in rSpan,
  400. nameof (R)
  401. );
  402. }
  403. gSpan = valuesString [valueComponentRanges [1]];
  404. if (!gSpan.IsAllAsciiDigits ())
  405. {
  406. throw new ColorParseException (
  407. in originalString,
  408. "Value was not composed entirely of decimal digits.",
  409. in gSpan,
  410. nameof (G)
  411. );
  412. }
  413. bSpan = valuesString [valueComponentRanges [2]];
  414. if (!bSpan.IsAllAsciiDigits ())
  415. {
  416. throw new ColorParseException (
  417. in originalString,
  418. "Value was not composed entirely of decimal digits.",
  419. in bSpan,
  420. nameof (B)
  421. );
  422. }
  423. }
  424. }
  425. }
  426. /// <summary>Converts the provided <see langword="string"/> to a new <see cref="Color"/> value.</summary>
  427. /// <param name="text">
  428. /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
  429. /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="GetClosestNamedColor (Color)"/> string
  430. /// values.
  431. /// </param>
  432. /// <param name="formatProvider">
  433. /// Optional <see cref="IFormatProvider"/> to provide formatting services for the input text.
  434. /// <br/> Defaults to <see cref="CultureInfo.InvariantCulture"/> if <see langword="null"/>.
  435. /// </param>
  436. /// <param name="result">
  437. /// The parsed value, if successful, or <see langword="default"/>(<see cref="Color"/>), if
  438. /// unsuccessful.
  439. /// </param>
  440. /// <returns>A <see langword="bool"/> value indicating whether parsing was successful.</returns>
  441. /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
  442. [Pure]
  443. [SkipLocalsInit]
  444. public static bool TryParse (string? text, IFormatProvider? formatProvider, out Color result)
  445. {
  446. return TryParse (
  447. text.AsSpan (),
  448. formatProvider ?? CultureInfo.InvariantCulture,
  449. out result
  450. );
  451. }
  452. /// <summary>
  453. /// Converts the provided <see cref="ReadOnlySpan{T}"/> of <see langword="char"/> to a new <see cref="Color"/>
  454. /// value.
  455. /// </summary>
  456. /// <param name="text">
  457. /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
  458. /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="GetClosestNamedColor (Color)"/> string
  459. /// values.
  460. /// </param>
  461. /// <param name="formatProvider">
  462. /// If specified and not <see langword="null"/>, will be passed to
  463. /// <see cref="Parse(System.ReadOnlySpan{char},System.IFormatProvider?)"/>.
  464. /// </param>
  465. /// <param name="color">
  466. /// The parsed value, if successful, or <see langword="default"/>(<see cref="Color"/>), if
  467. /// unsuccessful.
  468. /// </param>
  469. /// <returns>A <see langword="bool"/> value indicating whether parsing was successful.</returns>
  470. /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
  471. [Pure]
  472. [SkipLocalsInit]
  473. public static bool TryParse (ReadOnlySpan<char> text, IFormatProvider? formatProvider, out Color color)
  474. {
  475. try
  476. {
  477. Color c = Parse (text, formatProvider);
  478. color = c;
  479. return true;
  480. }
  481. catch (ColorParseException)
  482. {
  483. color = default (Color);
  484. return false;
  485. }
  486. }
  487. /// <inheritdoc/>
  488. /// <remarks>
  489. /// Use of this method involves a stack allocation of <paramref name="utf8Destination"/>.Length * 2 bytes. Use of
  490. /// the overload taking a char span is recommended.
  491. /// </remarks>
  492. [SkipLocalsInit]
  493. public bool TryFormat (
  494. Span<byte> utf8Destination,
  495. out int bytesWritten,
  496. ReadOnlySpan<char> format,
  497. IFormatProvider? provider
  498. )
  499. {
  500. Span<char> charDestination = stackalloc char [utf8Destination.Length * 2];
  501. if (TryFormat (charDestination, out int charsWritten, format, provider))
  502. {
  503. Encoding.UTF8.GetBytes (charDestination, utf8Destination);
  504. bytesWritten = charsWritten / 2;
  505. return true;
  506. }
  507. utf8Destination.Clear ();
  508. bytesWritten = 0;
  509. return false;
  510. }
  511. /// <inheritdoc/>
  512. [Pure]
  513. [SkipLocalsInit]
  514. public static Color Parse (ReadOnlySpan<byte> utf8Text, IFormatProvider? provider) { return Parse (Encoding.UTF8.GetString (utf8Text), provider); }
  515. /// <inheritdoc/>
  516. [Pure]
  517. [SkipLocalsInit]
  518. public static bool TryParse (ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, out Color result)
  519. {
  520. return TryParse (Encoding.UTF8.GetString (utf8Text), provider, out result);
  521. }
  522. /// <summary>Converts the color to a string representation.</summary>
  523. /// <remarks>
  524. /// <para>If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string.</para>
  525. /// <para><see cref="A"/> (Alpha channel) is ignored and the returned string will not include it for this overload.</para>
  526. /// </remarks>
  527. /// <returns>The string representation of this value in #RRGGBB format.</returns>
  528. [Pure]
  529. [SkipLocalsInit]
  530. public override string ToString ()
  531. {
  532. // If Values has an exact match with a named color (in _colorNames), use that.
  533. return ColorExtensions.ColorToNameMap.TryGetValue (this, out ColorName colorName)
  534. ? Enum.GetName (typeof (ColorName), colorName) ?? $"#{R:X2}{G:X2}{B:X2}"
  535. : // Otherwise return as an RGB hex value.
  536. $"#{R:X2}{G:X2}{B:X2}";
  537. }
  538. /// <summary>Converts the provided string to a new <see cref="Color"/> instance.</summary>
  539. /// <param name="text">
  540. /// The text to analyze. Formats supported are "#RGB", "#RRGGBB", "#ARGB", "#AARRGGBB", "rgb(r,g,b)",
  541. /// "rgb(r,g,b,a)", "rgba(r,g,b)", "rgba(r,g,b,a)", and any of the <see cref="Gui.ColorName"/> string values.
  542. /// </param>
  543. /// <param name="color">The parsed value.</param>
  544. /// <returns>A boolean value indicating whether parsing was successful.</returns>
  545. /// <remarks>While <see cref="Color"/> supports the alpha channel <see cref="A"/>, Terminal.Gui does not.</remarks>
  546. public static bool TryParse (string text, [NotNullWhen (true)] out Color? color)
  547. {
  548. if (TryParse (text.AsSpan (), null, out Color c))
  549. {
  550. color = c;
  551. return true;
  552. }
  553. color = null;
  554. return false;
  555. }
  556. }