Cell.cs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. 
  2. namespace Terminal.Gui.Drawing;
  3. /// <summary>
  4. /// Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and
  5. /// <see cref="IDriver"/>).
  6. /// </summary>
  7. public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, string Grapheme = "")
  8. {
  9. /// <summary>The attributes to use when drawing the Glyph.</summary>
  10. public Attribute? Attribute { get; set; } = Attribute;
  11. /// <summary>
  12. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.Drawing.Cell"/> has been modified since the
  13. /// last time it was drawn.
  14. /// </summary>
  15. public bool IsDirty { get; set; } = IsDirty;
  16. private string _grapheme = Grapheme;
  17. /// <summary>
  18. /// The single grapheme cluster to display from this cell. If <see cref="Grapheme"/> is <see langword="null"/> or
  19. /// <see cref="string.Empty"/>, then <see cref="Cell"/> is ignored.
  20. /// </summary>
  21. public string Grapheme
  22. {
  23. readonly get => _grapheme;
  24. set
  25. {
  26. if (GraphemeHelper.GetGraphemeCount (value) > 1)
  27. {
  28. throw new InvalidOperationException ($"Only a single {nameof (Grapheme)} cluster is allowed per Cell.");
  29. }
  30. if (!string.IsNullOrEmpty (value) && value.Length == 1 && char.IsSurrogate (value [0]))
  31. {
  32. throw new ArgumentException ($"Only valid Unicode scalar values are allowed in a single {nameof (Grapheme)} cluster.");
  33. }
  34. try
  35. {
  36. _grapheme = !string.IsNullOrEmpty (value) && !value.IsNormalized (NormalizationForm.FormC)
  37. ? value.Normalize (NormalizationForm.FormC)
  38. : value;
  39. }
  40. catch (ArgumentException)
  41. {
  42. // leave text unnormalized
  43. _grapheme = value;
  44. }
  45. }
  46. }
  47. /// <summary>
  48. /// The rune for <see cref="Grapheme"/> or runes for <see cref="Grapheme"/> that when combined makes this Cell a combining sequence.
  49. /// </summary>
  50. /// <remarks>
  51. /// In the case where <see cref="Grapheme"/> has more than one rune it is a combining sequence that is normalized to a
  52. /// single Text which may occupies 1 or 2 columns.
  53. /// </remarks>
  54. public IReadOnlyList<Rune> Runes => string.IsNullOrEmpty (Grapheme) ? [] : Grapheme.EnumerateRunes ().ToList ();
  55. /// <inheritdoc/>
  56. public override string ToString ()
  57. {
  58. string visibleText = EscapeControlAndInvisible (Grapheme);
  59. return $"[\"{visibleText}\":{Attribute}]";
  60. }
  61. private static string EscapeControlAndInvisible (string text)
  62. {
  63. if (string.IsNullOrEmpty (text))
  64. {
  65. return "";
  66. }
  67. var sb = new StringBuilder ();
  68. foreach (var rune in text.EnumerateRunes ())
  69. {
  70. switch (rune.Value)
  71. {
  72. case '\0': sb.Append ("␀"); break;
  73. case '\t': sb.Append ("\\t"); break;
  74. case '\r': sb.Append ("\\r"); break;
  75. case '\n': sb.Append ("\\n"); break;
  76. case '\f': sb.Append ("\\f"); break;
  77. case '\v': sb.Append ("\\v"); break;
  78. default:
  79. if (char.IsControl ((char)rune.Value))
  80. {
  81. // show as \uXXXX
  82. sb.Append ($"\\u{rune.Value:X4}");
  83. }
  84. else
  85. {
  86. sb.Append (rune);
  87. }
  88. break;
  89. }
  90. }
  91. return sb.ToString ();
  92. }
  93. /// <summary>Converts the string into a <see cref="List{Cell}"/>.</summary>
  94. /// <param name="str">The string to convert.</param>
  95. /// <param name="attribute">The <see cref="Scheme"/> to use.</param>
  96. /// <returns></returns>
  97. public static List<Cell> ToCellList (string str, Attribute? attribute = null)
  98. {
  99. List<Cell> cells = [];
  100. cells.AddRange (GraphemeHelper.GetGraphemes (str).Select (grapheme => new Cell { Grapheme = grapheme, Attribute = attribute }));
  101. return cells;
  102. }
  103. /// <summary>
  104. /// Splits a string into a List that will contain a <see cref="List{Cell}"/> for each line.
  105. /// </summary>
  106. /// <param name="content">The string content.</param>
  107. /// <param name="attribute">The scheme.</param>
  108. /// <returns>A <see cref="List{Cell}"/> for each line.</returns>
  109. public static List<List<Cell>> StringToLinesOfCells (string content, Attribute? attribute = null)
  110. {
  111. List<Cell> cells = ToCellList (content, attribute);
  112. return SplitNewLines (cells);
  113. }
  114. /// <summary>Converts a <see cref="Cell"/> generic collection into a string.</summary>
  115. /// <param name="cells">The enumerable cell to convert.</param>
  116. /// <returns></returns>
  117. public static string ToString (IEnumerable<Cell> cells)
  118. {
  119. StringBuilder sb = new ();
  120. foreach (Cell cell in cells)
  121. {
  122. sb.Append (cell.Grapheme);
  123. }
  124. return sb.ToString ();
  125. }
  126. /// <summary>Converts a <see cref="List{Cell}"/> generic collection into a string.</summary>
  127. /// <param name="cellsList">The enumerable cell to convert.</param>
  128. /// <returns></returns>
  129. public static string ToString (List<List<Cell>> cellsList)
  130. {
  131. var str = string.Empty;
  132. for (var i = 0; i < cellsList.Count; i++)
  133. {
  134. IEnumerable<Cell> cellList = cellsList [i];
  135. str += ToString (cellList);
  136. if (i + 1 < cellsList.Count)
  137. {
  138. str += Environment.NewLine;
  139. }
  140. }
  141. return str;
  142. }
  143. // Turns the string into cells, this does not split the contents on a newline if it is present.
  144. internal static List<Cell> StringToCells (string str, Attribute? attribute = null)
  145. {
  146. return ToCellList (str, attribute);
  147. }
  148. internal static List<Cell> ToCells (IEnumerable<string> strings, Attribute? attribute = null)
  149. {
  150. StringBuilder sb = new ();
  151. foreach (string str in strings)
  152. {
  153. sb.Append (str);
  154. }
  155. return ToCellList (sb.ToString (), attribute);
  156. }
  157. private static List<List<Cell>> SplitNewLines (List<Cell> cells)
  158. {
  159. List<List<Cell>> lines = [];
  160. int start = 0, i = 0;
  161. var hasCR = false;
  162. // ASCII code 13 = Carriage Return.
  163. // ASCII code 10 = Line Feed.
  164. for (; i < cells.Count; i++)
  165. {
  166. if (cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 13)
  167. {
  168. hasCR = true;
  169. continue;
  170. }
  171. if ((cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 10)
  172. || cells [i].Grapheme == "\r\n")
  173. {
  174. if (i - start > 0)
  175. {
  176. lines.Add (cells.GetRange (start, hasCR ? i - 1 - start : i - start));
  177. }
  178. else
  179. {
  180. lines.Add (StringToCells (string.Empty));
  181. }
  182. start = i + 1;
  183. hasCR = false;
  184. }
  185. }
  186. if (i - start >= 0)
  187. {
  188. lines.Add (cells.GetRange (start, i - start));
  189. }
  190. return lines;
  191. }
  192. /// <summary>
  193. /// Splits a rune cell list into a List that will contain a <see cref="List{Cell}"/> for each line.
  194. /// </summary>
  195. /// <param name="cells">The cells list.</param>
  196. /// <returns></returns>
  197. public static List<List<Cell>> ToCells (List<Cell> cells) { return SplitNewLines (cells); }
  198. }