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