namespace Terminal.Gui.Drawing; /// /// Represents a single row/column in a Terminal.Gui rendering surface (e.g. and /// ). /// public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, string Grapheme = "") { /// The attributes to use when drawing the Glyph. public Attribute? Attribute { get; set; } = Attribute; /// /// Gets or sets a value indicating whether this has been modified since the /// last time it was drawn. /// public bool IsDirty { get; set; } = IsDirty; private string _grapheme = Grapheme; /// /// The single grapheme cluster to display from this cell. If is or /// , then is ignored. /// public string Grapheme { readonly get => _grapheme; set { if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1) { throw new InvalidOperationException ($"Only a single {nameof (Grapheme)} cluster is allowed per Cell."); } if (!string.IsNullOrEmpty (value) && value.Length == 1 && char.IsSurrogate (value [0])) { throw new ArgumentException ($"Only valid Unicode scalar values are allowed in a single {nameof (Grapheme)} cluster."); } try { _grapheme = !string.IsNullOrEmpty (value) && !value.IsNormalized (NormalizationForm.FormC) ? value.Normalize (NormalizationForm.FormC) : value; } catch (ArgumentException) { // leave text unnormalized _grapheme = value; } } } /// /// The rune for or runes for that when combined makes this Cell a combining sequence. /// /// /// In the case where has more than one rune it is a combining sequence that is normalized to a /// single Text which may occupies 1 or 2 columns. /// public IReadOnlyList Runes => string.IsNullOrEmpty (Grapheme) ? [] : Grapheme.EnumerateRunes ().ToList (); /// public override string ToString () { string visibleText = EscapeControlAndInvisible (Grapheme); return $"[\"{visibleText}\":{Attribute}]"; } private static string EscapeControlAndInvisible (string text) { if (string.IsNullOrEmpty (text)) { return ""; } var sb = new StringBuilder (); foreach (var rune in text.EnumerateRunes ()) { switch (rune.Value) { case '\0': sb.Append ("␀"); break; case '\t': sb.Append ("\\t"); break; case '\r': sb.Append ("\\r"); break; case '\n': sb.Append ("\\n"); break; case '\f': sb.Append ("\\f"); break; case '\v': sb.Append ("\\v"); break; default: if (char.IsControl ((char)rune.Value)) { // show as \uXXXX sb.Append ($"\\u{rune.Value:X4}"); } else { sb.Append (rune); } break; } } return sb.ToString (); } /// Converts the string into a . /// The string to convert. /// The to use. /// public static List ToCellList (string str, Attribute? attribute = null) { List cells = []; cells.AddRange (GraphemeHelper.GetGraphemes (str).Select (grapheme => new Cell { Grapheme = grapheme, Attribute = attribute })); return cells; } /// /// Splits a string into a List that will contain a for each line. /// /// The string content. /// The scheme. /// A for each line. public static List> StringToLinesOfCells (string content, Attribute? attribute = null) { List cells = ToCellList (content, attribute); return SplitNewLines (cells); } /// Converts a generic collection into a string. /// The enumerable cell to convert. /// public static string ToString (IEnumerable cells) { StringBuilder sb = new (); foreach (Cell cell in cells) { sb.Append (cell.Grapheme); } return sb.ToString (); } /// Converts a generic collection into a string. /// The enumerable cell to convert. /// public static string ToString (List> cellsList) { var str = string.Empty; for (var i = 0; i < cellsList.Count; i++) { IEnumerable cellList = cellsList [i]; str += ToString (cellList); if (i + 1 < cellsList.Count) { str += Environment.NewLine; } } return str; } // Turns the string into cells, this does not split the contents on a newline if it is present. internal static List StringToCells (string str, Attribute? attribute = null) { return ToCellList (str, attribute); } internal static List ToCells (IEnumerable strings, Attribute? attribute = null) { StringBuilder sb = new (); foreach (string str in strings) { sb.Append (str); } return ToCellList (sb.ToString (), attribute); } private static List> SplitNewLines (List cells) { List> lines = []; int start = 0, i = 0; var hasCR = false; // ASCII code 13 = Carriage Return. // ASCII code 10 = Line Feed. for (; i < cells.Count; i++) { if (cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 13) { hasCR = true; continue; } if ((cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 10) || cells [i].Grapheme == "\r\n") { if (i - start > 0) { lines.Add (cells.GetRange (start, hasCR ? i - 1 - start : i - start)); } else { lines.Add (StringToCells (string.Empty)); } start = i + 1; hasCR = false; } } if (i - start >= 0) { lines.Add (cells.GetRange (start, i - start)); } return lines; } /// /// Splits a rune cell list into a List that will contain a for each line. /// /// The cells list. /// public static List> ToCells (List cells) { return SplitNewLines (cells); } }