Skip to content

Commit cd75a20

Browse files
BDisptigCopilot
authored
Fixes #4387. Runes should not be used on a cell, but rather should use a single grapheme rendering 1 or 2 columns (#4388)
* Fixes #4382. StringExtensions.GetColumns method should only return the total text width and not the sum of all runes width * Trying to fix unit test error * Update StringExtensions.cs Co-authored-by: Copilot <[email protected]> * Resolving merge conflicts * Prevents Runes throwing if Grapheme is null * Add unit test to prove that null and empty string doesn't not throws anything. * Fix unit test failure * Fix IsValidLocation for wide graphemes * Add more combining * Prevent set invalid graphemes * Fix unit tests * Grapheme doesn't support invalid code points like lone surrogates * Fixes more unit tests * Fix unit test * Seems all test are fixed now * Adjust CharMap scenario with graphemes * Upgrade Wcwidth to version 4.0.0 * Reformat * Trying fix CheckDefaultState assertion * Revert "Trying fix CheckDefaultState assertion" This reverts commit c9b46b7. * Forgot to include driver.End in the test * Reapply "Trying fix CheckDefaultState assertion" This reverts commit 1060ac9. * Remove ToString * Fix merge errors * Change to conditional expression * Assertion to prove that no exception throws during cell initialization. * Remove unnecessary assignment * Remove assignment to end * Replace string concatenation with 'StringBuilder'. * Replace more string concatenation with 'StringBuilder' * Remove redundant call to 'ToString' because Rune cast to a String object. * Replace foreach loop with Sum linq --------- Co-authored-by: Tig <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 726b15d commit cd75a20

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2596
-2087
lines changed

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="[9.0.0,10)" />
1919
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.6" />
2020
<PackageVersion Include="System.IO.Abstractions" Version="[22.0.16,23)" />
21-
<PackageVersion Include="Wcwidth" Version="[3.0.0,)" />
21+
<PackageVersion Include="Wcwidth" Version="[4.0.0,)" />
2222
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21.2,2)" />
2323
<PackageVersion Include="Serilog" Version="4.2.0" />
2424
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />

Examples/UICatalog/Scenarios/CombiningMarks.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public override void Main ()
1616
Application.Current!.SetNeedsDraw ();
1717

1818
var i = -1;
19-
top.AddStr ("Terminal.Gui only supports combining marks that normalize. See Issue #2616.");
19+
top.Move (0, ++i);
20+
top.AddStr ("Terminal.Gui supports all combining sequences that can be rendered as an unique grapheme.");
2021
top.Move (0, ++i);
2122
top.AddStr ("\u0301<- \"\\u0301\" using AddStr.");
2223
top.Move (0, ++i);
@@ -38,7 +39,7 @@ public override void Main ()
3839
top.AddRune ('\u0301');
3940
top.AddRune ('\u0328');
4041
top.AddRune (']');
41-
top.AddStr ("<- \"[a\\u0301\\u0301\\u0328]\" using AddRune for each.");
42+
top.AddStr ("<- \"[a\\u0301\\u0301\\u0328]\" using AddRune for each. Avoid use AddRune for combining sequences because may result with empty blocks at end.");
4243
top.Move (0, ++i);
4344
top.AddStr ("[a\u0301\u0301\u0328]<- \"[a\\u0301\\u0301\\u0328]\" using AddStr.");
4445
top.Move (0, ++i);
@@ -82,6 +83,16 @@ public override void Main ()
8283
top.AddStr ("[\U0001F468\U0001F469\U0001F9D2]<- \"[\\U0001F468\\U0001F469\\U0001F9D2]\" using AddStr.");
8384
top.Move (0, ++i);
8485
top.AddStr ("[\U0001F468\u200D\U0001F469\u200D\U0001F9D2]<- \"[\\U0001F468\\u200D\\U0001F469\\u200D\\U0001F9D2]\" using AddStr.");
86+
top.Move (0, ++i);
87+
top.AddStr ("[\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466]<- \"[\\U0001F468\\u200D\\U0001F469\\u200D\\U0001F467\\u200D\\U0001F466]\" using AddStr.");
88+
top.Move (0, ++i);
89+
top.AddStr ("[\u0e32\u0e33]<- \"[\\u0e32\\u0e33]\" using AddStr.");
90+
top.Move (0, ++i);
91+
top.AddStr ("[\U0001F469\u200D\u2764\uFE0F\u200D\U0001F48B\u200D\U0001F468]<- \"[\\U0001F469\\u200D\\u2764\\uFE0F\\u200D\\U0001F48B\\u200D\\U0001F468]\" using AddStr.");
92+
top.Move (0, ++i);
93+
top.AddStr ("[\u0061\uFE20\u0065\uFE21]<- \"[\\u0061\\uFE20\\u0065\\uFE21]\" using AddStr.");
94+
top.Move (0, ++i);
95+
top.AddStr ("[\u1100\uD7B0]<- \"[\\u1100\\uD7B0]\" using AddStr.");
8596
};
8697

