Skip to content

Fixes #2247. Preparing for the NStack v1.0.7 which now handling properly non-BMP code points. #2248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1401,7 +1401,10 @@ public static void Write (char value)
/// <param name="buffer"></param>
public static void Write (char [] buffer)
{
throw new NotImplementedException ();
_buffer [CursorLeft, CursorTop] = (char)0;
foreach (var ch in buffer) {
_buffer [CursorLeft, CursorTop] += ch;
}
}

//
Expand Down
11 changes: 8 additions & 3 deletions Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ public override void AddRune (Rune rune)
needMove = false;
}
if (runeWidth < 2 && ccol > 0
&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
&& Rune.ColumnWidth ((Rune)contents [crow, ccol - 1, 0]) > 1) {

contents [crow, ccol - 1, 0] = (int)(uint)' ';

} else if (runeWidth < 2 && ccol <= Clip.Right - 1
&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
&& Rune.ColumnWidth ((Rune)contents [crow, ccol, 0]) > 1) {

contents [crow, ccol + 1, 0] = (int)(uint)' ';
contents [crow, ccol + 1, 2] = 1;
Expand Down Expand Up @@ -234,7 +234,12 @@ public override void UpdateScreen ()
if (color != redrawColor)
SetColor (color);

FakeConsole.Write ((char)contents [row, col, 0]);
Rune rune = contents [row, col, 0];
if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
FakeConsole.Write (spair);
} else {
FakeConsole.Write ((char)rune);
}
contents [row, col, 2] = 0;
}
}
Expand Down
7 changes: 3 additions & 4 deletions Terminal.Gui/Core/ConsoleDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ public abstract class ConsoleDriver {
/// <param name="col">Column to move the cursor to.</param>
/// <param name="row">Row to move the cursor to.</param>
public abstract void Move (int col, int row);

/// <summary>
/// Adds the specified rune to the display at the current cursor position.
/// </summary>
Expand All @@ -696,11 +696,10 @@ public abstract class ConsoleDriver {
/// <returns></returns>
public static Rune MakePrintable (Rune c)
{
var controlChars = c & 0xFFFF;
if (controlChars <= 0x1F || controlChars >= 0X7F && controlChars <= 0x9F) {
if (c <= 0x1F || (c >= 0X7F && c <= 0x9F)) {
// ASCII (C0) control characters.
// C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1)
return new Rune (controlChars + 0x2400);
return new Rune (c + 0x2400);
}

return c;
Expand Down
22 changes: 8 additions & 14 deletions Terminal.Gui/Core/TextFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,12 +293,6 @@ internal set {
}
}

/// <summary>
/// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of <c>0x100000</c> causes
/// the underlying Rune to be identified as a "private use" Unicode character.
/// </summary>HotKeyTagMask
public uint HotKeyTagMask { get; set; } = 0x100000;

/// <summary>
/// Gets the cursor position from <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will be positioned over it.
/// </summary>
Expand All @@ -317,8 +311,9 @@ public List<ustring> Lines {
get {
// With this check, we protect against subclasses with overrides of Text
if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
lines = new List<ustring> ();
lines.Add (ustring.Empty);
lines = new List<ustring> {
ustring.Empty
};
NeedsFormat = false;
return lines;
}
Expand Down Expand Up @@ -716,7 +711,7 @@ public static ustring Justify (ustring text, int width, char spaceChar = ' ', Te
}

static char [] whitespace = new char [] { ' ', '\t' };
private int hotKeyPos;
private int hotKeyPos = -1;

/// <summary>
/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
Expand Down Expand Up @@ -1113,14 +1108,13 @@ public static bool FindHotKey (ustring text, Rune hotKeySpecifier, bool firstUpp
/// <returns>The text with the hotkey tagged.</returns>
/// <remarks>
/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for
/// Runes with a bitmask of <c>otKeyTagMask</c> and remove that bitmask.
/// </remarks>
public ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
{
// Set the high bit
var runes = text.ToRuneList ();
if (Rune.IsLetterOrNumber (runes [hotPos])) {
runes [hotPos] = new Rune ((uint)runes [hotPos] | HotKeyTagMask);
runes [hotPos] = new Rune ((uint)runes [hotPos]);
}
return ustring.Make (runes);
}
Expand Down Expand Up @@ -1297,13 +1291,13 @@ public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor, Rect c
rune = runes [idx];
}
}
if ((rune & HotKeyTagMask) == HotKeyTagMask) {
if (idx == HotKeyPos) {
if ((isVertical && textVerticalAlignment == VerticalTextAlignment.Justified) ||
(!isVertical && textAlignment == TextAlignment.Justified)) {
(!isVertical && textAlignment == TextAlignment.Justified)) {
CursorPosition = idx - start;
}
Application.Driver?.SetAttribute (hotColor);
Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
Application.Driver?.AddRune (rune);
Application.Driver?.SetAttribute (normalColor);
} else {
Application.Driver?.AddRune (rune);
Expand Down
2 changes: 1 addition & 1 deletion UnitTests/ConsoleDriverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ public void Write_Do_Not_Change_On_ProcessKey ()
[InlineData (0x0000001F, 0x241F)]
[InlineData (0x0000007F, 0x247F)]
[InlineData (0x0000009F, 0x249F)]
[InlineData (0x0001001A, 0x241A)]
[InlineData (0x0001001A, 0x1001A)]
public void MakePrintable_Converts_Control_Chars_To_Proper_Unicode (uint code, uint expected)
{
var actual = ConsoleDriver.MakePrintable (code);
Expand Down
24 changes: 16 additions & 8 deletions UnitTests/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelp

for (int r = 0; r < driver.Rows; r++) {
for (int c = 0; c < driver.Cols; c++) {
sb.Append ((char)contents [r, c, 0]);
Rune rune = contents [r, c, 0];
if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
sb.Append (spair);
} else {
sb.Append ((char)rune);
}
if (Rune.ColumnWidth (rune) > 1) {
c++;
}
}
sb.AppendLine ();
}
Expand Down Expand Up @@ -82,7 +90,7 @@ public static void AssertDriverContentsAre (string expectedLook, ITestOutputHelp

public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
{
var lines = new List<List<char>> ();
var lines = new List<List<Rune>> ();
var sb = new StringBuilder ();
var driver = ((FakeDriver)Application.Driver);
var x = -1;
Expand All @@ -93,15 +101,15 @@ public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestO
var contents = driver.Contents;

for (int r = 0; r < driver.Rows; r++) {
var runes = new List<char> ();
var runes = new List<Rune> ();
for (int c = 0; c < driver.Cols; c++) {
var rune = (char)contents [r, c, 0];
var rune = (Rune)contents [r, c, 0];
if (rune != ' ') {
if (x == -1) {
x = c;
y = r;
for (int i = 0; i < c; i++) {
runes.InsertRange (i, new List<char> () { ' ' });
runes.InsertRange (i, new List<Rune> () { ' ' });
}
}
if (Rune.ColumnWidth (rune) > 1) {
Expand Down Expand Up @@ -130,7 +138,7 @@ public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestO

// Remove trailing whitespace on each line
for (int r = 0; r < lines.Count; r++) {
List<char> row = lines [r];
List<Rune> row = lines [r];
for (int c = row.Count - 1; c >= 0; c--) {
var rune = row [c];
if (rune != ' ' || (row.Sum (x => Rune.ColumnWidth (x)) == w)) {
Expand All @@ -140,9 +148,9 @@ public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestO
}
}

// Convert char list to string
// Convert Rune list to string
for (int r = 0; r < lines.Count; r++) {
var line = new string (lines [r].ToArray ());
var line = NStack.ustring.Make (lines [r]).ToString ();
if (r == lines.Count - 1) {
sb.Append (line);
} else {
Expand Down
112 changes: 104 additions & 8 deletions UnitTests/TextFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2424,38 +2424,38 @@ public void ReplaceHotKeyWithTag ()
var tf = new TextFormatter ();
ustring text = "test";
int hotPos = 0;
uint tag = tf.HotKeyTagMask | 't';
uint tag = 't';

Assert.Equal (ustring.Make (new Rune [] { tag, 'e', 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));

tag = tf.HotKeyTagMask | 'e';
tag = 'e';
hotPos = 1;
Assert.Equal (ustring.Make (new Rune [] { 't', tag, 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));

var result = tf.ReplaceHotKeyWithTag (text, hotPos);
Assert.Equal ('e', (uint)(result.ToRunes () [1] & ~tf.HotKeyTagMask));
Assert.Equal ('e', (uint)(result.ToRunes () [1]));

text = "Ok";
tag = 0x100000 | 'O';
tag = 'O';
hotPos = 0;
Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
Assert.Equal ('O', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
Assert.Equal ('O', (uint)(result.ToRunes () [0]));

text = "[◦ Ok ◦]";
text = ustring.Make (new Rune [] { '[', '◦', ' ', 'O', 'k', ' ', '◦', ']' });
var runes = text.ToRuneList ();
Assert.Equal (text.RuneCount, runes.Count);
Assert.Equal (text, ustring.Make (runes));
tag = tf.HotKeyTagMask | 'O';
tag = 'O';
hotPos = 3;
Assert.Equal (ustring.Make (new Rune [] { '[', '◦', ' ', tag, 'k', ' ', '◦', ']' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
Assert.Equal ('O', (uint)(result.ToRunes () [3] & ~tf.HotKeyTagMask));
Assert.Equal ('O', (uint)(result.ToRunes () [3]));

text = "^k";
tag = '^';
hotPos = 0;
Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
Assert.Equal ('^', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
Assert.Equal ('^', (uint)(result.ToRunes () [0]));
}

[Fact]
Expand Down Expand Up @@ -4163,5 +4163,101 @@ public void Ustring_Array_Is_Not_Equal_ToRunes_Array_And_String_Array ()
Assert.Equal ("你", ((Rune)usToRunes [9]).ToString ());
Assert.Equal ("你", s [9].ToString ());
}

[Fact, AutoInitShutdown]
public void Non_Bmp_ConsoleWidth_ColumnWidth_Equal_Two ()
{
ustring us = "\U0001d539";
Rune r = 0x1d539;

Assert.Equal ("𝔹", us);
Assert.Equal ("𝔹", r.ToString ());
Assert.Equal (us, r.ToString ());

Assert.Equal (2, us.ConsoleWidth);
Assert.Equal (2, Rune.ColumnWidth (r));

var win = new Window (us);
var label = new Label (ustring.Make (r));
var tf = new TextField (us) { Y = 1, Width = 3 };
win.Add (label, tf);
var top = Application.Top;
top.Add (win);

Application.Begin (top);
((FakeDriver)Application.Driver).SetBufferSize (10, 4);

var expected = @"
┌ 𝔹 ────┐
│𝔹 │
│𝔹 │
└────────┘";
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);

TestHelpers.AssertDriverContentsAre (expected, output);

var expectedColors = new Attribute [] {
// 0
Colors.Base.Normal,
// 1
Colors.Base.Focus,
// 2
Colors.Base.HotNormal
};

TestHelpers.AssertDriverColorsAre (@"
0222200000
0000000000
0111000000
0000000000", expectedColors);
}

[Fact, AutoInitShutdown]
public void CJK_Compatibility_Ideographs_ConsoleWidth_ColumnWidth_Equal_Two ()
{
ustring us = "\U0000f900";
Rune r = 0xf900;

Assert.Equal ("豈", us);
Assert.Equal ("豈", r.ToString ());
Assert.Equal (us, r.ToString ());

Assert.Equal (2, us.ConsoleWidth);
Assert.Equal (2, Rune.ColumnWidth (r));

var win = new Window (us);
var label = new Label (ustring.Make (r));
var tf = new TextField (us) { Y = 1, Width = 3 };
win.Add (label, tf);
var top = Application.Top;
top.Add (win);

Application.Begin (top);
((FakeDriver)Application.Driver).SetBufferSize (10, 4);

var expected = @"
┌ 豈 ────┐
│豈 │
│豈 │
└────────┘";
TestHelpers.AssertDriverContentsWithFrameAre (expected, output);

TestHelpers.AssertDriverContentsAre (expected, output);

var expectedColors = new Attribute [] {
// 0
Colors.Base.Normal,
// 1
Colors.Base.Focus,
// 2
Colors.Base.HotNormal
};

TestHelpers.AssertDriverColorsAre (@"
0222200000
0000000000
0111000000
0000000000", expectedColors);
}
}
}