From 9e6d5f3a0e8e8c0a6dbea62f08e2c7ef2fdbec31 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 12 Mar 2025 16:56:15 +0000 Subject: [PATCH 1/7] Add constructor Key(int) and operator for handled with non-Bmp. --- Terminal.Gui/Input/Keyboard/Key.cs | 52 +++++++++++++++++- .../Input/Keyboard/KeyTests.cs | 53 +++++++++++++++---- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index e0d4119aa3..dfcd34e672 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -138,6 +138,44 @@ public Key (string str) KeyCode = key.KeyCode; } + /// + /// Constructs a new Key from an integer describing the key. + /// It parses the integer as Key by calling the constructor with a char or calls the constructor with a + /// KeyCode. + /// + /// + /// Don't rely on passed from to because + /// would not return the expected keys from 'a' to 'z'. + /// + /// The integer describing the key. + /// + /// + public Key (int value) + { + if (value < 0 || value > RuneExtensions.MaxUnicodeCodePoint) + { + throw new ArgumentOutOfRangeException (@$"Invalid key value: {value}", nameof (value)); + } + + if (char.IsSurrogate ((char)value)) + { + throw new ArgumentException (@$"Surrogate key not allowed: {value}", nameof (value)); + } + + Key key; + + if (((Rune)value).IsBmp) + { + key = new ((char)value); + } + else + { + key = new ((KeyCode)value); + } + + KeyCode = key.KeyCode; + } + /// /// The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers. /// Useful for determining if a key represents is a printable character. @@ -388,6 +426,11 @@ public static Rune ToRune (KeyCode key) /// public static implicit operator Key (string str) { return new (str); } + /// Cast to a . + /// See for more information. + /// + public static implicit operator Key (int value) { return new (value); } + /// Cast a to a . /// See for more information. /// @@ -550,7 +593,7 @@ private static string TrimEndSeparator (string input, Rune separator) // "Ctrl+" (trim) // "Ctrl++" (trim) - if (input.Length > 1 && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator) + if (input.Length > 1 && !char.IsHighSurrogate (input [^2]) && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator) { return input [..^1]; } @@ -640,6 +683,13 @@ public static bool TryParse (string text, out Key key) return false; } + if (text.Length == 2 && char.IsHighSurrogate (text [^2]) && char.IsLowSurrogate (text [^1])) + { + // It's a surrogate pair and there is no modifiers + key = new (new Rune (text [^2], text [^1]).Value); + return true; + } + // e.g. "Ctrl++" if ((Rune)text [^1] != separator && parts.Any (string.IsNullOrEmpty)) { diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs index 0fcb4bead4..26605314a0 100644 --- a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs @@ -52,7 +52,8 @@ public class KeyTests { "Ctrl-A", Key.A.WithCtrl }, { "Alt-A", Key.A.WithAlt }, { "A-Ctrl", Key.A.WithCtrl }, - { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt } + { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt }, + { "📄", (KeyCode)0x1F4C4 } }; [Theory] @@ -120,10 +121,13 @@ public void AsRune_ShouldReturnCorrectIntValue (KeyCode key, uint expected) [InlineData ('\'', (KeyCode)'\'')] [InlineData ('\xFFFF', (KeyCode)0xFFFF)] [InlineData ('\x0', (KeyCode)0x0)] - public void Cast_Char_To_Key (char ch, KeyCode expectedKeyCode) + public void Cast_Char_Int_To_Key (char ch, KeyCode expectedKeyCode) { var key = (Key)ch; Assert.Equal (expectedKeyCode, key.KeyCode); + + key = (int)ch; + Assert.Equal (expectedKeyCode, key.KeyCode); } [Fact] @@ -140,23 +144,25 @@ public void Cast_Key_ToString () [InlineData (KeyCode.A | KeyCode.ShiftMask, KeyCode.A | KeyCode.ShiftMask)] [InlineData (KeyCode.Z, KeyCode.Z)] [InlineData (KeyCode.Space, KeyCode.Space)] - public void Cast_KeyCode_To_Key (KeyCode cdk, KeyCode expected) + public void Cast_KeyCode_Int_To_Key (KeyCode cdk, KeyCode expected) { - // explicit + // KeyCode var key = (Key)cdk; Assert.Equal (((Key)expected).ToString (), key.ToString ()); - // implicit - key = cdk; + // Int + key = key.AsRune.Value; Assert.Equal (((Key)expected).ToString (), key.ToString ()); } // string cast operators - [Fact] - public void Cast_String_To_Key () + [Theory] + [InlineData ("Ctrl+Q", KeyCode.Q | KeyCode.CtrlMask)] + [InlineData ("📄", (KeyCode)0x1F4C4)] + public void Cast_String_To_Key (string str, KeyCode expectedKeyCode) { - var key = (Key)"Ctrl+Q"; - Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, key.KeyCode); + var key = (Key)str; + Assert.Equal (expectedKeyCode, key.KeyCode); } [Theory] @@ -190,12 +196,37 @@ public void Casting_Between_Key_And_KeyCode (KeyCode keyCode, KeyCode key) [InlineData ('\'', (KeyCode)'\'')] [InlineData ('\xFFFF', (KeyCode)0xFFFF)] [InlineData ('\x0', (KeyCode)0x0)] - public void Constructor_Char (char ch, KeyCode expectedKeyCode) + public void Constructor_Char_Int (char ch, KeyCode expectedKeyCode) { var key = new Key (ch); Assert.Equal (expectedKeyCode, key.KeyCode); + + key = new ((int)ch); + Assert.Equal (expectedKeyCode, key.KeyCode); + } + + [Theory] + [InlineData (0x1F4C4, (KeyCode)0x1F4C4, "📄")] + [InlineData (0x1F64B, (KeyCode)0x1F64B, "🙋")] + [InlineData (0x1F46A, (KeyCode)0x1F46A, "👪")] + public void Constructor_Int_Non_Bmp (int value, KeyCode expectedKeyCode, string expectedString) + { + var key = new Key (value); + Assert.Equal (expectedKeyCode, key.KeyCode); + Assert.Equal (expectedString, key.AsRune.ToString ()); + Assert.Equal (expectedString, key.ToString ()); } + [Theory] + [InlineData (-1)] + [InlineData (0x11FFFF)] + public void Constructor_Int_Invalid_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); } + + [Theory] + [InlineData ('\ud83d')] + [InlineData ('\udcc4')] + public void Constructor_Int_Surrogate_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); } + [Fact] public void Constructor_Default_ShouldSetKeyToNull () { From c1397fd1e58646409c8381c634aac1caa926935f Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 12 Mar 2025 17:02:42 +0000 Subject: [PATCH 2/7] Fix TextField non-BMP issues --- Terminal.Gui/Views/TextField.cs | 18 ++++-------------- Tests/UnitTests/Views/TextFieldTests.cs | 10 ++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 6c872d95c4..a6c1e8ff5a 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -730,21 +730,11 @@ public virtual void DeleteCharRight () /// Use the previous cursor position. public void InsertText (string toAdd, bool useOldCursorPos = true) { - foreach (char ch in toAdd) + foreach (Rune rune in toAdd.EnumerateRunes ()) { - Key key; - - try - { - key = ch; - } - catch (Exception) - { - throw new ArgumentException ( - $"Cannot insert character '{ch}' because it does not map to a Key" - ); - } - + // All rune can be mapped to a Key and no exception will throw here because + // EnumerateRunes will replace a surrogate char with the Rune.ReplacementChar + Key key = rune.Value; InsertText (key, useOldCursorPos); } } diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 14c84a282f..06f878d207 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -2178,4 +2178,14 @@ public void Draw_Esc_Rune () tf.Dispose (); } + + [Fact] + public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair () + { + var tf = new TextField (); + //📄 == \ud83d\udcc4 == \U0001F4C4 + // � == Rune.ReplacementChar + tf.InsertText ("aA,;\ud83d\udcc4\U0001F4C4\udcc4\ud83d"); + Assert.Equal ("aA,;📄📄��", tf.Text); + } } From e0f8678e5a71a6d3eac8cf5d59d09c4b7fa4b584 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 12 Mar 2025 17:03:38 +0000 Subject: [PATCH 3/7] Fix TextField PositionCursor. --- Terminal.Gui/Views/TextField.cs | 2 +- Tests/UnitTests/Views/TextFieldTests.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index a6c1e8ff5a..3d83aaaafb 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -1104,7 +1104,7 @@ public virtual void Paste () TextModel.SetCol (ref col, Viewport.Width - 1, cols); } - int pos = _cursorPosition - ScrollOffset + Math.Min (Viewport.X, 0); + int pos = col - ScrollOffset + Math.Min (Viewport.X, 0); Move (pos, 0); return new Point (pos, 0); diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 06f878d207..3e6f618f13 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -2188,4 +2188,22 @@ public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair () tf.InsertText ("aA,;\ud83d\udcc4\U0001F4C4\udcc4\ud83d"); Assert.Equal ("aA,;📄📄��", tf.Text); } + + [Fact] + public void PositionCursor_Respect_GetColumns () + { + var tf = new TextField () { Width = 5 }; + tf.BeginInit (); + tf.EndInit (); + + tf.NewKeyDownEvent (new ("📄")); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new (2, 0), tf.PositionCursor ()); + Assert.Equal ("📄", tf.Text); + + tf.NewKeyDownEvent (new (KeyCode.A)); + Assert.Equal (2, tf.CursorPosition); + Assert.Equal (new (3, 0), tf.PositionCursor ()); + Assert.Equal ("📄a", tf.Text); + } } From b1bbefae2eb60e01bcf61dee5057c3612182da5b Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 12 Mar 2025 17:41:04 +0000 Subject: [PATCH 4/7] Reformat --- Tests/UnitTests/Views/TextFieldTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 3e6f618f13..11d8ee92c2 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -2183,6 +2183,7 @@ public void Draw_Esc_Rune () public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair () { var tf = new TextField (); + //📄 == \ud83d\udcc4 == \U0001F4C4 // � == Rune.ReplacementChar tf.InsertText ("aA,;\ud83d\udcc4\U0001F4C4\udcc4\ud83d"); @@ -2192,7 +2193,7 @@ public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair () [Fact] public void PositionCursor_Respect_GetColumns () { - var tf = new TextField () { Width = 5 }; + var tf = new TextField { Width = 5 }; tf.BeginInit (); tf.EndInit (); From 1f4f38aab44475d921a22df136b198b33811528c Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 13 Mar 2025 02:39:00 +0000 Subject: [PATCH 5/7] Add IsValidInput method to handle clipboard paste when pressing CTRL+V in WT --- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 26 +++++++++++++++++++ .../CursesDriver/CursesDriver.cs | 7 +++-- .../ConsoleDrivers/NetDriver/NetDriver.cs | 7 +++-- .../WindowsDriver/WindowsDriver.cs | 9 ++++--- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 46b7f50332..0ee7203c7e 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -691,6 +691,32 @@ public void OnMouseEvent (MouseEventArgs a) /// If simulates the Ctrl key being pressed. public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); + internal char _highSurrogate = '\0'; + + internal bool IsValidInput (KeyCode keyCode, out KeyCode result) + { + result = keyCode; + + if (char.IsHighSurrogate ((char)keyCode)) + { + _highSurrogate = (char)keyCode; + + return false; + } + + if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode)) + { + result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value; + _highSurrogate = '\0'; + } + else if (_highSurrogate > 0) + { + _highSurrogate = '\0'; + } + + return true; + } + #endregion private AnsiRequestScheduler? _scheduler; diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 48bcb713f5..f8101d5468 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -924,8 +924,11 @@ internal void ProcessInput () k &= ~KeyCode.Space; } - OnKeyDown (new Key (k)); - OnKeyUp (new Key (k)); + if (IsValidInput (k, out k)) + { + OnKeyDown (new (k)); + OnKeyUp (new (k)); + } } } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs index 2ede00a000..59d75b5817 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs @@ -321,8 +321,11 @@ private void ProcessInput (InputResult inputEvent) break; } - OnKeyDown (new (map)); - OnKeyUp (new (map)); + if (IsValidInput (map, out map)) + { + OnKeyDown (new (map)); + OnKeyUp (new (map)); + } break; case EventType.Mouse: diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 1f74c0321e..1fc1eb8424 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -507,9 +507,12 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) break; } - // This follows convention in NetDriver - OnKeyDown (new Key (map)); - OnKeyUp (new Key (map)); + if (IsValidInput (map, out map)) + { + // This follows convention in NetDriver + OnKeyDown (new (map)); + OnKeyUp (new (map)); + } break; From 8af4ff3279a5078ee5e67766f1b17da92786c71b Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 13 Mar 2025 13:15:42 +0000 Subject: [PATCH 6/7] Add handle IsValidInput in FakeDriver and unit tests --- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 10 +- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 8 +- .../ConsoleDrivers/ConsoleDriverTests.cs | 111 ++++++++++++++++++ 3 files changed, 126 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 0ee7203c7e..b172781168 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -708,8 +708,16 @@ internal bool IsValidInput (KeyCode keyCode, out KeyCode result) { result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value; _highSurrogate = '\0'; + + return true; + } + + if (char.IsSurrogate ((char)keyCode)) + { + return false; } - else if (_highSurrogate > 0) + + if (_highSurrogate > 0) { _highSurrogate = '\0'; } diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 5e9a883d72..3dc0082428 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -352,8 +352,12 @@ private void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo) } KeyCode map = MapKey (consoleKeyInfo); - OnKeyDown (new Key (map)); - OnKeyUp (new Key (map)); + + if (IsValidInput (map, out map)) + { + OnKeyDown (new (map)); + OnKeyUp (new (map)); + } //OnKeyPressed (new KeyEventArgs (map)); } diff --git a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs index fbede01b02..de29652781 100644 --- a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -283,4 +283,115 @@ public void TerminalResized_Simulation (Type driverType) // Application.Run (win); // Application.Shutdown (); // } + + [Theory] + [InlineData ('\ud83d', '\udcc4')] // This seems right sequence but Stack is LIFO + [InlineData ('\ud83d', '\ud83d')] + [InlineData ('\udcc4', '\udcc4')] + public void FakeDriver_IsValidInput_Wrong_Surrogate_Sequence (char c1, char c2) + { + var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver)); + Application.Init (driver); + + Stack mKeys = new ( + [ + new ('a', ConsoleKey.A, false, false, false), + new (c1, ConsoleKey.None, false, false, false), + new (c2, ConsoleKey.None, false, false, false) + ]); + + Console.MockKeyPresses = mKeys; + + Toplevel top = new (); + var view = new View { CanFocus = true }; + var rText = ""; + var idx = 0; + + view.KeyDown += (s, e) => + { + Assert.Equal (new ('a'), e.AsRune); + Assert.Equal ("a", e.AsRune.ToString ()); + rText += e.AsRune; + e.Handled = true; + idx++; + }; + top.Add (view); + + Application.Iteration += (s, a) => + { + if (mKeys.Count == 0) + { + Application.RequestStop (); + } + }; + + Application.Run (top); + + Assert.Equal ("a", rText); + Assert.Equal (1, idx); + Assert.Equal (0, ((FakeDriver)driver)._highSurrogate); + + top.Dispose (); + + // Shutdown must be called to safely clean up Application if Init has been called + Application.Shutdown (); + } + + [Fact] + public void FakeDriver_IsValidInput_Correct_Surrogate_Sequence () + { + var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver)); + Application.Init (driver); + + Stack mKeys = new ( + [ + new ('a', ConsoleKey.A, false, false, false), + new ('\udcc4', ConsoleKey.None, false, false, false), + new ('\ud83d', ConsoleKey.None, false, false, false) + ]); + + Console.MockKeyPresses = mKeys; + + Toplevel top = new (); + var view = new View { CanFocus = true }; + var rText = ""; + var idx = 0; + + view.KeyDown += (s, e) => + { + if (idx == 0) + { + Assert.Equal (new (0x1F4C4), e.AsRune); + Assert.Equal ("📄", e.AsRune.ToString ()); + } + else + { + Assert.Equal (new ('a'), e.AsRune); + Assert.Equal ("a", e.AsRune.ToString ()); + } + + rText += e.AsRune; + e.Handled = true; + idx++; + }; + top.Add (view); + + Application.Iteration += (s, a) => + { + if (mKeys.Count == 0) + { + Application.RequestStop (); + } + }; + + Application.Run (top); + + Assert.Equal ("📄a", rText); + Assert.Equal (2, idx); + + top.Dispose (); + + // Shutdown must be called to safely clean up Application if Init has been called + Application.Shutdown (); + } } From ab591d1d644a5b23a1f5109cbe37f3dac1563946 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 13 Mar 2025 18:16:53 +0100 Subject: [PATCH 7/7] Fixes #3984 - `Margin` w/out shadow should not force draw (#3985) * shortcut tests * Generic demos * Optimize Margin to not defer draw if there's no shadow --- Terminal.Gui/View/Adornment/Margin.cs | 2 +- Terminal.Gui/View/View.Drawing.cs | 38 +++++++++++++-------- Terminal.Gui/Views/GraphView/Annotations.cs | 2 +- Terminal.Gui/Views/Menu/Menu.cs | 2 +- Terminal.Gui/Views/TileView.cs | 2 +- UICatalog/Scenarios/Generic.cs | 18 ++++++++-- 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/Terminal.Gui/View/Adornment/Margin.cs b/Terminal.Gui/View/Adornment/Margin.cs index ac19770282..9d5dc44e88 100644 --- a/Terminal.Gui/View/Adornment/Margin.cs +++ b/Terminal.Gui/View/Adornment/Margin.cs @@ -48,7 +48,7 @@ public Margin (View parent) : base (parent) internal void CacheClip () { - if (Thickness != Thickness.Empty) + if (Thickness != Thickness.Empty && ShadowStyle != ShadowStyle.None) { // PERFORMANCE: How expensive are these clones? _cachedClip = GetClip ()?.Clone (); diff --git a/Terminal.Gui/View/View.Drawing.cs b/Terminal.Gui/View/View.Drawing.cs index 9d5521c025..09954c701b 100644 --- a/Terminal.Gui/View/View.Drawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -27,6 +27,7 @@ internal static void Draw (IEnumerable views, bool force) view.Draw (context); } + // Draw the margins (those whith Shadows) last to ensure they are drawn on top of the content. Margin.DrawMargins (viewsArray); } @@ -57,9 +58,9 @@ public void Draw (DrawContext? context = null) { // ------------------------------------ // Draw the Border and Padding. - // Note Margin is special-cased and drawn in a separate pass to support + // Note Margin with a Shadow is special-cased and drawn in a separate pass to support // transparent shadows. - DoDrawBorderAndPadding (originalClip); + DoDrawAdornments (originalClip); SetClip (originalClip); // ------------------------------------ @@ -106,7 +107,7 @@ public void Draw (DrawContext? context = null) // ------------------------------------ // Re-draw the border and padding subviews // HACK: This is a hack to ensure that the border and padding subviews are drawn after the line canvas. - DoDrawBorderAndPaddingSubViews (); + DoDrawAdornmentsSubViews (); // ------------------------------------ // Advance the diagnostics draw indicator @@ -116,8 +117,8 @@ public void Draw (DrawContext? context = null) } // ------------------------------------ - // This causes the Margin to be drawn in a second pass - // PERFORMANCE: If there is a Margin, it will be redrawn each iteration of the main loop. + // This causes the Margin to be drawn in a second pass if it has a ShadowStyle + // PERFORMANCE: If there is a Margin w/ Shadow, it will be redrawn each iteration of the main loop. Margin?.CacheClip (); // ------------------------------------ @@ -131,8 +132,11 @@ public void Draw (DrawContext? context = null) #region DrawAdornments - private void DoDrawBorderAndPaddingSubViews () + private void DoDrawAdornmentsSubViews () { + + // NOTE: We do not support subviews of Margin? + if (Border?.SubViews is { } && Border.Thickness != Thickness.Empty) { // PERFORMANCE: Get the check for DrawIndicator out of this somehow. @@ -164,7 +168,7 @@ private void DoDrawBorderAndPaddingSubViews () } } - private void DoDrawBorderAndPadding (Region? originalClip) + private void DoDrawAdornments (Region? originalClip) { if (this is Adornment) { @@ -194,27 +198,28 @@ private void DoDrawBorderAndPadding (Region? originalClip) // A SubView may add to the LineCanvas. This ensures any Adornment LineCanvas updates happen. Border?.SetNeedsDraw (); Padding?.SetNeedsDraw (); + Margin?.SetNeedsDraw (); } - if (OnDrawingBorderAndPadding ()) + if (OnDrawingAdornments ()) { return; } // TODO: add event. - DrawBorderAndPadding (); + DrawAdornments (); } /// - /// Causes and to be drawn. + /// Causes , , and to be drawn. /// /// /// - /// is drawn in a separate pass. + /// is drawn in a separate pass if is set. /// /// - public void DrawBorderAndPadding () + public void DrawAdornments () { // We do not attempt to draw Margin. It is drawn in a separate pass. @@ -230,6 +235,11 @@ public void DrawBorderAndPadding () Padding?.Draw (); } + + if (Margin is { } && Margin.Thickness != Thickness.Empty && Margin.ShadowStyle == ShadowStyle.None) + { + Margin?.Draw (); + } } private void ClearFrame () @@ -255,7 +265,7 @@ private void ClearFrame () /// false (the default), this method will cause the be prepared to be rendered. /// /// to stop further drawing of the Adornments. - protected virtual bool OnDrawingBorderAndPadding () { return false; } + protected virtual bool OnDrawingAdornments () { return false; } #endregion DrawAdornments @@ -635,7 +645,7 @@ private void DoRenderLineCanvas () /// /// Gets or sets whether this View will use it's SuperView's for rendering any /// lines. If the rendering of any borders drawn by this Frame will be done by its parent's - /// SuperView. If (the default) this View's method will + /// SuperView. If (the default) this View's method will /// be /// called to render the borders. /// diff --git a/Terminal.Gui/Views/GraphView/Annotations.cs b/Terminal.Gui/Views/GraphView/Annotations.cs index 02b67c974b..7d41ae4f95 100644 --- a/Terminal.Gui/Views/GraphView/Annotations.cs +++ b/Terminal.Gui/Views/GraphView/Annotations.cs @@ -150,7 +150,7 @@ public void Render (GraphView graph) if (BorderStyle != LineStyle.None) { - DrawBorderAndPadding (); + DrawAdornments (); RenderLineCanvas (); } diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 1ea62b3abd..3f1e406fb5 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -831,7 +831,7 @@ private void Top_DrawComplete (object? sender, DrawEventArgs e) return; } - DrawBorderAndPadding (); + DrawAdornments (); RenderLineCanvas (); // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework. diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs index f3aed9c2ab..398474f48a 100644 --- a/Terminal.Gui/Views/TileView.cs +++ b/Terminal.Gui/Views/TileView.cs @@ -173,7 +173,7 @@ public int IndexOf (View toFind, bool recursive = false) /// Overridden so no Frames get drawn /// - protected override bool OnDrawingBorderAndPadding () { return true; } + protected override bool OnDrawingAdornments () { return true; } /// protected override bool OnRenderingLineCanvas () { return false; } diff --git a/UICatalog/Scenarios/Generic.cs b/UICatalog/Scenarios/Generic.cs index 3fc926dedf..33ca9e73a8 100644 --- a/UICatalog/Scenarios/Generic.cs +++ b/UICatalog/Scenarios/Generic.cs @@ -18,7 +18,21 @@ public override void Main () Title = GetQuitKeyAndName (), }; - var button = new Button { Id = "button", X = Pos.Center (), Y = 1, Text = "_Press me!" }; + FrameView frame = new () + { + Height = Dim.Fill (), + Width = Dim.Fill (), + Title = "Frame" + }; + appWindow.Add (frame); + + var button = new Shortcut () + { + Id = "button", + X = Pos.Center (), + Y = 1, + Text = "_Press me!" + }; button.Accepting += (s, e) => { @@ -27,7 +41,7 @@ public override void Main () MessageBox.ErrorQuery ("Error", "You pressed the button!", "_Ok"); }; - appWindow.Add (button); + frame.Add (button); // Run - Start the application. Application.Run (appWindow);