8798
Application.Run (top);

Examples/UICatalog/Scenarios/LineDrawing.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ protected override bool OnDrawingContent ()
284284
SetCurrentAttribute (c.Value.Value.Attribute ?? GetAttributeForRole (VisualRole.Normal));
285285

286286
// TODO: #2616 - Support combining sequences that don't normalize
287-
AddRune (c.Key.X, c.Key.Y, c.Value.Value.Rune);
287+
AddStr (c.Key.X, c.Key.Y, c.Value.Value.Grapheme);
288288
}
289289
}
290290
}

Examples/UICatalog/Scenarios/Sliders.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,17 @@ public void MakeSliders (View v, List<object> options)
8686
{
8787
if (single.Orientation == Orientation.Horizontal)
8888
{
89-
single.Style.SpaceChar = new () { Rune = Glyphs.HLine };
90-
single.Style.OptionChar = new () { Rune = Glyphs.HLine };
89+
single.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () };
90+
single.Style.OptionChar = new () { Grapheme = Glyphs.HLine.ToString () };
9191
}
9292
else
9393
{
94-
single.Style.SpaceChar = new () { Rune = Glyphs.VLine };
95-
single.Style.OptionChar = new () { Rune = Glyphs.VLine };
94+
single.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () };
95+
single.Style.OptionChar = new () { Grapheme = Glyphs.VLine.ToString () };
9696
}
9797
};
98-
single.Style.SetChar = new () { Rune = Glyphs.ContinuousMeterSegment };
99-
single.Style.DragChar = new () { Rune = Glyphs.ContinuousMeterSegment };
98+
single.Style.SetChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () };
99+
single.Style.DragChar = new () { Grapheme = Glyphs.ContinuousMeterSegment.ToString () };
100100

101101
v.Add (single);
102102

