diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
index 4022880b7..e78baa96a 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs
@@ -1401,7 +1401,10 @@ namespace Terminal.Gui {
///
public static void Write (char [] buffer)
{
- throw new NotImplementedException ();
+ _buffer [CursorLeft, CursorTop] = (char)0;
+ foreach (var ch in buffer) {
+ _buffer [CursorLeft, CursorTop] += ch;
+ }
}
//
diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
index b86e8f11a..77526629b 100644
--- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
+++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
@@ -132,12 +132,12 @@ namespace Terminal.Gui {
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;
@@ -262,7 +262,12 @@ namespace Terminal.Gui {
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;
}
}
diff --git a/Terminal.Gui/Core/ConsoleDriver.cs b/Terminal.Gui/Core/ConsoleDriver.cs
index 8586a288d..edd0a8d74 100644
--- a/Terminal.Gui/Core/ConsoleDriver.cs
+++ b/Terminal.Gui/Core/ConsoleDriver.cs
@@ -681,7 +681,7 @@ namespace Terminal.Gui {
/// Column to move the cursor to.
/// Row to move the cursor to.
public abstract void Move (int col, int row);
-
+
///
/// Adds the specified rune to the display at the current cursor position.
///
@@ -696,11 +696,10 @@ namespace Terminal.Gui {
///
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;
diff --git a/Terminal.Gui/Core/TextFormatter.cs b/Terminal.Gui/Core/TextFormatter.cs
index 8dbe1d23f..56458b3e0 100644
--- a/Terminal.Gui/Core/TextFormatter.cs
+++ b/Terminal.Gui/Core/TextFormatter.cs
@@ -293,12 +293,6 @@ namespace Terminal.Gui {
}
}
- ///
- /// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of 0x100000 causes
- /// the underlying Rune to be identified as a "private use" Unicode character.
- /// HotKeyTagMask
- public uint HotKeyTagMask { get; set; } = 0x100000;
-
///
/// Gets the cursor position from . If the is defined, the cursor will be positioned over it.
///
@@ -317,8 +311,9 @@ namespace Terminal.Gui {
get {
// With this check, we protect against subclasses with overrides of Text
if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
- lines = new List ();
- lines.Add (ustring.Empty);
+ lines = new List {
+ ustring.Empty
+ };
NeedsFormat = false;
return lines;
}
@@ -716,7 +711,7 @@ namespace Terminal.Gui {
}
static char [] whitespace = new char [] { ' ', '\t' };
- private int hotKeyPos;
+ private int hotKeyPos = -1;
///
/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
@@ -1113,14 +1108,13 @@ namespace Terminal.Gui {
/// The text with the hotkey tagged.
///
/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for
- /// Runes with a bitmask of otKeyTagMask and remove that bitmask.
///
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);
}
@@ -1297,13 +1291,13 @@ namespace Terminal.Gui {
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);
diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs
index 60e5242c0..2c51d48ee 100644
--- a/Terminal.Gui/Core/View.cs
+++ b/Terminal.Gui/Core/View.cs
@@ -987,13 +987,13 @@ namespace Terminal.Gui {
if (view == null || subviews == null)
return;
- SetNeedsLayout ();
- SetNeedsDisplay ();
var touched = view.Frame;
subviews.Remove (view);
tabIndexes.Remove (view);
view.container = null;
view.tabIndex = -1;
+ SetNeedsLayout ();
+ SetNeedsDisplay ();
if (subviews.Count < 1) {
CanFocus = false;
}
diff --git a/Terminal.Gui/Core/Window.cs b/Terminal.Gui/Core/Window.cs
index d0811eb16..85c8929cc 100644
--- a/Terminal.Gui/Core/Window.cs
+++ b/Terminal.Gui/Core/Window.cs
@@ -277,18 +277,17 @@ namespace Terminal.Gui {
{
var padding = Border.GetSumThickness ();
var scrRect = ViewToScreen (new Rect (0, 0, Frame.Width, Frame.Height));
- //var borderLength = Border.DrawMarginFrame ? 1 : 0;
- // FIXED: Why do we draw the frame twice? This call is here to clear the content area, I think. Why not just clear that area?
- if (!NeedDisplay.IsEmpty) {
+ if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) {
Driver.SetAttribute (GetNormalColor ());
Clear ();
+ contentView.SetNeedsDisplay ();
}
var savedClip = contentView.ClipToBounds ();
// Redraw our contentView
// DONE: smartly constrict contentView.Bounds to just be what intersects with the 'bounds' we were passed
- contentView.Redraw (!NeedDisplay.IsEmpty ? contentView.Bounds : bounds);
+ contentView.Redraw (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded ? contentView.Bounds : bounds);
Driver.Clip = savedClip;
ClearLayoutNeeded ();
diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj
index 1c16d17f5..b46145312 100644
--- a/Terminal.Gui/Terminal.Gui.csproj
+++ b/Terminal.Gui/Terminal.Gui.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs
index 04d54745a..2244531d1 100644
--- a/Terminal.Gui/Views/Menu.cs
+++ b/Terminal.Gui/Views/Menu.cs
@@ -690,6 +690,7 @@ namespace Terminal.Gui {
}
} while (barItems.Children [current] == null || disabled);
SetNeedsDisplay ();
+ SetParentSetNeedsDisplay ();
if (!host.UseSubMenusSingleFrame)
host.OnMenuOpened ();
return true;
@@ -737,11 +738,24 @@ namespace Terminal.Gui {
}
} while (barItems.Children [current] == null || disabled);
SetNeedsDisplay ();
+ SetParentSetNeedsDisplay ();
if (!host.UseSubMenusSingleFrame)
host.OnMenuOpened ();
return true;
}
+ private void SetParentSetNeedsDisplay ()
+ {
+ if (host.openSubMenu != null) {
+ foreach (var menu in host.openSubMenu) {
+ menu.SetNeedsDisplay ();
+ }
+ }
+
+ host?.openMenu.SetNeedsDisplay ();
+ host.SetNeedsDisplay ();
+ }
+
public override bool MouseEvent (MouseEvent me)
{
if (!host.handled && !host.HandleGrabView (me, this)) {
@@ -778,6 +792,7 @@ namespace Terminal.Gui {
current = me.Y - 1;
if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) {
SetNeedsDisplay ();
+ SetParentSetNeedsDisplay ();
return true;
}
host.OnMenuOpened ();
@@ -806,6 +821,7 @@ namespace Terminal.Gui {
return host.CloseMenu (false, true);
} else {
SetNeedsDisplay ();
+ SetParentSetNeedsDisplay ();
}
return true;
}
@@ -1589,7 +1605,7 @@ namespace Terminal.Gui {
var subMenu = openCurrentMenu.current > -1 && openCurrentMenu.barItems.Children.Length > 0
? openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current])
: null;
- if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && subMenu == null) {
+ if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count - 1 == selectedSub) && subMenu == null) {
if (openSubMenu != null && !CloseMenu (false, true))
return;
NextMenu (false, ignoreUseSubMenusSingleFrame);
diff --git a/UnitTests/ConsoleDriverTests.cs b/UnitTests/ConsoleDriverTests.cs
index 173028179..db8d16753 100644
--- a/UnitTests/ConsoleDriverTests.cs
+++ b/UnitTests/ConsoleDriverTests.cs
@@ -639,7 +639,7 @@ namespace Terminal.Gui.ConsoleDrivers {
[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);
diff --git a/UnitTests/MenuTests.cs b/UnitTests/MenuTests.cs
index 96313ea1c..e41041e58 100644
--- a/UnitTests/MenuTests.cs
+++ b/UnitTests/MenuTests.cs
@@ -1654,5 +1654,85 @@ Edit
Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
}
+
+ [Fact, AutoInitShutdown]
+ public void MenuBar_In_Window_Without_Other_Views ()
+ {
+ var win = new Window ();
+ var menu = new MenuBar (new MenuBarItem [] {
+ new MenuBarItem ("File", new MenuItem [] {
+ new MenuItem ("New", "", null)
+ }),
+ new MenuBarItem ("Edit", new MenuItem [] {
+ new MenuBarItem ("Delete", new MenuItem [] {
+ new MenuItem ("All", "", null),
+ new MenuItem ("Selected", "", null)
+ })
+ })
+ }); ;
+ win.Add (menu);
+ var top = Application.Top;
+ top.Add (win);
+ Application.Begin (top);
+ ((FakeDriver)Application.Driver).SetBufferSize (40, 8);
+
+ TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File Edit │
+│ │
+│ │
+│ │
+│ │
+│ │
+└──────────────────────────────────────┘", output);
+
+ Assert.True (win.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
+ win.Redraw (win.Bounds);
+ TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File Edit │
+│┌──────┐ │
+││ New │ │
+│└──────┘ │
+│ │
+│ │
+└──────────────────────────────────────┘", output);
+
+ Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+ win.Redraw (win.Bounds);
+ TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File Edit │
+│ ┌─────────┐ │
+│ │ Delete ►│ │
+│ └─────────┘ │
+│ │
+│ │
+└──────────────────────────────────────┘", output);
+
+ Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+ win.Redraw (win.Bounds);
+ TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File Edit │
+│ ┌─────────┐ │
+│ │ Delete ►│┌───────────┐ │
+│ └─────────┘│ All │ │
+│ │ Selected │ │
+│ └───────────┘ │
+└──────────────────────────────────────┘", output);
+
+ Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+ win.Redraw (win.Bounds);
+ TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File Edit │
+│┌──────┐ │
+││ New │ │
+│└──────┘ │
+│ │
+│ │
+└──────────────────────────────────────┘", output);
+ }
}
}
diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs
index 0b178b584..ac13d56c0 100644
--- a/UnitTests/TestHelpers.cs
+++ b/UnitTests/TestHelpers.cs
@@ -92,7 +92,15 @@ class TestHelpers {
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 ();
}
@@ -121,7 +129,7 @@ class TestHelpers {
public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
{
- var lines = new List> ();
+ var lines = new List> ();
var sb = new StringBuilder ();
var driver = ((FakeDriver)Application.Driver);
var x = -1;
@@ -132,15 +140,15 @@ class TestHelpers {
var contents = driver.Contents;
for (int r = 0; r < driver.Rows; r++) {
- var runes = new List ();
+ var runes = new List ();
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 () { ' ' });
+ runes.InsertRange (i, new List () { ' ' });
}
}
if (Rune.ColumnWidth (rune) > 1) {
@@ -169,7 +177,7 @@ class TestHelpers {
// Remove trailing whitespace on each line
for (int r = 0; r < lines.Count; r++) {
- List row = lines [r];
+ List 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)) {
@@ -179,9 +187,9 @@ class TestHelpers {
}
}
- // 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 {
diff --git a/UnitTests/TextFormatterTests.cs b/UnitTests/TextFormatterTests.cs
index 0e1777d31..e2750439a 100644
--- a/UnitTests/TextFormatterTests.cs
+++ b/UnitTests/TextFormatterTests.cs
@@ -2424,38 +2424,38 @@ namespace Terminal.Gui.Core {
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]
@@ -4163,5 +4163,101 @@ This TextFormatter (tf2) is rewritten.
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);
+ }
}
}
\ No newline at end of file