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.GetGraphemeCount (value) > 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); }
}
| | | | | | | | |