@@ -257,7 +257,7 @@ public override void Main ()
257257
{
258258
s.Orientation = Orientation.Horizontal;
259259

260-
s.Style.SpaceChar = new () { Rune = Glyphs.HLine };
260+
s.Style.SpaceChar = new () { Grapheme = Glyphs.HLine.ToString () };
261261

262262
if (prev == null)
263263
{
@@ -275,7 +275,7 @@ public override void Main ()
275275
{
276276
s.Orientation = Orientation.Vertical;
277277

278-
s.Style.SpaceChar = new () { Rune = Glyphs.VLine };
278+
s.Style.SpaceChar = new () { Grapheme = Glyphs.VLine.ToString () };
279279

280280
if (prev == null)
281281
{

Examples/UICatalog/Scenarios/SyntaxHighlighting.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ public override void Main ()
152152
),
153153
null,
154154
new (
155-
"_Load Rune Cells",
155+
"_Load Text Cells",
156156
"",
157157
() => ApplyLoadCells ()
158158
),
159159
new (
160-
"_Save Rune Cells",
160+
"_Save Text Cells",
161161
"",
162162
() => SaveCells ()
163163
),
@@ -240,12 +240,9 @@ private void ApplyLoadCells ()
240240
{
241241
string csName = color.Key;
242242

243-
foreach (Rune rune in csName.EnumerateRunes ())
244-
{
245-
cells.Add (new () { Rune = rune, Attribute = color.Value.Normal });
246-
}
243+
cells.AddRange (Cell.ToCellList (csName, color.Value.Normal));
247244

248-
cells.Add (new () { Rune = (Rune)'\n', Attribute = color.Value.Focus });
245+
cells.Add (new () { Grapheme = "\n", Attribute = color.Value.Focus });
249246
}
250247

251248
if (File.Exists (_path))

Terminal.Gui/Drawing/Cell.cs

Lines changed: 86 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,117 @@
11

2-
32
namespace Terminal.Gui.Drawing;
43

54
/// <summary>
65
/// Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and
76
/// <see cref="IDriver"/>).
87
/// </summary>
9-
public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default)
8+
public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, string Grapheme = "")
109
{
1110
/// <summary>The attributes to use when drawing the Glyph.</summary>
1211
public Attribute? Attribute { get; set; } = Attribute;
1312

1413
/// <summary>
15-
/// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.Cell"/> has been modified since the
14+
/// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.Drawing.Cell"/> has been modified since the
1615
/// last time it was drawn.
1716
/// </summary>
1817
public bool IsDirty { get; set; } = IsDirty;
1918

20-
private Rune _rune = Rune;
19+
private string _grapheme = Grapheme;
2120

22-
/// <summary>The character to display. If <see cref="Rune"/> is <see langword="null"/>, then <see cref="Rune"/> is ignored.</summary>
23-
public Rune Rune
21+
/// <summary>
22+
/// The single grapheme cluster to display from this cell. If <see cref="Grapheme"/> is <see langword="null"/> or
23+
/// <see cref="string.Empty"/>, then <see cref="Cell"/> is ignored.
24+
/// </summary>
25+
public string Grapheme
2426
{
25-
get => _rune;
27+
readonly get => _grapheme;
2628
set
2729
{
28-
_combiningMarks?.Clear ();
29-
_rune = value;
30-
}
31-
}
30+
if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1)
31+
{
32+
throw new InvalidOperationException ($"Only a single {nameof (Grapheme)} cluster is allowed per Cell.");
33+
}
3234

33-
private List<Rune>? _combiningMarks;
35+
if (!string.IsNullOrEmpty (value) && value.Length == 1 && char.IsSurrogate (value [0]))
36+
{
37+
throw new ArgumentException ($"Only valid Unicode scalar values are allowed in a single {nameof (Grapheme)} cluster.");
38+
}
3439

35-
/// <summary>
36-
/// The combining marks for <see cref="Rune"/> that when combined makes this Cell a combining sequence. If
37-
/// <see cref="CombiningMarks"/> empty, then <see cref="CombiningMarks"/> is ignored.
38-
/// </summary>
39-
/// <remarks>
40-
/// Only valid in the rare case where <see cref="Rune"/> is a combining sequence that could not be normalized to a
41-
/// single Rune.
42-
/// </remarks>
43-
internal IReadOnlyList<Rune> CombiningMarks
44-
{
45-
// PERFORMANCE: Downside of the interface return type is that List<T> struct enumerator cannot be utilized, i.e. enumerator is allocated.
46-
// If enumeration is used heavily in the future then might be better to expose the List<T> Enumerator directly via separate mechanism.
47-
get
48-
{
49-
// Avoid unnecessary list allocation.
50-
if (_combiningMarks == null)
40+
try
5141
{
52-
return Array.Empty<Rune> ();
42+
_grapheme = !string.IsNullOrEmpty (value) && !value.IsNormalized (NormalizationForm.FormC)
43+
? value.Normalize (NormalizationForm.FormC)
44+
: value;
45+
}
46+
catch (ArgumentException)
47+
{
48+
// leave text unnormalized
49+
_grapheme = value;
5350
}
54-
return _combiningMarks;
5551
}
5652
}
5753

5854
/// <summary>
59-
/// Adds combining mark to the cell.
55+
/// The rune for <see cref="Grapheme"/> or runes for <see cref="Grapheme"/> that when combined makes this Cell a combining sequence.
6056
/// </summary>
61-
/// <param name="combiningMark">The combining mark to add to the cell.</param>
62-
internal void AddCombiningMark (Rune combiningMark)
57+
/// <remarks>
58+
/// In the case where <see cref="Grapheme"/> has more than one rune it is a combining sequence that is normalized to a
59+
/// single Text which may occupies 1 or 2 columns.
60+
/// </remarks>
61+
public IReadOnlyList<Rune> Runes => string.IsNullOrEmpty (Grapheme) ? [] : Grapheme.EnumerateRunes ().ToList ();
62+
63+
/// <inheritdoc/>
64+
public override string ToString ()
6365
{
64-
_combiningMarks ??= [];
65-
_combiningMarks.Add (combiningMark);
66+
string visibleText = EscapeControlAndInvisible (Grapheme);
67+
68+
return $"[\"{visibleText}\":{Attribute}]";
6669
}
6770

68-
/// <summary>
69-
/// Clears combining marks of the cell.
70-
/// </summary>
71-
internal void ClearCombiningMarks ()
71+
private static string EscapeControlAndInvisible (string text)
7272
{
73-
_combiningMarks?.Clear ();
74-
}
73+
if (string.IsNullOrEmpty (text))
74+
{
75+
return "";
76+
}
7577

76-
/// <inheritdoc/>
77-
public override string ToString () { return $"['{Rune}':{Attribute}]"; }
78+
var sb = new StringBuilder ();
79+
80+
foreach (var rune in text.EnumerateRunes ())
81+
{
82+
switch (rune.Value)
83+
{
84+
case '\0': sb.Append ("␀"); break;
85+
case '\t': sb.Append ("\\t"); break;
86+
case '\r': sb.Append ("\\r"); break;
87+
case '\n': sb.Append ("\\n"); break;
88+
case '\f': sb.Append ("\\f"); break;
89+
case '\v': sb.Append ("\\v"); break;
90+
default:
91+
if (char.IsControl ((char)rune.Value))
92+
{
93+
// show as \uXXXX
94+
sb.Append ($"\\u{rune.Value:X4}");
95+
}
96+
else
97+
{
98+
sb.Append (rune);
99+
}
100+
break;
101+
}
102+
}
103+
104+
return sb.ToString ();
105+
}
78106

79107
/// <summary>Converts the string into a <see cref="List{Cell}"/>.</summary>
80108
/// <param name="str">The string to convert.</param>
81109
/// <param name="attribute">The <see cref="Scheme"/> to use.</param>
82110
/// <returns></returns>
83111
public static List<Cell> ToCellList (string str, Attribute? attribute = null)
84112
{
85-
List<Cell> cells = new ();
86-
87-
foreach (Rune rune in str.EnumerateRunes ())
88-
{
89-
cells.Add (new () { Rune = rune, Attribute = attribute });
90-
}
113+
List<Cell> cells = [];
114+
cells.AddRange (GraphemeHelper.GetGraphemes (str).Select (grapheme => new Cell { Grapheme = grapheme, Attribute = attribute }));
91115

92116
return cells;
93117
}
@@ -100,9 +124,7 @@ public static List<Cell> ToCellList (string str, Attribute? attribute = null)
100124
/// <returns>A <see cref="List{Cell}"/> for each line.</returns>
101125
public static List<List<Cell>> StringToLinesOfCells (string content, Attribute? attribute = null)
102126
{
103-
List<Cell> cells = content.EnumerateRunes ()
104-
.Select (x => new Cell { Rune = x, Attribute = attribute })
105-
.ToList ();
127+
List<Cell> cells = ToCellList (content, attribute);
106128

107129
return SplitNewLines (cells);
108130
}
@@ -112,14 +134,14 @@ public static List<List<Cell>> StringToLinesOfCells (string content, Attribute?
112134
/// <returns></returns>
113135
public static string ToString (IEnumerable<Cell> cells)
114136
{
115-
var str = string.Empty;
137+
StringBuilder sb = new ();
116138

117139
foreach (Cell cell in cells)
118140
{
119-
str += cell.Rune.ToString ();
141+
sb.Append (cell.Grapheme);
120142
}
121143

122-
return str;
144+
return sb.ToString ();
123145
}
124146

125147
/// <summary>Converts a <see cref="List{Cell}"/> generic collection into a string.</summary>
@@ -147,26 +169,19 @@ public static string ToString (List<List<Cell>> cellsList)
147169

148170
internal static List<Cell> StringToCells (string str, Attribute? attribute = null)
149171
{
150-
List<Cell> cells = [];
151-
152-
foreach (Rune rune in str.ToRunes ())
153-
{
154-
cells.Add (new () { Rune = rune, Attribute = attribute });
155-
}
156-
157-
return cells;
172+
return ToCellList (str, attribute);
158173
}
159174

160-
internal static List<Cell> ToCells (IEnumerable<Rune> runes, Attribute? attribute = null)
175+
internal static List<Cell> ToCells (IEnumerable<string> strings, Attribute? attribute = null)
161176
{
162-
List<Cell> cells = new ();
177+
StringBuilder sb = new ();
163178

164-
foreach (Rune rune in runes)
179+
foreach (string str in strings)
165180
{
166-
cells.Add (new () { Rune = rune, Attribute = attribute });
181+
sb.Append (str);
167182
}
168183

169-
return cells;
184+
return ToCellList (sb.ToString (), attribute);
170185
}
171186

172187
private static List<List<Cell>> SplitNewLines (List<Cell> cells)
@@ -179,14 +194,15 @@ private static List<List<Cell>> SplitNewLines (List<Cell> cells)
179194
// ASCII code 10 = Line Feed.
180195
for (; i < cells.Count; i++)
181196
{
182-
if (cells [i].Rune.Value == 13)
197+
if (cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 13)
183198
{
184199
hasCR = true;
185200

186201
continue;
187202
}
188203

189-
if (cells [i].Rune.Value == 10)
204+
if ((cells [i].Grapheme.Length == 1 && cells [i].Grapheme [0] == 10)
205+
|| cells [i].Grapheme == "\r\n")
190206
{
191207
if (i - start > 0)
192208
{

0 commit comments

Comments
 (0)