Tig 3 月之前
父节点
当前提交
3d4d7f3fab
共有 100 个文件被更改,包括 2646 次插入915 次删除
  1. 31 0
      Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs
  2. 84 0
      Benchmarks/Text/StringExtensions/ToStringEnumerable.cs
  3. 97 0
      Benchmarks/Text/TextFormatter/RemoveHotKeySpecifier.cs
  4. 90 0
      Benchmarks/Text/TextFormatter/ReplaceCRLFWithSpace.cs
  5. 115 0
      Benchmarks/Text/TextFormatter/StripCRLF.cs
  6. 18 18
      Directory.Packages.props
  7. 0 1
      ReactiveExample/LoginViewModel.cs
  8. 1 1
      Terminal.Gui/Application/Application.Initialization.cs
  9. 6 2
      Terminal.Gui/Application/Application.Run.cs
  10. 2 2
      Terminal.Gui/Application/ApplicationNavigation.cs
  11. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence.cs
  12. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs
  13. 4 4
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs
  14. 5 5
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs
  15. 2 2
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs
  16. 49 47
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs
  17. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs
  18. 3 3
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs
  19. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs
  20. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs
  21. 3 3
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs
  22. 3 3
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs
  23. 5 3
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs
  24. 5 3
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs
  25. 1 1
      Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs
  26. 3 3
      Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
  27. 29 3
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
  28. 20 0
      Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
  29. 1 1
      Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs
  30. 4 4
      Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs
  31. 1 2
      Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs
  32. 29 21
      Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs
  33. 2 2
      Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs
  34. 28 20
      Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs
  35. 5 4
      Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs
  36. 35 6
      Terminal.Gui/Drawing/Cell.cs
  37. 155 69
      Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs
  38. 4 0
      Terminal.Gui/Input/InputBindings.cs
  39. 3 1
      Terminal.Gui/Terminal.Gui.csproj
  40. 1 6
      Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs
  41. 44 4
      Terminal.Gui/Text/StringExtensions.cs
  42. 248 174
      Terminal.Gui/Text/TextFormatter.cs
  43. 2 17
      Terminal.Gui/View/Adornment/Adornment.cs
  44. 1 1
      Terminal.Gui/View/Adornment/Border.cs
  45. 3 3
      Terminal.Gui/View/Adornment/Margin.cs
  46. 1 1
      Terminal.Gui/View/Layout/Dim.cs
  47. 23 23
      Terminal.Gui/View/Layout/DimAuto.cs
  48. 4 4
      Terminal.Gui/View/Layout/DimAutoStyle.cs
  49. 1 1
      Terminal.Gui/View/Layout/LayoutEventArgs.cs
  50. 2 2
      Terminal.Gui/View/Layout/PosAlign.cs
  51. 2 2
      Terminal.Gui/View/SuperViewChangedEventArgs.cs
  52. 3 3
      Terminal.Gui/View/View.Adornments.cs
  53. 2 2
      Terminal.Gui/View/View.Command.cs
  54. 4 4
      Terminal.Gui/View/View.Content.cs
  55. 56 46
      Terminal.Gui/View/View.Drawing.cs
  56. 188 97
      Terminal.Gui/View/View.Hierarchy.cs
  57. 3 3
      Terminal.Gui/View/View.Keyboard.cs
  58. 33 33
      Terminal.Gui/View/View.Layout.cs
  59. 6 6
      Terminal.Gui/View/View.Mouse.cs
  60. 14 14
      Terminal.Gui/View/View.Navigation.cs
  61. 10 18
      Terminal.Gui/View/View.cs
  62. 1 1
      Terminal.Gui/View/ViewArrangement.cs
  63. 3 3
      Terminal.Gui/View/ViewportSettings.cs
  64. 16 16
      Terminal.Gui/Views/Bar.cs
  65. 7 7
      Terminal.Gui/Views/ComboBox.cs
  66. 1 1
      Terminal.Gui/Views/FileDialog.cs
  67. 1 1
      Terminal.Gui/Views/GraphView/Annotations.cs
  68. 2 2
      Terminal.Gui/Views/HexView.cs
  69. 6 6
      Terminal.Gui/Views/Label.cs
  70. 30 12
      Terminal.Gui/Views/Menu/ContextMenu.cs
  71. 11 1
      Terminal.Gui/Views/Menu/Menu.cs
  72. 60 39
      Terminal.Gui/Views/Menu/MenuBar.cs
  73. 4 9
      Terminal.Gui/Views/MenuBarv2.cs
  74. 7 10
      Terminal.Gui/Views/Menuv2.cs
  75. 4 0
      Terminal.Gui/Views/MessageBox.cs
  76. 1 1
      Terminal.Gui/Views/RadioGroup.cs
  77. 2 2
      Terminal.Gui/Views/ScrollBar/ScrollBar.cs
  78. 5 5
      Terminal.Gui/Views/Shortcut.cs
  79. 1 1
      Terminal.Gui/Views/Slider.cs
  80. 19 31
      Terminal.Gui/Views/SpinnerView/SpinnerView.cs
  81. 7 12
      Terminal.Gui/Views/StatusBar.cs
  82. 4 4
      Terminal.Gui/Views/TabView/TabRow.cs
  83. 91 3
      Terminal.Gui/Views/TabView/TabView.cs
  84. 13 10
      Terminal.Gui/Views/TextField.cs
  85. 12 7
      Terminal.Gui/Views/TextView.cs
  86. 7 7
      Terminal.Gui/Views/TileView.cs
  87. 14 14
      Terminal.Gui/Views/Toplevel.cs
  88. 3 3
      Terminal.Gui/Views/Wizard/Wizard.cs
  89. 4 4
      Terminal.Gui/Views/Wizard/WizardStep.cs
  90. 7 0
      Terminal.sln
  91. 2 0
      Terminal.sln.DotSettings
  92. 89 0
      TerminalGuiFluentTesting/ClassDiagram1.cd
  93. 34 0
      TerminalGuiFluentTesting/FakeInput.cs
  94. 6 0
      TerminalGuiFluentTesting/FakeNetInput.cs
  95. 28 0
      TerminalGuiFluentTesting/FakeOutput.cs
  96. 6 0
      TerminalGuiFluentTesting/FakeWindowsInput.cs
  97. 551 0
      TerminalGuiFluentTesting/GuiTestContext.cs
  98. 53 0
      TerminalGuiFluentTesting/NetSequences.cs
  99. 14 0
      TerminalGuiFluentTesting/TerminalGuiFluentTesting.csproj
  100. 21 0
      TerminalGuiFluentTesting/TextWriterLogger.cs

+ 31 - 0
Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs

@@ -0,0 +1,31 @@
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils;
+
+[MemoryDiagnoser]
+// Hide useless column from results.
+[HideColumns ("writer")]
+public class CSI_SetVsWrite
+{
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (TextWriterSource))]
+    public TextWriter Set (TextWriter writer)
+    {
+        writer.Write (Tui.EscSeqUtils.CSI_SetCursorPosition (1, 1));
+        return writer;
+    }
+
+    [Benchmark]
+    [ArgumentsSource (nameof (TextWriterSource))]
+    public TextWriter Write (TextWriter writer)
+    {
+        Tui.EscSeqUtils.CSI_WriteCursorPosition (writer, 1, 1);
+        return writer;
+    }
+
+    public static IEnumerable<object> TextWriterSource ()
+    {
+        return [StringWriter.Null];
+    }
+}

+ 84 - 0
Benchmarks/Text/StringExtensions/ToStringEnumerable.cs

@@ -0,0 +1,84 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.StringExtensions;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.StringExtensions.ToString(IEnumerable{Rune})"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+public class ToStringEnumerable
+{
+
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Previous (IEnumerable<Rune> runes, int len)
+    {
+        return StringConcatInLoop (runes);
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation with char buffer and
+    /// fallback to rune chars appending to StringBuilder.
+    /// </summary>
+    /// <param name="runes"></param>
+    /// <returns></returns>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Current (IEnumerable<Rune> runes, int len)
+    {
+        return Tui.StringExtensions.ToString (runes);
+    }
+
+    /// <summary>
+    /// Previous implementation with string concatenation in a loop.
+    /// </summary>
+    private static string StringConcatInLoop (IEnumerable<Rune> runes)
+    {
+        var str = string.Empty;
+
+        foreach (Rune rune in runes)
+        {
+            str += rune.ToString ();
+        }
+
+        return str;
+    }
+
+    public IEnumerable<object []> DataSource ()
+    {
+        // Extra length argument as workaround for the summary grouping
+        // different length collections to same baseline making comparison difficult.
+        foreach (string text in GetTextData ())
+        {
+            Rune [] runes = [..text.EnumerateRunes ()];
+            yield return [runes, runes.Length];
+        }
+    }
+
+    private IEnumerable<string> GetTextData ()
+    {
+        string textSource =
+            """
+            Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
+            Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
+            Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
+            Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
+            Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
+            """;
+
+        int[] lengths = [1, 10, 100, textSource.Length / 2, textSource.Length];
+
+        foreach (int length in lengths)
+        {
+            yield return textSource [..length];
+        }
+
+        string textLongerThanStackallocThreshold = string.Concat(Enumerable.Repeat(textSource, 10));
+        yield return textLongerThanStackallocThreshold;
+    }
+}

+ 97 - 0
Benchmarks/Text/TextFormatter/RemoveHotKeySpecifier.cs

@@ -0,0 +1,97 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.TextFormatter;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.TextFormatter.RemoveHotKeySpecifier"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof(Tui.TextFormatter))]
+public class RemoveHotKeySpecifier
+{
+    // Omit from summary table.
+    private static readonly Rune HotkeySpecifier = (Rune)'_';
+
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Previous (string text, int hotPos)
+    {
+        return StringConcatLoop (text, hotPos, HotkeySpecifier);
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation with stackalloc char buffer and fallback to rented array.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Current (string text, int hotPos)
+    {
+        return Tui.TextFormatter.RemoveHotKeySpecifier (text, hotPos, HotkeySpecifier);
+    }
+
+    /// <summary>
+    /// Previous implementation with string concatenation in a loop.
+    /// </summary>
+    public static string StringConcatLoop (string text, int hotPos, Rune hotKeySpecifier)
+    {
+        if (string.IsNullOrEmpty (text))
+        {
+            return text;
+        }
+
+        // Scan 
+        var start = string.Empty;
+        var i = 0;
+
+        foreach (Rune c in text.EnumerateRunes ())
+        {
+            if (c == hotKeySpecifier && i == hotPos)
+            {
+                i++;
+
+                continue;
+            }
+
+            start += c;
+            i++;
+        }
+
+        return start;
+    }
+
+    public IEnumerable<object []> DataSource ()
+    {
+        string[] texts = [
+            "",
+			// Typical scenario.
+			"_Save file (Ctrl+S)",
+			// Medium text, hotkey specifier somewhere in the middle.
+			"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed euismod metus. _Phasellus lectus metus, ultricies a commodo quis, facilisis vitae nulla.",
+			// Long text, hotkey specifier almost at the beginning.
+			"Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. _Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ. " +
+            "Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé. " +
+            "Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.",
+			// Long text, hotkey specifier almost at the end.
+			"Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ. " +
+            "Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé. " +
+            "Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. _Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.",
+        ];
+
+        foreach (string text in texts)
+        {
+            int hotPos = text.EnumerateRunes()
+                .Select((r, i) => r == HotkeySpecifier ? i : -1)
+                .FirstOrDefault(i => i > -1, -1);
+
+            yield return [text, hotPos];
+        }
+
+        // Typical scenario but without hotkey and with misleading position.
+        yield return ["Save file (Ctrl+S)", 3];
+    }
+}

+ 90 - 0
Benchmarks/Text/TextFormatter/ReplaceCRLFWithSpace.cs

@@ -0,0 +1,90 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.TextFormatter;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.TextFormatter.ReplaceCRLFWithSpace"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.TextFormatter))]
+public class ReplaceCRLFWithSpace
+{
+
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Previous (string str)
+    {
+        return ToRuneListReplaceImplementation (str);
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Current (string str)
+    {
+        return Tui.TextFormatter.ReplaceCRLFWithSpace (str);
+    }
+
+    /// <summary>
+    /// Previous implementation with intermediate rune list.
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
+    private static string ToRuneListReplaceImplementation (string str)
+    {
+        var runes = str.ToRuneList ();
+        for (int i = 0; i < runes.Count; i++)
+        {
+            switch (runes [i].Value)
+            {
+                case '\n':
+                    runes [i] = (Rune)' ';
+                    break;
+
+                case '\r':
+                    if ((i + 1) < runes.Count && runes [i + 1].Value == '\n')
+                    {
+                        runes [i] = (Rune)' ';
+                        runes.RemoveAt (i + 1);
+                        i++;
+                    }
+                    else
+                    {
+                        runes [i] = (Rune)' ';
+                    }
+                    break;
+            }
+        }
+        return Tui.StringExtensions.ToString (runes);
+    }
+
+    public IEnumerable<object> DataSource ()
+    {
+        // Extreme newline scenario
+        yield return "E\r\nx\r\nt\r\nr\r\ne\r\nm\r\ne\r\nn\r\ne\r\nw\r\nl\r\ni\r\nn\r\ne\r\ns\r\nc\r\ne\r\nn\r\na\r\nr\r\ni\r\no\r\n";
+        // Long text with few line endings
+        yield return
+            """
+			Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
+			Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
+			Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
+			Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
+			Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
+			"""
+            // Consistent line endings between systems for more consistent performance evaluation.
+            .ReplaceLineEndings ("\r\n");
+        // Long text without line endings
+        yield return
+            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed euismod metus. Phasellus lectus metus, ultricies a commodo quis, facilisis vitae nulla. " +
+            "Curabitur mollis ex nisl, vitae mattis nisl consequat at. Aliquam dolor lectus, tincidunt ac nunc eu, elementum molestie lectus. Donec lacinia eget dolor a scelerisque. " +
+            "Aenean elementum molestie rhoncus. Duis id ornare lorem. Nam eget porta sapien. Etiam rhoncus dignissim leo, ac suscipit magna finibus eu. Curabitur hendrerit elit erat, sit amet suscipit felis condimentum ut. " +
+            "Nullam semper tempor mi, nec semper quam fringilla eu. Aenean sit amet pretium augue, in posuere ante. Aenean convallis porttitor purus, et posuere velit dictum eu.";
+    }
+}

+ 115 - 0
Benchmarks/Text/TextFormatter/StripCRLF.cs

@@ -0,0 +1,115 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Tui = Terminal.Gui;
+
+namespace Terminal.Gui.Benchmarks.Text.TextFormatter;
+
+/// <summary>
+/// Benchmarks for <see cref="Tui.TextFormatter.StripCRLF"/> performance fine-tuning.
+/// </summary>
+[MemoryDiagnoser]
+[BenchmarkCategory (nameof (Tui.TextFormatter))]
+public class StripCRLF
+{
+    /// <summary>
+    /// Benchmark for previous implementation.
+    /// </summary>
+    /// <param name="str"></param>
+    /// <param name="keepNewLine"></param>
+    /// <returns></returns>
+    [Benchmark]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Previous (string str, bool keepNewLine)
+    {
+        return RuneListToString (str, keepNewLine);
+    }
+
+    /// <summary>
+    /// Benchmark for current implementation with StringBuilder and char span index of search.
+    /// </summary>
+    [Benchmark (Baseline = true)]
+    [ArgumentsSource (nameof (DataSource))]
+    public string Current (string str, bool keepNewLine)
+    {
+        return Tui.TextFormatter.StripCRLF (str, keepNewLine);
+    }
+
+    /// <summary>
+    /// Previous implementation with intermediate rune list.
+    /// </summary>
+    private static string RuneListToString (string str, bool keepNewLine = false)
+    {
+        List<Rune> runes = str.ToRuneList ();
+
+        for (var i = 0; i < runes.Count; i++)
+        {
+            switch ((char)runes [i].Value)
+            {
+                case '\n':
+                    if (!keepNewLine)
+                    {
+                        runes.RemoveAt (i);
+                    }
+
+                    break;
+
+                case '\r':
+                    if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
+                    {
+                        runes.RemoveAt (i);
+
+                        if (!keepNewLine)
+                        {
+                            runes.RemoveAt (i);
+                        }
+
+                        i++;
+                    }
+                    else
+                    {
+                        if (!keepNewLine)
+                        {
+                            runes.RemoveAt (i);
+                        }
+                    }
+
+                    break;
+            }
+        }
+
+        return Tui.StringExtensions.ToString (runes);
+    }
+
+    public IEnumerable<object []> DataSource ()
+    {
+        string[] textPermutations = [
+            // Extreme newline scenario
+            "E\r\nx\r\nt\r\nr\r\ne\r\nm\r\ne\r\nn\r\ne\r\nw\r\nl\r\ni\r\nn\r\ne\r\ns\r\nc\r\ne\r\nn\r\na\r\nr\r\ni\r\no\r\n",
+            // Long text with few line endings
+            """
+            Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
+            Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
+            Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
+            Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
+            Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
+            """
+            // Consistent line endings between systems for more consistent performance evaluation.
+            .ReplaceLineEndings ("\r\n"),
+            // Long text without line endings
+            "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed euismod metus. Phasellus lectus metus, ultricies a commodo quis, facilisis vitae nulla. " +
+            "Curabitur mollis ex nisl, vitae mattis nisl consequat at. Aliquam dolor lectus, tincidunt ac nunc eu, elementum molestie lectus. Donec lacinia eget dolor a scelerisque. " +
+            "Aenean elementum molestie rhoncus. Duis id ornare lorem. Nam eget porta sapien. Etiam rhoncus dignissim leo, ac suscipit magna finibus eu. Curabitur hendrerit elit erat, sit amet suscipit felis condimentum ut. " +
+            "Nullam semper tempor mi, nec semper quam fringilla eu. Aenean sit amet pretium augue, in posuere ante. Aenean convallis porttitor purus, et posuere velit dictum eu."
+        ];
+
+        bool[] newLinePermutations = [true, false];
+
+        foreach (string text in textPermutations)
+        {
+            foreach (bool keepNewLine in newLinePermutations)
+            {
+                yield return [text, keepNewLine];
+            }
+        }
+    }
+}

+ 18 - 18
Directory.Packages.props

@@ -7,41 +7,41 @@
 		<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="[8,9)" />
 
 		<PackageVersion Include="ColorHelper" Version="[1.8.1,2)" />
-		<PackageVersion Include="JetBrains.Annotations" Version="[2024.2.0,)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.10,5)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.10,5)" />
-		<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.10,5)" />
-		<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
-		<PackageVersion Include="System.IO.Abstractions" Version="[21.0.22,22)" />
+		<PackageVersion Include="JetBrains.Annotations" Version="[2024.3.0,)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis" Version="[4.13,5)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="[4.13,5)" />
+		<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="[4.13,5)" />
+		<PackageVersion Include="Microsoft.Extensions.Logging" Version="[9.0.2,10)" />
+		<PackageVersion Include="System.IO.Abstractions" Version="[22.0.11,23)" />
 		<PackageVersion Include="System.Text.Json" Version="[8.0.5,9)" />
 		<PackageVersion Include="Wcwidth" Version="[2,3)" />
 
-		<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21,2)" />
+		<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21.2,2)" />
 		<PackageVersion Include="Serilog" Version="4.2.0" />
 		<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
 		<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
 		<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
-		<PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.5,4)" />
+		<PackageVersion Include="SixLabors.ImageSharp" Version="[3.1.7,4)" />
 		<PackageVersion Include="CsvHelper" Version="[33.0.1,34)" />
 		<PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" />
 		<PackageVersion Include="System.CommandLine" Version="[2.0.0-beta4.22272.1,3)" />
 
 		<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
 
-		<PackageVersion Include="CommunityToolkit.Mvvm" Version="[8.2.2,9)" />
-		<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[8,9)" />	
-		<PackageVersion Include="ReactiveUI" Version="[20.1.1,21)" />
+		<PackageVersion Include="CommunityToolkit.Mvvm" Version="[8.4.0,9)" />
+		<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[9.0.2,10)" />	
+		<PackageVersion Include="ReactiveUI" Version="[20.1.63,21)" />
 		<PackageVersion Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="[1.3.1,2)" />
-		<PackageVersion Include="ReactiveUI.SourceGenerators" Version="[1.0.3,2)"/>	
+		<PackageVersion Include="ReactiveUI.SourceGenerators" Version="[2.1.8,3)"/>	
 
-		<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.10,18)" />
-		<PackageVersion Include="Moq" Version="[4.20.70,5)" />
-		<PackageVersion Include="ReportGenerator" Version="[5.3.8,6)" />
-		<PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[21.0.29,22)" />
-		<PackageVersion Include="xunit" Version="[2.9.0,3)" />
+		<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="[17.13,18)" />
+		<PackageVersion Include="Moq" Version="[4.20.72,5)" />
+		<PackageVersion Include="ReportGenerator" Version="[5.4.4,6)" />
+		<PackageVersion Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[22.0.11,23)" />
+		<PackageVersion Include="xunit" Version="[2.9.3,3)" />
 		<PackageVersion Include="Xunit.Combinatorial" Version="[1.6.24,2)" />
 		<PackageVersion Include="xunit.runner.visualstudio" Version="[2.8.2,3)"/> 
-		<PackageVersion Include="coverlet.collector" Version="[6.0.2,7)" />
+		<PackageVersion Include="coverlet.collector" Version="[6.0.4,7)" />
 		
 	</ItemGroup>
 

+ 0 - 1
ReactiveExample/LoginViewModel.cs

@@ -40,7 +40,6 @@ public partial class LoginViewModel : ReactiveObject
 
     public LoginViewModel ()
     {
-        InitializeCommands ();
         IObservable<bool> canLogin = this.WhenAnyValue
             (
                 x => x.Username,

+ 1 - 1
Terminal.Gui/Application/Application.Initialization.cs

@@ -7,7 +7,7 @@ namespace Terminal.Gui;
 
 public static partial class Application // Initialization (Init/Shutdown)
 {
-    /// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary>
+    /// <summary>Initializes a new instance of a Terminal.Gui Application. <see cref="Shutdown"/> must be called when the application is closing.</summary>
     /// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
     /// <para>
     ///     This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and

+ 6 - 2
Terminal.Gui/Application/Application.Run.cs

@@ -71,7 +71,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     /// <param name="toplevel">The <see cref="Toplevel"/> to prepare execution for.</param>
     /// <remarks>
     ///     This method prepares the provided <see cref="Toplevel"/> for running with the focus, it adds this to the list
-    ///     of <see cref="Toplevel"/>s, lays out the Subviews, focuses the first element, and draws the <see cref="Toplevel"/>
+    ///     of <see cref="Toplevel"/>s, lays out the SubViews, focuses the first element, and draws the <see cref="Toplevel"/>
     ///     in the screen. This is usually followed by executing the <see cref="RunLoop"/> method, and then the
     ///     <see cref="End(RunState)"/> method upon termination which will undo these changes.
     /// </remarks>
@@ -89,7 +89,11 @@ public static partial class Application // Run (Begin, Run, End, Stop)
         //#endif
 
         // Ensure the mouse is ungrabbed.
-        MouseGrabView = null;
+        if (MouseGrabView is { })
+        {
+            UngrabMouse ();
+            MouseGrabView = null;
+        }
 
         var rs = new RunState (toplevel);
 

+ 2 - 2
Terminal.Gui/Application/ApplicationNavigation.cs

@@ -33,7 +33,7 @@ public class ApplicationNavigation
     }
 
     /// <summary>
-    ///     Gets whether <paramref name="view"/> is in the Subview hierarchy of <paramref name="start"/>.
+    ///     Gets whether <paramref name="view"/> is in the SubView hierarchy of <paramref name="start"/>.
     /// </summary>
     /// <param name="start"></param>
     /// <param name="view"></param>
@@ -50,7 +50,7 @@ public class ApplicationNavigation
             return true;
         }
 
-        foreach (View subView in start.Subviews)
+        foreach (View subView in start.SubViews)
         {
             if (view == subView)
             {

+ 1 - 1
Terminal.Gui/ConsoleDrivers/AnsiEscapeSequence.cs

@@ -33,7 +33,7 @@ public class AnsiEscapeSequence
     ///         to the oldest outstanding request.
     ///     </para>
     /// </summary>
-    public required string Terminator { get; init; }
+    public required string? Terminator { get; init; }
 
 
 

+ 1 - 1
Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs

@@ -12,7 +12,7 @@ public class AnsiEscapeSequenceRequest : AnsiEscapeSequence
     ///     Invoked when the console responds with an ANSI response code that matches the
     ///     <see cref="AnsiEscapeSequence.Terminator"/>
     /// </summary>
-    public required Action<string> ResponseReceived { get; init; }
+    public required Action<string?> ResponseReceived { get; init; }
 
     /// <summary>
     ///     Invoked if the console fails to responds to the ANSI response code

+ 4 - 4
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs

@@ -17,11 +17,11 @@ public class AnsiMouseParser
     /// </summary>
     /// <param name="cur"></param>
     /// <returns></returns>
-    public bool IsMouse (string cur)
+    public bool IsMouse (string? cur)
     {
         // Typically in this format
         // ESC [ < {button_code};{x_pos};{y_pos}{final_byte}
-        return cur.EndsWith ('M') || cur.EndsWith ('m');
+        return cur!.EndsWith ('M') || cur.EndsWith ('m');
     }
 
     /// <summary>
@@ -30,10 +30,10 @@ public class AnsiMouseParser
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    public MouseEventArgs? ProcessMouseInput (string input)
+    public MouseEventArgs? ProcessMouseInput (string? input)
     {
         // Match mouse wheel events first
-        Match match = _mouseEventPattern.Match (input);
+        Match match = _mouseEventPattern.Match (input!);
 
         if (match.Success)
         {

+ 5 - 5
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs

@@ -108,7 +108,7 @@ public class AnsiRequestScheduler
 
     private void EvictStaleRequests ()
     {
-        foreach (string stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key))
+        foreach (string? stale in _lastSend.Where (v => IsStale (v.Value)).Select (k => k.Key))
         {
             EvictStaleRequests (stale);
         }
@@ -123,9 +123,9 @@ public class AnsiRequestScheduler
     /// </summary>
     /// <param name="withTerminator"></param>
     /// <returns></returns>
-    private bool EvictStaleRequests (string withTerminator)
+    private bool EvictStaleRequests (string? withTerminator)
     {
-        if (_lastSend.TryGetValue (withTerminator, out DateTime dt))
+        if (_lastSend.TryGetValue (withTerminator!, out DateTime dt))
         {
             if (IsStale (dt))
             {
@@ -178,7 +178,7 @@ public class AnsiRequestScheduler
 
     private void Send (AnsiEscapeSequenceRequest r)
     {
-        _lastSend.AddOrUpdate (r.Terminator, _ => Now (), (_, _) => Now ());
+        _lastSend.AddOrUpdate (r.Terminator!, _ => Now (), (_, _) => Now ());
         _parser.ExpectResponse (r.Terminator, r.ResponseReceived, r.Abandoned, false);
         r.Send ();
     }
@@ -206,7 +206,7 @@ public class AnsiRequestScheduler
 
     private bool ShouldThrottle (AnsiEscapeSequenceRequest r)
     {
-        if (_lastSend.TryGetValue (r.Terminator, out DateTime value))
+        if (_lastSend.TryGetValue (r.Terminator!, out DateTime value))
         {
             return Now () - value < _throttle;
         }

+ 2 - 2
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs

@@ -1,7 +1,7 @@
 #nullable enable
 namespace Terminal.Gui;
 
-internal record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned)
+internal record AnsiResponseExpectation (string? Terminator, Action<IHeld> Response, Action? Abandoned)
 {
-    public bool Matches (string cur) { return cur.EndsWith (Terminator); }
+    public bool Matches (string? cur) { return cur!.EndsWith (Terminator!); }
 }

+ 49 - 47
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs

@@ -6,12 +6,32 @@ namespace Terminal.Gui;
 
 internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 {
-    private const char Escape = '\x1B';
+    private const char ESCAPE = '\x1B';
     private readonly AnsiMouseParser _mouseParser = new ();
+#pragma warning disable IDE1006 // Naming Styles
     protected readonly AnsiKeyboardParser _keyboardParser = new ();
     protected object _lockExpectedResponses = new ();
 
     protected object _lockState = new ();
+    protected readonly IHeld _heldContent;
+
+    /// <summary>
+    ///     Responses we are expecting to come in.
+    /// </summary>
+    protected readonly List<AnsiResponseExpectation> _expectedResponses = [];
+
+    /// <summary>
+    ///     Collection of responses that we <see cref="StopExpecting"/>.
+    /// </summary>
+    protected readonly List<AnsiResponseExpectation> _lateResponses = [];
+
+    /// <summary>
+    ///     Responses that you want to look out for that will come in continuously e.g. mouse events.
+    ///     Key is the terminator.
+    /// </summary>
+    protected readonly List<AnsiResponseExpectation> _persistentExpectations = [];
+
+#pragma warning restore IDE1006 // Naming Styles
 
     /// <summary>
     ///     Event raised when mouse events are detected - requires setting <see cref="HandleMouse"/> to true
@@ -35,22 +55,6 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     /// </summary>
     public bool HandleKeyboard { get; set; } = false;
 
-    /// <summary>
-    ///     Responses we are expecting to come in.
-    /// </summary>
-    protected readonly List<AnsiResponseExpectation> _expectedResponses = [];
-
-    /// <summary>
-    ///     Collection of responses that we <see cref="StopExpecting"/>.
-    /// </summary>
-    protected readonly List<AnsiResponseExpectation> _lateResponses = [];
-
-    /// <summary>
-    ///     Responses that you want to look out for that will come in continuously e.g. mouse events.
-    ///     Key is the terminator.
-    /// </summary>
-    protected readonly List<AnsiResponseExpectation> _persistentExpectations = [];
-
     private AnsiResponseParserState _state = AnsiResponseParserState.Normal;
 
     /// <inheritdoc/>
@@ -64,8 +68,6 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         }
     }
 
-    protected readonly IHeld _heldContent;
-
     /// <summary>
     ///     When <see cref="State"/> was last changed.
     /// </summary>
@@ -74,17 +76,17 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     // These all are valid terminators on ansi responses,
     // see CSI in https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s
     // No - N or O
-    protected readonly HashSet<char> _knownTerminators = new (
-                                                              [
-                                                                  '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
-
-                                                                  // No - N or O
-                                                                  'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Z',
-                                                                  '^', '`', '~',
-                                                                  'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
-                                                                  'l', 'm', 'n',
-                                                                  'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
-                                                              ]);
+    protected readonly HashSet<char> _knownTerminators =
+    [
+        '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+
+        // No - N or O
+        'P', 'Q', 'R', 'S', 'T', 'W', 'X', 'Z',
+        '^', '`', '~',
+        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
+        'l', 'm', 'n',
+        'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
+    ];
 
     protected AnsiResponseParserBase (IHeld heldContent) { _heldContent = heldContent; }
 
@@ -137,7 +139,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
             char currentChar = getCharAtIndex (index);
             object currentObj = getObjectAtIndex (index);
 
-            bool isEscape = currentChar == Escape;
+            bool isEscape = currentChar == ESCAPE;
 
             switch (State)
             {
@@ -233,7 +235,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     {
         lock (_lockState)
         {
-            string cur = _heldContent.HeldToString ();
+            string? cur = _heldContent.HeldToString ();
 
             if (HandleKeyboard)
             {
@@ -250,7 +252,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
             // We have something totally unexpected, not a CSI and
             // still Esc+<something>. So give last minute swallow chance
-            if (cur.Length >= 2 && cur [0] == Escape)
+            if (cur!.Length >= 2 && cur [0] == ESCAPE)
             {
                 // Maybe swallow anyway if user has custom delegate
                 bool swallow = ShouldSwallowUnexpectedResponse ();
@@ -270,7 +272,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     {
         lock (_lockState)
         {
-            string cur = _heldContent.HeldToString ();
+            string? cur = _heldContent.HeldToString ();
 
             if (HandleMouse && IsMouse (cur))
             {
@@ -328,7 +330,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
 
             // Finally if it is a valid ansi response but not one we are expect (e.g. its mouse activity)
             // then we can release it back to input processing stream
-            if (_knownTerminators.Contains (cur.Last ()) && cur.StartsWith (EscSeqUtils.CSI))
+            if (_knownTerminators.Contains (cur!.Last ()) && cur!.StartsWith (EscSeqUtils.CSI))
             {
                 // We have found a terminator so bail
                 State = AnsiResponseParserState.Normal;
@@ -354,7 +356,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         return false; // Continue accumulating
     }
 
-    private void RaiseMouseEvent (string cur)
+    private void RaiseMouseEvent (string? cur)
     {
         MouseEventArgs? ev = _mouseParser.ProcessMouseInput (cur);
 
@@ -364,9 +366,9 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
         }
     }
 
-    private bool IsMouse (string cur) { return _mouseParser.IsMouse (cur); }
+    private bool IsMouse (string? cur) { return _mouseParser.IsMouse (cur); }
 
-    protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string cur)
+    protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string? cur)
     {
         Key? k = pattern.GetKey (cur);
 
@@ -394,7 +396,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     /// <returns></returns>
     protected abstract bool ShouldSwallowUnexpectedResponse ();
 
-    private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
+    private bool MatchResponse (string? cur, List<AnsiResponseExpectation> collection, bool invokeCallback, bool removeExpectation)
     {
         // Check for expected responses
         AnsiResponseExpectation? matchingResponse = collection.FirstOrDefault (r => r.Matches (cur));
@@ -422,7 +424,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     }
 
     /// <inheritdoc/>
-    public void ExpectResponse (string terminator, Action<string> response, Action? abandoned, bool persistent)
+    public void ExpectResponse (string? terminator, Action<string?> response, Action? abandoned, bool persistent)
     {
         lock (_lockExpectedResponses)
         {
@@ -438,17 +440,17 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
     }
 
     /// <inheritdoc/>
-    public bool IsExpecting (string terminator)
+    public bool IsExpecting (string? terminator)
     {
         lock (_lockExpectedResponses)
         {
             // If any of the new terminator matches any existing terminators characters it's a collision so true.
-            return _expectedResponses.Any (r => r.Terminator.Intersect (terminator).Any ());
+            return _expectedResponses.Any (r => r.Terminator!.Intersect (terminator!).Any ());
         }
     }
 
     /// <inheritdoc/>
-    public void StopExpecting (string terminator, bool persistent)
+    public void StopExpecting (string? terminator, bool persistent)
     {
         lock (_lockExpectedResponses)
         {
@@ -530,7 +532,7 @@ internal class AnsiResponseParser<T> : AnsiResponseParserBase
     /// <param name="response"></param>
     /// <param name="abandoned"></param>
     /// <param name="persistent"></param>
-    public void ExpectResponseT (string terminator, Action<IEnumerable<Tuple<char, T>>> response, Action? abandoned, bool persistent)
+    public void ExpectResponseT (string? terminator, Action<IEnumerable<Tuple<char, T>>> response, Action? abandoned, bool persistent)
     {
         lock (_lockExpectedResponses)
         {
@@ -562,7 +564,7 @@ internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()
     ///         keystrokes 'swallowed' (i.e. not returned to input stream).
     ///     </para>
     /// </summary>
-    public Func<string, bool> UnknownResponseHandler { get; set; } = _ => false;
+    public Func<string?, bool> UnknownResponseHandler { get; set; } = _ => false;
 
     public string ProcessInput (string input)
     {
@@ -583,13 +585,13 @@ internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()
         output.Append (c);
     }
 
-    public string Release ()
+    public string? Release ()
     {
         lock (_lockState)
         {
             TryLastMinuteSequences ();
 
-            string output = _heldContent.HeldToString ();
+            string? output = _heldContent.HeldToString ();
             ResetState ();
 
             return output;

+ 1 - 1
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs

@@ -11,7 +11,7 @@ internal class GenericHeld<T> : IHeld
 
     public void ClearHeld () { held.Clear (); }
 
-    public string HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); }
+    public string? HeldToString () { return new (held.Select (h => h.Item1).ToArray ()); }
 
     public IEnumerable<object> HeldToObjects () { return held; }
 

+ 3 - 3
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs

@@ -30,7 +30,7 @@ public interface IAnsiResponseParser
     ///     that already has one.
     ///     exists.
     /// </exception>
-    void ExpectResponse (string terminator, Action<string> response, Action? abandoned, bool persistent);
+    void ExpectResponse (string? terminator, Action<string?> response, Action? abandoned, bool persistent);
 
     /// <summary>
     ///     Returns true if there is an existing expectation (i.e. we are waiting a response
@@ -38,7 +38,7 @@ public interface IAnsiResponseParser
     /// </summary>
     /// <param name="terminator"></param>
     /// <returns></returns>
-    bool IsExpecting (string terminator);
+    bool IsExpecting (string? terminator);
 
     /// <summary>
     ///     Removes callback and expectation that we will get a response for the
@@ -50,5 +50,5 @@ public interface IAnsiResponseParser
     ///     <see langword="true"/> if you want to remove a persistent
     ///     request listener.
     /// </param>
-    void StopExpecting (string requestTerminator, bool persistent);
+    void StopExpecting (string? requestTerminator, bool persistent);
 }

+ 1 - 1
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs

@@ -16,7 +16,7 @@ internal interface IHeld
     ///     Returns string representation of the held objects
     /// </summary>
     /// <returns></returns>
-    string HeldToString ();
+    string? HeldToString ();
 
     /// <summary>
     ///     Returns the collection objects directly e.g. <see langword="char"/>

+ 1 - 1
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs

@@ -20,7 +20,7 @@ public class AnsiKeyboardParser
     /// <param name="input"></param>
     /// <param name="isLastMinute"></param>
     /// <returns></returns>
-    public AnsiKeyboardParserPattern? IsKeyboard (string input, bool isLastMinute = false)
+    public AnsiKeyboardParserPattern? IsKeyboard (string? input, bool isLastMinute = false)
     {
         return _patterns.FirstOrDefault (pattern => pattern.IsLastMinute == isLastMinute && pattern.IsMatch (input));
     }

+ 3 - 3
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs

@@ -23,7 +23,7 @@ public abstract class AnsiKeyboardParserPattern
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    public abstract bool IsMatch (string input);
+    public abstract bool IsMatch (string? input);
 
     private readonly string _name;
 
@@ -37,7 +37,7 @@ public abstract class AnsiKeyboardParserPattern
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    public Key? GetKey (string input)
+    public Key? GetKey (string? input)
     {
         Key? key = GetKeyImpl (input);
         Logging.Trace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key} using {_name}");
@@ -51,5 +51,5 @@ public abstract class AnsiKeyboardParserPattern
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    protected abstract Key? GetKeyImpl (string input);
+    protected abstract Key? GetKeyImpl (string? input);
 }

+ 3 - 3
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs

@@ -41,7 +41,7 @@ public class CsiKeyPattern : AnsiKeyboardParserPattern
     private readonly Regex _pattern;
 
     /// <inheritdoc/>
-    public override bool IsMatch (string input) { return _pattern.IsMatch (input); }
+    public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
 
     /// <summary>
     ///     Creates a new instance of the <see cref="CsiKeyPattern"/> class.
@@ -57,9 +57,9 @@ public class CsiKeyPattern : AnsiKeyboardParserPattern
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    protected override Key? GetKeyImpl (string input)
+    protected override Key? GetKeyImpl (string? input)
     {
-        Match match = _pattern.Match (input);
+        Match match = _pattern.Match (input!);
 
         if (!match.Success)
         {

+ 5 - 3
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs

@@ -7,13 +7,15 @@ internal class EscAsAltPattern : AnsiKeyboardParserPattern
 {
     public EscAsAltPattern () { IsLastMinute = true; }
 
+#pragma warning disable IDE1006 // Naming Styles
     private static readonly Regex _pattern = new (@"^\u001b([a-zA-Z0-9_])$");
+#pragma warning restore IDE1006 // Naming Styles
 
-    public override bool IsMatch (string input) { return _pattern.IsMatch (input); }
+    public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
 
-    protected override Key? GetKeyImpl (string input)
+    protected override Key? GetKeyImpl (string? input)
     {
-        Match match = _pattern.Match (input);
+        Match match = _pattern.Match (input!);
 
         if (!match.Success)
         {

+ 5 - 3
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs

@@ -9,19 +9,21 @@ namespace Terminal.Gui;
 /// </summary>
 public class Ss3Pattern : AnsiKeyboardParserPattern
 {
+#pragma warning disable IDE1006 // Naming Styles
     private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCAB])$");
+#pragma warning restore IDE1006 // Naming Styles
 
     /// <inheritdoc/>
-    public override bool IsMatch (string input) { return _pattern.IsMatch (input); }
+    public override bool IsMatch (string? input) { return _pattern.IsMatch (input!); }
 
     /// <summary>
     ///     Returns the ss3 key that corresponds to the provided input escape sequence
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    protected override Key? GetKeyImpl (string input)
+    protected override Key? GetKeyImpl (string? input)
     {
-        Match match = _pattern.Match (input);
+        Match match = _pattern.Match (input!);
 
         if (!match.Success)
         {

+ 1 - 1
Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs

@@ -10,7 +10,7 @@ internal class StringHeld : IHeld
 
     public void ClearHeld () { _held.Clear (); }
 
-    public string HeldToString () { return _held.ToString (); }
+    public string? HeldToString () { return _held.ToString (); }
 
     public IEnumerable<object> HeldToObjects () { return _held.ToString ().Select (c => (object)c); }
 

+ 3 - 3
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -217,7 +217,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                         if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
                         {
                             // Just add this mark to the list
-                            Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                            Contents [Row, Col - 1].AddCombiningMark (rune);
 
                             // Ignore. Don't move to next column (let the driver figure out what to do).
                         }
@@ -240,7 +240,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                             else
                             {
                                 // It didn't normalize. Add it to the Cell to left's CM list
-                                Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                                Contents [Row, Col - 1].AddCombiningMark (rune);
 
                                 // Ignore. Don't move to next column (let the driver figure out what to do).
                             }
@@ -298,7 +298,7 @@ public abstract class ConsoleDriver : IConsoleDriver
                         else if (!Clip.Contains (Col, Row))
                         {
                             // Our 1st column is outside the clip, so we can't display a wide character.
-                            Contents [Row, Col+1].Rune = Rune.ReplacementChar;
+                            Contents [Row, Col + 1].Rune = Rune.ReplacementChar;
                         }
                         else
                         {

+ 29 - 3
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -1,5 +1,5 @@
 #nullable enable
-using Terminal.Gui.ConsoleDrivers;
+using System.Globalization;
 using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
 
 namespace Terminal.Gui;
@@ -154,13 +154,13 @@ public static class EscSeqUtils
     /// <summary>
     ///     Control sequence for disabling mouse events.
     /// </summary>
-    public static string CSI_DisableMouseEvents { get; set; } =
+    public static readonly string CSI_DisableMouseEvents =
         CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse;
 
     /// <summary>
     ///     Control sequence for enabling mouse events.
     /// </summary>
-    public static string CSI_EnableMouseEvents { get; set; } =
+    public static readonly string CSI_EnableMouseEvents =
         CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse;
 
     /// <summary>
@@ -1688,6 +1688,32 @@ public static class EscSeqUtils
         builder.Append ($"{CSI}{row};{col}H");
     }
 
+    /// <summary>
+    ///     ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
+    ///     of the y line
+    /// </summary>
+    /// <param name="writer">TextWriter where to write the cursor position sequence.</param>
+    /// <param name="row">Origin is (1,1).</param>
+    /// <param name="col">Origin is (1,1).</param>
+    public static void CSI_WriteCursorPosition (TextWriter writer, int row, int col)
+    {
+        const int maxInputBufferSize =
+            // CSI (2) + ';' + 'H'
+            4 +
+            // row + col (2x int sign + int max value)
+            2 + 20;
+        Span<char> buffer = stackalloc char[maxInputBufferSize];
+        if (!buffer.TryWrite (CultureInfo.InvariantCulture, $"{CSI}{row};{col}H", out int charsWritten))
+        {
+            string tooLongCursorPositionSequence = $"{CSI}{row};{col}H";
+            throw new InvalidOperationException (
+                $"{nameof(CSI_WriteCursorPosition)} buffer (len: {buffer.Length}) is too short for cursor position sequence '{tooLongCursorPositionSequence}' (len: {tooLongCursorPositionSequence.Length}).");
+        }
+
+        ReadOnlySpan<char> cursorPositionSequence = buffer[..charsWritten];
+        writer.Write (cursorPositionSequence);
+    }
+
     //ESC [ <y> ; <x> f - HVP     Horizontal Vertical Position* Cursor moves to<x>; <y> coordinate within the viewport, where <x> is the column of the<y> line
     //ESC [ s - ANSISYSSC       Save Cursor – Ansi.sys emulation	**With no parameters, performs a save cursor operation like DECSC
     //ESC [ u - ANSISYSRC       Restore Cursor – Ansi.sys emulation	**With no parameters, performs a restore cursor operation like DECRC

+ 20 - 0
Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs

@@ -194,6 +194,23 @@ public class ApplicationV2 : ApplicationImpl
     {
         Logging.Logger.LogInformation ($"RequestStop '{top}'");
 
+        top ??= Application.Top;
+
+        if (top == null)
+        {
+            return;
+        }
+
+        var ev = new ToplevelClosingEventArgs (top);
+        top.OnClosing (ev);
+
+        if (ev.Cancel)
+        {
+            return;
+        }
+
+        top.Running = false;
+
         // TODO: This definition of stop seems sketchy
         Application.TopLevels.TryPop (out _);
 
@@ -205,6 +222,9 @@ public class ApplicationV2 : ApplicationImpl
         {
             Application.Top = null;
         }
+
+        // Notify that it is closed
+        top.OnClosed (top);
     }
 
     /// <inheritdoc/>

+ 1 - 1
Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs

@@ -11,7 +11,7 @@ public interface IConsoleOutput : IDisposable
     ///     <see cref="IOutputBuffer"/> overload.
     /// </summary>
     /// <param name="text"></param>
-    void Write (string text);
+    void Write (ReadOnlySpan<char> text);
 
     /// <summary>
     ///     Write the contents of the <paramref name="buffer"/> to the console

+ 4 - 4
Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs

@@ -122,7 +122,7 @@ public class MainLoop<T> : IMainLoop<T>
 
         if (Application.Top != null)
         {
-            bool needsDrawOrLayout = AnySubviewsNeedDrawn (Application.Top);
+            bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Top);
 
             bool sizeChanged = WindowSizeMonitor.Poll ();
 
@@ -174,7 +174,7 @@ public class MainLoop<T> : IMainLoop<T>
         }
     }
 
-    private bool AnySubviewsNeedDrawn (View v)
+    private bool AnySubViewsNeedDrawn (View v)
     {
         if (v.NeedsDraw || v.NeedsLayout)
         {
@@ -183,9 +183,9 @@ public class MainLoop<T> : IMainLoop<T>
             return true;
         }
 
-        foreach (View subview in v.Subviews)
+        foreach (View subview in v.SubViews)
         {
-            if (AnySubviewsNeedDrawn (subview))
+            if (AnySubViewsNeedDrawn (subview))
             {
                 return true;
             }

+ 1 - 2
Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs

@@ -49,11 +49,10 @@ public class NetInputProcessor : InputProcessor<ConsoleKeyInfo>
     private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input)
     {
         string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'";
-        var expectedLiteral = "new Rune('todo')";
 
         return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, "
                + $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, "
                + $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, "
-               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral}";
+               + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}),";
     }
 }

+ 29 - 21
Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs

@@ -34,7 +34,10 @@ public class NetOutput : IConsoleOutput
     }
 
     /// <inheritdoc/>
-    public void Write (string text) { Console.Write (text); }
+    public void Write (ReadOnlySpan<char> text)
+    {
+        Console.Out.Write (text);
+    }
 
     /// <inheritdoc/>
     public void Write (IOutputBuffer buffer)
@@ -57,6 +60,9 @@ public class NetOutput : IConsoleOutput
         CursorVisibility? savedVisibility = _cachedCursorVisibility;
         SetCursorVisibility (CursorVisibility.Invisible);
 
+        const int maxCharsPerRune = 2;
+        Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
+
         for (int row = top; row < rows; row++)
         {
             if (Console.WindowHeight < 1)
@@ -115,26 +121,28 @@ public class NetOutput : IConsoleOutput
                     {
                         redrawAttr = attr;
 
-                        output.Append (
-                                       EscSeqUtils.CSI_SetForegroundColorRGB (
-                                                                              attr.Foreground.R,
-                                                                              attr.Foreground.G,
-                                                                              attr.Foreground.B
-                                                                             )
-                                      );
-
-                        output.Append (
-                                       EscSeqUtils.CSI_SetBackgroundColorRGB (
-                                                                              attr.Background.R,
-                                                                              attr.Background.G,
-                                                                              attr.Background.B
-                                                                             )
-                                      );
+                        EscSeqUtils.CSI_AppendForegroundColorRGB (
+                            output,
+                            attr.Foreground.R,
+                            attr.Foreground.G,
+                            attr.Foreground.B
+                        );
+
+                        EscSeqUtils.CSI_AppendBackgroundColorRGB (
+                            output,
+                            attr.Background.R,
+                            attr.Background.G,
+                            attr.Background.B
+                        );
                     }
 
                     outputWidth++;
+
+                    // Avoid Rune.ToString() by appending the rune chars.
                     Rune rune = buffer.Contents [row, col].Rune;
-                    output.Append (rune);
+                    int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer);
+                    ReadOnlySpan<char> runeChars = runeBuffer[..runeCharsWritten];
+                    output.Append (runeChars);
 
                     if (buffer.Contents [row, col].CombiningMarks.Count > 0)
                     {
@@ -162,7 +170,7 @@ public class NetOutput : IConsoleOutput
             if (output.Length > 0)
             {
                 SetCursorPositionImpl (lastCol, row);
-                Console.Write (output);
+                Console.Out.Write (output);
             }
         }
 
@@ -171,7 +179,7 @@ public class NetOutput : IConsoleOutput
             if (!string.IsNullOrWhiteSpace (s.SixelData))
             {
                 SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y);
-                Console.Write (s.SixelData);
+                Console.Out.Write (s.SixelData);
             }
         }
 
@@ -185,7 +193,7 @@ public class NetOutput : IConsoleOutput
     private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
     {
         SetCursorPositionImpl (lastCol, row);
-        Console.Write (output);
+        Console.Out.Write (output);
         output.Clear ();
         lastCol += outputWidth;
         outputWidth = 0;
@@ -222,7 +230,7 @@ public class NetOutput : IConsoleOutput
 
         // + 1 is needed because non-Windows is based on 1 instead of 0 and
         // Console.CursorTop/CursorLeft isn't reliable.
-        Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
+        EscSeqUtils.CSI_WriteCursorPosition (Console.Out, row + 1, col + 1);
 
         return true;
     }

+ 2 - 2
Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs

@@ -164,7 +164,7 @@ public class OutputBuffer : IOutputBuffer
                         if (Contents [Row, Col - 1].CombiningMarks.Count > 0)
                         {
                             // Just add this mark to the list
-                            Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                            Contents [Row, Col - 1].AddCombiningMark (rune);
 
                             // Ignore. Don't move to next column (let the driver figure out what to do).
                         }
@@ -187,7 +187,7 @@ public class OutputBuffer : IOutputBuffer
                             else
                             {
                                 // It didn't normalize. Add it to the Cell to left's CM list
-                                Contents [Row, Col - 1].CombiningMarks.Add (rune);
+                                Contents [Row, Col - 1].AddCombiningMark (rune);
 
                                 // Ignore. Don't move to next column (let the driver figure out what to do).
                             }

+ 28 - 20
Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs

@@ -1,4 +1,5 @@
 #nullable enable
+using System.Buffers;
 using System.ComponentModel;
 using System.Runtime.InteropServices;
 using Microsoft.Extensions.Logging;
@@ -6,12 +7,13 @@ using static Terminal.Gui.WindowsConsole;
 
 namespace Terminal.Gui;
 
-internal class WindowsOutput : IConsoleOutput
+internal partial class WindowsOutput : IConsoleOutput
 {
-    [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
-    private static extern bool WriteConsole (
+    [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static partial bool WriteConsole (
         nint hConsoleOutput,
-        string lpbufer,
+        ReadOnlySpan<char> lpbufer,
         uint numberOfCharsToWriten,
         out uint lpNumberOfCharsWritten,
         nint lpReserved
@@ -84,7 +86,7 @@ internal class WindowsOutput : IConsoleOutput
         }
     }
 
-    public void Write (string str)
+    public void Write (ReadOnlySpan<char> str)
     {
         if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
         {
@@ -183,7 +185,6 @@ internal class WindowsOutput : IConsoleOutput
 
     public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
     {
-        var stringBuilder = new StringBuilder ();
 
         //Debug.WriteLine ("WriteToConsole");
 
@@ -213,10 +214,10 @@ internal class WindowsOutput : IConsoleOutput
         }
         else
         {
-            stringBuilder.Clear ();
+            StringBuilder stringBuilder = new();
 
             stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
-            stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
+            EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 0, 0);
 
             Attribute? prev = null;
 
@@ -227,8 +228,8 @@ internal class WindowsOutput : IConsoleOutput
                 if (attr != prev)
                 {
                     prev = attr;
-                    stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
-                    stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
+                    EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
+                    EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
                 }
 
                 if (info.Char != '\x1b')
@@ -247,14 +248,20 @@ internal class WindowsOutput : IConsoleOutput
             stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
             stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
 
-            var s = stringBuilder.ToString ();
+            // TODO: Potentially could stackalloc whenever reasonably small (<= 8 kB?) write buffer is needed.
+            char [] rentedWriteArray = ArrayPool<char>.Shared.Rent (minimumLength: stringBuilder.Length);
+            try
+            {
+                Span<char> writeBuffer = rentedWriteArray.AsSpan(0, stringBuilder.Length);
+                stringBuilder.CopyTo (0, writeBuffer, stringBuilder.Length);
 
-            // TODO: requires extensive testing if we go down this route
-            // If console output has changed
-            //if (s != _lastWrite)
-            //{
-            // supply console with the new content
-            result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
+                // Supply console with the new content.
+                result = WriteConsole (_screenBuffer, writeBuffer, (uint)writeBuffer.Length, out uint _, nint.Zero);
+            }
+            finally
+            {
+                ArrayPool<char>.Shared.Return (rentedWriteArray);
+            }
 
             foreach (SixelToRender sixel in Application.Sixel)
             {
@@ -297,9 +304,10 @@ internal class WindowsOutput : IConsoleOutput
     /// <inheritdoc/>
     public void SetCursorVisibility (CursorVisibility visibility)
     {
-        var sb = new StringBuilder ();
-        sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
-        Write (sb.ToString ());
+        string cursorVisibilitySequence = visibility != CursorVisibility.Invisible
+            ? EscSeqUtils.CSI_ShowCursor
+            : EscSeqUtils.CSI_HideCursor;
+        Write (cursorVisibilitySequence);
     }
 
     private Point _lastCursorPosition;

+ 5 - 4
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs

@@ -6,7 +6,7 @@ using Terminal.Gui.ConsoleDrivers;
 
 namespace Terminal.Gui;
 
-internal class WindowsConsole
+internal partial class WindowsConsole
 {
     private CancellationTokenSource? _inputReadyCancellationTokenSource;
     private readonly BlockingCollection<InputRecord> _inputQueue = new (new ConcurrentQueue<InputRecord> ());
@@ -926,10 +926,11 @@ internal class WindowsConsole
         ref SmallRect lpWriteRegion
     );
 
-    [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
-    private static extern bool WriteConsole (
+    [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
+    [return: MarshalAs (UnmanagedType.Bool)]
+    private static partial bool WriteConsole (
         nint hConsoleOutput,
-        string lpbufer,
+        ReadOnlySpan<char> lpbufer,
         uint NumberOfCharsToWriten,
         out uint lpNumberOfCharsWritten,
         nint lpReserved

+ 35 - 6
Terminal.Gui/Drawing/Cell.cs

@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+#nullable enable
+
+namespace Terminal.Gui;
 
 /// <summary>
 ///     Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and
@@ -23,12 +25,12 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru
         get => _rune;
         set
         {
-            CombiningMarks.Clear ();
+            _combiningMarks?.Clear ();
             _rune = value;
         }
     }
 
-    private List<Rune> _combiningMarks;
+    private List<Rune>? _combiningMarks;
 
     /// <summary>
     ///     The combining marks for <see cref="Rune"/> that when combined makes this Cell a combining sequence. If
@@ -38,10 +40,37 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Ru
     ///     Only valid in the rare case where <see cref="Rune"/> is a combining sequence that could not be normalized to a
     ///     single Rune.
     /// </remarks>
-    internal List<Rune> CombiningMarks
+    internal IReadOnlyList<Rune> CombiningMarks
+    {
+        // PERFORMANCE: Downside of the interface return type is that List<T> struct enumerator cannot be utilized, i.e. enumerator is allocated.
+        // If enumeration is used heavily in the future then might be better to expose the List<T> Enumerator directly via separate mechanism.
+        get
+        {
+            // Avoid unnecessary list allocation.
+            if (_combiningMarks == null)
+            {
+                return Array.Empty<Rune> ();
+            }
+            return _combiningMarks;
+        }
+    }
+
+    /// <summary>
+    ///     Adds combining mark to the cell.
+    /// </summary>
+    /// <param name="combiningMark">The combining mark to add to the cell.</param>
+    internal void AddCombiningMark (Rune combiningMark)
+    {
+        _combiningMarks ??= [];
+        _combiningMarks.Add (combiningMark);
+    }
+
+    /// <summary>
+    ///     Clears combining marks of the cell.
+    /// </summary>
+    internal void ClearCombiningMarks ()
     {
-        get => _combiningMarks ?? [];
-        private set => _combiningMarks = value ?? [];
+        _combiningMarks?.Clear ();
     }
 
     /// <inheritdoc/>

+ 155 - 69
Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs

@@ -11,9 +11,11 @@ namespace Terminal.Gui;
 /// </summary>
 public sealed class Colors : INotifyCollectionChanged, IDictionary<string, ColorScheme?>
 {
+    private static readonly object _lock = new object ();
+
     static Colors ()
     {
-        ColorSchemes = new (5, StringComparer.InvariantCultureIgnoreCase);
+        ColorSchemes = new Dictionary<string, ColorScheme?> (5, StringComparer.InvariantCultureIgnoreCase);
         Reset ();
     }
 
@@ -66,111 +68,195 @@ public sealed class Colors : INotifyCollectionChanged, IDictionary<string, Color
     [UsedImplicitly]
     public static Dictionary<string, ColorScheme?> ColorSchemes { get; private set; }
 
-    /// <inheritdoc/>
-    public IEnumerator<KeyValuePair<string, ColorScheme?>> GetEnumerator () { return ColorSchemes.GetEnumerator (); }
-
-    /// <inheritdoc/>
-    IEnumerator IEnumerable.GetEnumerator () { return GetEnumerator (); }
+    /// <summary>
+    ///     Raised when the collection changes.
+    /// </summary>
+    public event NotifyCollectionChangedEventHandler? CollectionChanged;
 
-    /// <inheritdoc/>
-    public void Add (KeyValuePair<string, ColorScheme?> item)
+    /// <inheritdoc />
+    public ColorScheme? this [string key]
     {
-        ColorSchemes.Add (item.Key, item.Value);
-        CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Add, item));
+        get
+        {
+            lock (_lock)
+            {
+                return ColorSchemes [key];
+            }
+        }
+        set
+        {
+            lock (_lock)
+            {
+                if (ColorSchemes.ContainsKey (key))
+                {
+                    ColorScheme? oldValue = ColorSchemes [key];
+                    ColorSchemes [key] = value;
+                    CollectionChanged?.Invoke (this, new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Replace, value, oldValue));
+                }
+                else
+                {
+                    ColorSchemes.Add (key, value);
+                    CollectionChanged?.Invoke (this, new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, new KeyValuePair<string, ColorScheme?> (key, value)));
+                }
+            }
+        }
     }
 
-    /// <inheritdoc/>
-    public void Clear ()
+    /// <inheritdoc />
+    public int Count
     {
-        ColorSchemes.Clear ();
-        CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Reset));
+        get
+        {
+            lock (_lock)
+            {
+                return ColorSchemes.Count;
+            }
+        }
     }
 
-    /// <inheritdoc/>
-    public bool Contains (KeyValuePair<string, ColorScheme?> item) { return ColorSchemes.Contains (item); }
-
-    /// <inheritdoc/>
-    public void CopyTo (KeyValuePair<string, ColorScheme?> [] array, int arrayIndex) { ((ICollection)ColorSchemes).CopyTo (array, arrayIndex); }
+    /// <inheritdoc />
+    public bool IsReadOnly => false;
 
-    /// <inheritdoc/>
-    public bool Remove (KeyValuePair<string, ColorScheme?> item)
+    /// <inheritdoc />
+    public ICollection<string> Keys
     {
-        if (ColorSchemes.Remove (item.Key))
+        get
         {
-            CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Remove, item));
-
-            return true;
+            lock (_lock)
+            {
+                return new List<string> (ColorSchemes.Keys);
+            }
         }
+    }
 
-        return false;
+    /// <inheritdoc />
+    public ICollection<ColorScheme?> Values
+    {
+        get
+        {
+            lock (_lock)
+            {
+                return new List<ColorScheme?> (ColorSchemes.Values);
+            }
+        }
     }
 
-    /// <inheritdoc/>
-    public int Count => ColorSchemes.Count;
+    /// <inheritdoc />
+    public void Add (KeyValuePair<string, ColorScheme?> item)
+    {
+        lock (_lock)
+        {
+            ColorSchemes.Add (item.Key, item.Value);
+            CollectionChanged?.Invoke (this, new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Add, item));
+        }
+    }
 
-    /// <inheritdoc/>
-    public bool IsReadOnly => false;
+    /// <inheritdoc />
+    public void Add (string key, ColorScheme? value)
+    {
+        Add (new KeyValuePair<string, ColorScheme?> (key, value));
+    }
 
-    /// <inheritdoc/>
-    public void Add (string key, ColorScheme? value) { Add (new (key, value)); }
+    /// <inheritdoc />
+    public void Clear ()
+    {
+        lock (_lock)
+        {
+            ColorSchemes.Clear ();
+            CollectionChanged?.Invoke (this, new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Reset));
+        }
+    }
 
-    /// <inheritdoc/>
-    public bool ContainsKey (string key) { return ColorSchemes.ContainsKey (key); }
+    /// <inheritdoc />
+    public bool Contains (KeyValuePair<string, ColorScheme?> item)
+    {
+        lock (_lock)
+        {
+            return ColorSchemes.Contains (item);
+        }
+    }
 
-    /// <inheritdoc/>
-    public bool Remove (string key)
+    /// <inheritdoc />
+    public bool ContainsKey (string key)
     {
-        if (ColorSchemes.Remove (key))
+        lock (_lock)
         {
-            CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Remove, key));
+            return ColorSchemes.ContainsKey (key);
+        }
+    }
 
-            return true;
+    public void CopyTo (KeyValuePair<string, ColorScheme?> [] array, int arrayIndex)
+    {
+        lock (_lock)
+        {
+            ((ICollection)ColorSchemes).CopyTo (array, arrayIndex);
         }
+    }
 
-        return false;
+    public IEnumerator<KeyValuePair<string, ColorScheme?>> GetEnumerator ()
+    {
+        lock (_lock)
+        {
+            return new List<KeyValuePair<string, ColorScheme?>> (ColorSchemes).GetEnumerator ();
+        }
     }
 
-    /// <inheritdoc/>
-    public bool TryGetValue (string key, out ColorScheme? value) { return ColorSchemes.TryGetValue (key, out value); }
+    IEnumerator IEnumerable.GetEnumerator ()
+    {
+        return GetEnumerator ();
+    }
 
-    /// <inheritdoc/>
-    public ColorScheme? this [string key]
+    public bool Remove (KeyValuePair<string, ColorScheme?> item)
     {
-        get => ColorSchemes [key];
-        set
+        lock (_lock)
         {
-            if (ColorSchemes.TryAdd (key, value))
+            if (ColorSchemes.Remove (item.Key))
             {
-                CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Add, new KeyValuePair<string, ColorScheme?> (key, value)));
+                CollectionChanged?.Invoke (this, new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Remove, item));
+                return true;
             }
-            else
+            return false;
+        }
+    }
+
+    public bool Remove (string key)
+    {
+        lock (_lock)
+        {
+            if (ColorSchemes.Remove (key))
             {
-                ColorScheme? oldValue = ColorSchemes [key];
-                ColorSchemes [key] = value;
-                CollectionChanged?.Invoke (this, new (NotifyCollectionChangedAction.Replace, value, oldValue));
+                CollectionChanged?.Invoke (this, new NotifyCollectionChangedEventArgs (NotifyCollectionChangedAction.Remove, key));
+                return true;
             }
+            return false;
         }
     }
 
-    /// <inheritdoc/>
-    public ICollection<string> Keys => ColorSchemes.Keys;
+    /// <inheritdoc />
+    public bool TryGetValue (string key, out ColorScheme? value)
+    {
+        lock (_lock)
+        {
+            return ColorSchemes.TryGetValue (key, out value);
+        }
+    }
 
-    /// <inheritdoc/>
-    public ICollection<ColorScheme?> Values => ColorSchemes.Values;
 
-    /// <inheritdoc/>
-    public event NotifyCollectionChangedEventHandler? CollectionChanged;
-
-    /// <summary>Resets the <see cref="ColorSchemes"/> dictionary to the default values.</summary>
+    /// <summary>
+    ///     Resets the <see cref="ColorSchemes"/> dictionary to its default values.
+    /// </summary>
+    /// <returns></returns>
     public static Dictionary<string, ColorScheme?> Reset ()
     {
-        ColorSchemes.Clear ();
-        ColorSchemes.Add ("TopLevel", new ());
-        ColorSchemes.Add ("Base", new ());
-        ColorSchemes.Add ("Dialog", new ());
-        ColorSchemes.Add ("Menu", new ());
-        ColorSchemes.Add ("Error", new ());
-
-        return ColorSchemes;
+        lock (_lock)
+        {
+            ColorSchemes.Clear ();
+            ColorSchemes.Add ("TopLevel", new ColorScheme ());
+            ColorSchemes.Add ("Base", new ColorScheme ());
+            ColorSchemes.Add ("Dialog", new ColorScheme ());
+            ColorSchemes.Add ("Menu", new ColorScheme ());
+            ColorSchemes.Add ("Error", new ColorScheme ());
+            return ColorSchemes;
+        }
     }
-}
+}

+ 4 - 0
Terminal.Gui/Input/InputBindings.cs

@@ -43,10 +43,12 @@ public abstract class InputBindings<TEvent, TBinding> where TBinding : IInputBin
             throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs));
         }
 
+#pragma warning disable CS8601 // Possible null reference assignment.
         if (TryGet (eventArgs, out TBinding _))
         {
             throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding}).");
         }
+#pragma warning restore CS8601 // Possible null reference assignment.
 
         // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy 
         // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus 
@@ -208,6 +210,7 @@ public abstract class InputBindings<TEvent, TBinding> where TBinding : IInputBin
     /// <param name="newCommands">The set of commands to replace the old ones with.</param>
     public void ReplaceCommands (TEvent eventArgs, params Command [] newCommands)
     {
+#pragma warning disable CS8601 // Possible null reference assignment.
         if (TryGet (eventArgs, out TBinding _))
         {
             Remove (eventArgs);
@@ -217,6 +220,7 @@ public abstract class InputBindings<TEvent, TBinding> where TBinding : IInputBin
         {
             Add (eventArgs, newCommands);
         }
+#pragma warning restore CS8601 // Possible null reference assignment.
     }
 
     /// <summary>Removes a <typeparamref name="TEvent"/> from the collection.</summary>

+ 3 - 1
Terminal.Gui/Terminal.Gui.csproj

@@ -78,14 +78,16 @@
         <Using Include="Terminal.Gui.EnumExtensions" />
     </ItemGroup>
     <!-- =================================================================== -->
-    <!-- Assembliy names for which internal items are visible -->
+    <!-- Assembly names for which internal items are visible -->
     <!-- =================================================================== -->
     <ItemGroup>
+        <InternalsVisibleTo Include="Benchmarks"/>
         <InternalsVisibleTo Include="UnitTests" />
         <InternalsVisibleTo Include="UnitTests.Parallelizable" />
         <InternalsVisibleTo Include="StressTests" />
         <InternalsVisibleTo Include="IntegrationTests" />
         <InternalsVisibleTo Include="TerminalGuiDesigner" />
+        <InternalsVisibleTo Include="TerminalGuiFluentTesting" />
         <InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
     </ItemGroup>
     <!-- =================================================================== -->

+ 1 - 6
Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs

@@ -75,11 +75,6 @@ public abstract partial class PopupAutocomplete : AutocompleteBase
         }
     }
 
-    private void _top_Added (object sender, SuperViewChangedEventArgs e)
-    {
-        throw new NotImplementedException ();
-    }
-
     /// <inheritdoc/>
     public override void EnsureSelectedIdxIsValid ()
     {
@@ -559,7 +554,7 @@ public abstract partial class PopupAutocomplete : AutocompleteBase
 
     private void RemovePopupFromTop ()
     {
-        if (_popup is { } && _top.Subviews.Contains (_popup))
+        if (_popup is { } && _top.SubViews.Contains (_popup))
         {
             _top?.Remove (_popup);
             _popup.Dispose ();

+ 44 - 4
Terminal.Gui/Text/StringExtensions.cs

@@ -124,14 +124,54 @@ public static class StringExtensions
     /// <returns></returns>
     public static string ToString (IEnumerable<Rune> runes)
     {
-        var str = string.Empty;
+        const int maxCharsPerRune = 2;
+        const int maxStackallocTextBufferSize = 1048; // ~2 kB
 
-        foreach (Rune rune in runes)
+        // If rune count is easily available use stackalloc buffer or alternatively rented array.
+        if (runes.TryGetNonEnumeratedCount (out int count))
         {
-            str += rune.ToString ();
+            if (count == 0)
+            {
+                return string.Empty;
+            }
+
+            char[]? rentedBufferArray = null;
+            try
+            {
+                int maxRequiredTextBufferSize = count * maxCharsPerRune;
+                Span<char> textBuffer = maxRequiredTextBufferSize <= maxStackallocTextBufferSize
+                    ? stackalloc char[maxRequiredTextBufferSize]
+                    : (rentedBufferArray = ArrayPool<char>.Shared.Rent(maxRequiredTextBufferSize));
+
+                Span<char> remainingBuffer = textBuffer;
+                foreach (Rune rune in runes)
+                {
+                    int charsWritten = rune.EncodeToUtf16 (remainingBuffer);
+                    remainingBuffer = remainingBuffer [charsWritten..];
+                }
+
+                ReadOnlySpan<char> text = textBuffer[..^remainingBuffer.Length];
+                return text.ToString ();
+            }
+            finally
+            {
+                if (rentedBufferArray != null)
+                {
+                    ArrayPool<char>.Shared.Return (rentedBufferArray);
+                }
+            }
         }
 
-        return str;
+        // Fallback to StringBuilder append.
+        StringBuilder stringBuilder = new();
+        Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
+        foreach (Rune rune in runes)
+        {
+            int charsWritten = rune.EncodeToUtf16 (runeBuffer);
+            ReadOnlySpan<char> runeChars = runeBuffer [..charsWritten];
+            stringBuilder.Append (runeChars);
+        }
+        return stringBuilder.ToString ();
     }
 
     /// <summary>Converts a byte generic collection into a string in the provided encoding (default is UTF8)</summary>

+ 248 - 174
Terminal.Gui/Text/TextFormatter.cs

@@ -1,4 +1,5 @@
 #nullable enable
+using System.Buffers;
 using System.Diagnostics;
 
 namespace Terminal.Gui;
@@ -9,6 +10,9 @@ namespace Terminal.Gui;
 /// </summary>
 public class TextFormatter
 {
+    // Utilized in CRLF related helper methods for faster newline char index search.
+    private static readonly SearchValues<char> NewlineSearchValues = SearchValues.Create(['\r', '\n']);
+
     private Key _hotKey = new ();
     private int _hotKeyPos = -1;
     private List<string> _lines = new ();
@@ -438,7 +442,7 @@ public class TextFormatter
             }
         }
     }
-    
+
     /// <summary>
     ///     Determines if the viewport width will be used or only the text width will be used,
     ///     If <see langword="true"/> all the viewport area will be filled with whitespaces and the same background color
@@ -938,67 +942,67 @@ public class TextFormatter
             {
                 // Horizontal Alignment
                 case Alignment.End when isVertical:
-                {
-                    int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
-                    x = screen.Right - runesWidth;
+                    {
+                        int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, line, linesFormatted.Count - line, TabWidth);
+                        x = screen.Right - runesWidth;
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.End:
-                {
-                    int runesWidth = StringExtensions.ToString (runes).GetColumns ();
-                    x = screen.Right - runesWidth;
+                    {
+                        int runesWidth = StringExtensions.ToString (runes).GetColumns ();
+                        x = screen.Right - runesWidth;
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Start when isVertical:
-                {
-                    int runesWidth = line > 0
+                    {
+                        int runesWidth = line > 0
                                          ? GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth)
                                          : 0;
-                    x = screen.Left + runesWidth;
+                        x = screen.Left + runesWidth;
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Start:
                     x = screen.Left;
 
                     break;
                 case Alignment.Fill when isVertical:
-                {
-                    int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
-                    int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
-                    int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
-                    int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
-                    var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
-
-                    x = line == 0
-                            ? screen.Left
-                            : line < linesFormatted.Count - 1
-                                ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
-                                : screen.Right - lastLineWidth;
+                    {
+                        int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
+                        int prevLineWidth = line > 0 ? GetColumnsRequiredForVerticalText (linesFormatted, line - 1, 1, TabWidth) : 0;
+                        int firstLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, 1, TabWidth);
+                        int lastLineWidth = GetColumnsRequiredForVerticalText (linesFormatted, linesFormatted.Count - 1, 1, TabWidth);
+                        var interval = (int)Math.Round ((double)(screen.Width + firstLineWidth + lastLineWidth) / linesFormatted.Count);
+
+                        x = line == 0
+                                ? screen.Left
+                                : line < linesFormatted.Count - 1
+                                    ? screen.Width - runesWidth <= lastLineWidth ? screen.Left + prevLineWidth : screen.Left + line * interval
+                                    : screen.Right - lastLineWidth;
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Fill:
                     x = screen.Left;
 
                     break;
                 case Alignment.Center when isVertical:
-                {
-                    int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
-                    int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
-                    x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
+                    {
+                        int runesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, linesFormatted.Count, TabWidth);
+                        int linesWidth = GetColumnsRequiredForVerticalText (linesFormatted, 0, line, TabWidth);
+                        x = screen.Left + linesWidth + (screen.Width - runesWidth) / 2;
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Center:
-                {
-                    int runesWidth = StringExtensions.ToString (runes).GetColumns ();
-                    x = screen.Left + (screen.Width - runesWidth) / 2;
+                    {
+                        int runesWidth = StringExtensions.ToString (runes).GetColumns ();
+                        x = screen.Left + (screen.Width - runesWidth) / 2;
 
-                    break;
-                }
+                        break;
+                    }
                 default:
                     Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
 
@@ -1029,28 +1033,28 @@ public class TextFormatter
 
                     break;
                 case Alignment.Fill:
-                {
-                    var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
+                    {
+                        var interval = (int)Math.Round ((double)(screen.Height + 2) / linesFormatted.Count);
 
-                    y = line == 0 ? screen.Top :
-                        line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
+                        y = line == 0 ? screen.Top :
+                            line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Center when isVertical:
-                {
-                    int s = (screen.Height - runes.Length) / 2;
-                    y = screen.Top + s;
+                    {
+                        int s = (screen.Height - runes.Length) / 2;
+                        y = screen.Top + s;
 
-                    break;
-                }
+                        break;
+                    }
                 case Alignment.Center:
-                {
-                    int s = (screen.Height - linesFormatted.Count) / 2;
-                    y = screen.Top + line + s;
+                    {
+                        int s = (screen.Height - linesFormatted.Count) / 2;
+                        y = screen.Top + line + s;
 
-                    break;
-                }
+                        break;
+                    }
                 default:
                     Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
 
@@ -1140,125 +1144,175 @@ public class TextFormatter
     public static bool IsHorizontalDirection (TextDirection textDirection)
     {
         return textDirection switch
-               {
-                   TextDirection.LeftRight_TopBottom => true,
-                   TextDirection.LeftRight_BottomTop => true,
-                   TextDirection.RightLeft_TopBottom => true,
-                   TextDirection.RightLeft_BottomTop => true,
-                   _ => false
-               };
+        {
+            TextDirection.LeftRight_TopBottom => true,
+            TextDirection.LeftRight_BottomTop => true,
+            TextDirection.RightLeft_TopBottom => true,
+            TextDirection.RightLeft_BottomTop => true,
+            _ => false
+        };
     }
 
     /// <summary>Check if it is a vertical direction</summary>
     public static bool IsVerticalDirection (TextDirection textDirection)
     {
         return textDirection switch
-               {
-                   TextDirection.TopBottom_LeftRight => true,
-                   TextDirection.TopBottom_RightLeft => true,
-                   TextDirection.BottomTop_LeftRight => true,
-                   TextDirection.BottomTop_RightLeft => true,
-                   _ => false
-               };
+        {
+            TextDirection.TopBottom_LeftRight => true,
+            TextDirection.TopBottom_RightLeft => true,
+            TextDirection.BottomTop_LeftRight => true,
+            TextDirection.BottomTop_RightLeft => true,
+            _ => false
+        };
     }
 
     /// <summary>Check if it is Left to Right direction</summary>
     public static bool IsLeftToRight (TextDirection textDirection)
     {
         return textDirection switch
-               {
-                   TextDirection.LeftRight_TopBottom => true,
-                   TextDirection.LeftRight_BottomTop => true,
-                   _ => false
-               };
+        {
+            TextDirection.LeftRight_TopBottom => true,
+            TextDirection.LeftRight_BottomTop => true,
+            _ => false
+        };
     }
 
     /// <summary>Check if it is Top to Bottom direction</summary>
     public static bool IsTopToBottom (TextDirection textDirection)
     {
         return textDirection switch
-               {
-                   TextDirection.TopBottom_LeftRight => true,
-                   TextDirection.TopBottom_RightLeft => true,
-                   _ => false
-               };
+        {
+            TextDirection.TopBottom_LeftRight => true,
+            TextDirection.TopBottom_RightLeft => true,
+            _ => false
+        };
     }
 
     // TODO: Move to StringExtensions?
-    private static string StripCRLF (string str, bool keepNewLine = false)
+    internal static string StripCRLF (string str, bool keepNewLine = false)
     {
-        List<Rune> runes = str.ToRuneList ();
+        ReadOnlySpan<char> remaining = str.AsSpan ();
+        int firstNewlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+        // Early exit to avoid StringBuilder allocation if there are no newline characters.
+        if (firstNewlineCharIndex < 0)
+        {
+            return str;
+        }
 
-        for (var i = 0; i < runes.Count; i++)
+        StringBuilder stringBuilder = new();
+        ReadOnlySpan<char> firstSegment = remaining[..firstNewlineCharIndex];
+        stringBuilder.Append (firstSegment);
+
+        // The first newline is not yet skipped because the "keepNewLine" condition has not been evaluated.
+        // This means there will be 1 extra iteration because the same newline index is checked again in the loop.
+        remaining = remaining [firstNewlineCharIndex..];
+
+        while (remaining.Length > 0)
         {
-            switch ((char)runes [i].Value)
+            int newlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+            if (newlineCharIndex == -1)
             {
-                case '\n':
-                    if (!keepNewLine)
-                    {
-                        runes.RemoveAt (i);
-                    }
+                break;
+            }
 
-                    break;
+            ReadOnlySpan<char> segment = remaining[..newlineCharIndex];
+            stringBuilder.Append (segment);
 
-                case '\r':
-                    if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
+            int stride = segment.Length;
+            // Evaluate how many line break characters to preserve.
+            char newlineChar = remaining [newlineCharIndex];
+            if (newlineChar == '\n')
+            {
+                stride++;
+                if (keepNewLine)
+                {
+                    stringBuilder.Append ('\n');
+                }
+            }
+            else // '\r'
+            {
+                int nextCharIndex = newlineCharIndex + 1;
+                bool crlf = nextCharIndex < remaining.Length && remaining [nextCharIndex] == '\n';
+                if (crlf)
+                {
+                    stride += 2;
+                    if (keepNewLine)
                     {
-                        runes.RemoveAt (i);
-
-                        if (!keepNewLine)
-                        {
-                            runes.RemoveAt (i);
-                        }
-
-                        i++;
+                        stringBuilder.Append ('\n');
                     }
-                    else
+                }
+                else
+                {
+                    stride++;
+                    if (keepNewLine)
                     {
-                        if (!keepNewLine)
-                        {
-                            runes.RemoveAt (i);
-                        }
+                        stringBuilder.Append ('\r');
                     }
-
-                    break;
+                }
             }
+            remaining = remaining [stride..];
         }
-
-        return StringExtensions.ToString (runes);
+        stringBuilder.Append (remaining);
+        return stringBuilder.ToString ();
     }
 
     // TODO: Move to StringExtensions?
-    private static string ReplaceCRLFWithSpace (string str)
+    internal static string ReplaceCRLFWithSpace (string str)
     {
-        List<Rune> runes = str.ToRuneList ();
+        ReadOnlySpan<char> remaining = str.AsSpan ();
+        int firstNewlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+        // Early exit to avoid StringBuilder allocation if there are no newline characters.
+        if (firstNewlineCharIndex < 0)
+        {
+            return str;
+        }
 
-        for (var i = 0; i < runes.Count; i++)
+        StringBuilder stringBuilder = new();
+        ReadOnlySpan<char> firstSegment = remaining[..firstNewlineCharIndex];
+        stringBuilder.Append (firstSegment);
+
+        // The first newline is not yet skipped because the newline type has not been evaluated.
+        // This means there will be 1 extra iteration because the same newline index is checked again in the loop.
+        remaining = remaining [firstNewlineCharIndex..];
+
+        while (remaining.Length > 0)
         {
-            switch (runes [i].Value)
+            int newlineCharIndex = remaining.IndexOfAny (NewlineSearchValues);
+            if (newlineCharIndex == -1)
             {
-                case '\n':
-                    runes [i] = (Rune)' ';
-
-                    break;
+                break;
+            }
 
-                case '\r':
-                    if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
-                    {
-                        runes [i] = (Rune)' ';
-                        runes.RemoveAt (i + 1);
-                        i++;
-                    }
-                    else
-                    {
-                        runes [i] = (Rune)' ';
-                    }
+            ReadOnlySpan<char> segment = remaining[..newlineCharIndex];
+            stringBuilder.Append (segment);
 
-                    break;
+            int stride = segment.Length;
+            // Replace newlines
+            char newlineChar = remaining [newlineCharIndex];
+            if (newlineChar == '\n')
+            {
+                stride++;
+                stringBuilder.Append (' ');
+            }
+            else // '\r'
+            {
+                int nextCharIndex = newlineCharIndex + 1;
+                bool crlf = nextCharIndex < remaining.Length && remaining [nextCharIndex] == '\n';
+                if (crlf)
+                {
+                    stride += 2;
+                    stringBuilder.Append (' ');
+                }
+                else
+                {
+                    stride++;
+                    stringBuilder.Append (' ');
+                }
             }
+            remaining = remaining [stride..];
         }
-
-        return StringExtensions.ToString (runes);
+        stringBuilder.Append (remaining);
+        return stringBuilder.ToString ();
     }
 
     // TODO: Move to StringExtensions?
@@ -1570,21 +1624,21 @@ public class TextFormatter
                     case ' ':
                         return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
                     case '\t':
-                    {
-                        length += tabWidth + 1;
-
-                        if (length == tabWidth && tabWidth > cWidth)
                         {
-                            return to + 1;
-                        }
+                            length += tabWidth + 1;
 
-                        if (length > cWidth && tabWidth > cWidth)
-                        {
-                            return to;
-                        }
+                            if (length == tabWidth && tabWidth > cWidth)
+                            {
+                                return to + 1;
+                            }
 
-                        return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
-                    }
+                            if (length > cWidth && tabWidth > cWidth)
+                            {
+                                return to;
+                            }
+
+                            return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
+                        }
                     default:
                         to++;
 
@@ -1593,11 +1647,11 @@ public class TextFormatter
             }
 
             return cLength switch
-                   {
-                       > 0 when to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t' => from,
-                       > 0 when to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t') => from,
-                       _ => to
-                   };
+            {
+                > 0 when to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t' => from,
+                > 0 when to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t') => from,
+                _ => to
+            };
         }
 
         if (start < text.GetRuneCount ())
@@ -2033,13 +2087,13 @@ public class TextFormatter
     private static string PerformCorrectFormatDirection (TextDirection textDirection, string line)
     {
         return textDirection switch
-               {
-                   TextDirection.RightLeft_BottomTop
-                       or TextDirection.RightLeft_TopBottom
-                       or TextDirection.BottomTop_LeftRight
-                       or TextDirection.BottomTop_RightLeft => StringExtensions.ToString (line.EnumerateRunes ().Reverse ()),
-                   _ => line
-               };
+        {
+            TextDirection.RightLeft_BottomTop
+                or TextDirection.RightLeft_TopBottom
+                or TextDirection.BottomTop_LeftRight
+                or TextDirection.BottomTop_RightLeft => StringExtensions.ToString (line.EnumerateRunes ().Reverse ()),
+            _ => line
+        };
     }
 
     private static List<Rune> PerformCorrectFormatDirection (TextDirection textDirection, List<Rune> runes)
@@ -2050,13 +2104,13 @@ public class TextFormatter
     private static List<string> PerformCorrectFormatDirection (TextDirection textDirection, List<string> lines)
     {
         return textDirection switch
-               {
-                   TextDirection.TopBottom_RightLeft
-                       or TextDirection.LeftRight_BottomTop
-                       or TextDirection.RightLeft_BottomTop
-                       or TextDirection.BottomTop_RightLeft => lines.ToArray ().Reverse ().ToList (),
-                   _ => lines
-               };
+        {
+            TextDirection.TopBottom_RightLeft
+                or TextDirection.LeftRight_BottomTop
+                or TextDirection.RightLeft_BottomTop
+                or TextDirection.BottomTop_RightLeft => lines.ToArray ().Reverse ().ToList (),
+            _ => lines
+        };
     }
 
     /// <summary>
@@ -2390,24 +2444,44 @@ public class TextFormatter
             return text;
         }
 
-        // Scan 
-        var start = string.Empty;
-        var i = 0;
-
-        foreach (Rune c in text.EnumerateRunes ())
+        const int maxStackallocCharBufferSize = 512; // ~1 kB
+        char[]? rentedBufferArray = null;
+        try
         {
-            if (c == hotKeySpecifier && i == hotPos)
+            Span<char> buffer = text.Length <= maxStackallocCharBufferSize
+                ? stackalloc char[text.Length]
+                : (rentedBufferArray = ArrayPool<char>.Shared.Rent(text.Length));
+
+            int i = 0;
+            var remainingBuffer = buffer;
+            foreach (Rune c in text.EnumerateRunes ())
             {
+                if (c == hotKeySpecifier && i == hotPos)
+                {
+                    i++;
+                    continue;
+                }
+                int charsWritten = c.EncodeToUtf16 (remainingBuffer);
+                remainingBuffer = remainingBuffer [charsWritten..];
                 i++;
+            }
 
-                continue;
+            ReadOnlySpan<char> newText = buffer [..^remainingBuffer.Length];
+            // If the resulting string would be the same as original then just return the original.
+            if (newText.Equals(text, StringComparison.Ordinal))
+            {
+                return text;
             }
 
-            start += c;
-            i++;
+            return new string (newText);
+        }
+        finally
+        {
+            if (rentedBufferArray != null)
+            {
+                ArrayPool<char>.Shared.Return (rentedBufferArray);
+            }
         }
-
-        return start;
     }
 
     #endregion // Static Members

+ 2 - 17
Terminal.Gui/View/Adornment/Adornment.cs

@@ -11,7 +11,7 @@ namespace Terminal.Gui;
 ///     <para>
 ///         Each of <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/> has slightly different
 ///         behavior relative to <see cref="ColorScheme"/>, <see cref="View.SetFocus()"/>, keyboard input, and
-///         mouse input. Each can be customized by manipulating their Subviews.
+///         mouse input. Each can be customized by manipulating their SubViews.
 ///     </para>
 /// </remarsk>
 public class Adornment : View, IDesignable
@@ -82,21 +82,6 @@ public class Adornment : View, IDesignable
     #endregion Thickness
 
     #region View Overrides
-
-    /// <summary>
-    ///     Adornments cannot be used as sub-views (see <see cref="Parent"/>); setting this property will throw
-    ///     <see cref="InvalidOperationException"/>.
-    /// </summary>
-    /// <remarks>
-    ///     While there are no real use cases for an Adornment being a subview, it is not explicitly dis-allowed to support
-    ///     testing. E.g. in AllViewsTester.
-    /// </remarks>
-    public override View? SuperView
-    {
-        get => base.SuperView!;
-        set => throw new InvalidOperationException (@"Adornments can not be Subviews or have SuperViews. Use Parent instead.");
-    }
-
     /// <summary>
     ///     Gets the rectangle that describes the area of the Adornment. The Location is always (0,0).
     ///     The size is the size of the <see cref="View.Frame"/>.
@@ -180,7 +165,7 @@ public class Adornment : View, IDesignable
     protected override bool OnDrawingText () { return Thickness == Thickness.Empty; }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingSubviews () { return Thickness == Thickness.Empty; }
+    protected override bool OnDrawingSubViews () { return Thickness == Thickness.Empty; }
 
 
     /// <summary>Does nothing for Adornment</summary>

+ 1 - 1
Terminal.Gui/View/Adornment/Border.cs

@@ -711,7 +711,7 @@ public class Border : Adornment
         {
             Attribute focus = Parent.GetNormalColor ();
 
-            if (Parent.SuperView is { } && Parent.SuperView?.Subviews!.Count (s => s.CanFocus) > 1)
+            if (Parent.SuperView is { } && Parent.SuperView?.SubViews!.Count (s => s.CanFocus) > 1)
             {
                 // Only use focus color if there are multiple focusable views
                 focus = GetFocusColor ();

+ 3 - 3
Terminal.Gui/View/Adornment/Margin.cs

@@ -32,7 +32,7 @@ public class Margin : Adornment
         // BUGBUG: We should not set HighlightStyle.Pressed here, but wherever it is actually needed
         // HighlightStyle |= HighlightStyle.Pressed;
         Highlight += Margin_Highlight;
-        SubviewLayout += Margin_LayoutStarted;
+        SubViewLayout += Margin_LayoutStarted;
 
         // Margin should not be focusable
         CanFocus = false;
@@ -48,7 +48,7 @@ public class Margin : Adornment
 
     internal void CacheClip ()
     {
-        if (Thickness != Thickness.Empty)
+        if (Thickness != Thickness.Empty && ShadowStyle != ShadowStyle.None)
         {
             // PERFORMANCE: How expensive are these clones?
             _cachedClip = GetClip ()?.Clone ();
@@ -82,7 +82,7 @@ public class Margin : Adornment
 
             view.NeedsDraw = false;
 
-            foreach (var subview in view.Subviews)
+            foreach (var subview in view.SubViews)
             {
                 stack.Push (subview);
             }

+ 1 - 1
Terminal.Gui/View/Layout/Dim.cs

@@ -96,7 +96,7 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
     public static Dim? Absolute (int size) { return new DimAbsolute (size); }
 
     /// <summary>
-    ///     Creates a <see cref="Dim"/> object that automatically sizes the view to fit all the view's Content, Subviews, and/or Text.
+    ///     Creates a <see cref="Dim"/> object that automatically sizes the view to fit all the view's Content, SubViews, and/or Text.
     /// </summary>
     /// <remarks>
     ///     <para>

+ 23 - 23
Terminal.Gui/View/Layout/DimAuto.cs

@@ -72,7 +72,7 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
         {
             maxCalculatedSize = textSize;
 
-            if (us is { ContentSizeTracksViewport: false, Subviews.Count: 0 })
+            if (us is { ContentSizeTracksViewport: false, InternalSubViews.Count: 0 })
             {
                 // ContentSize was explicitly set. Use `us.ContentSize` to determine size.
                 maxCalculatedSize = dimension == Dimension.Width ? us.GetContentSize ().Width : us.GetContentSize ().Height;
@@ -81,12 +81,12 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
             {
                 // TOOD: All the below is a naive implementation. It may be possible to optimize this.
 
-                List<View> includedSubviews = us.Subviews.ToList ();
+                List<View> includedSubViews = us.InternalSubViews.ToList ();
 
                 // If [x] it can cause `us.ContentSize` to change.
                 // If [ ] it doesn't need special processing for us to determine `us.ContentSize`.
 
-                // -------------------- Pos types that are dependent on `us.Subviews`
+                // -------------------- Pos types that are dependent on `us.SubViews`
                 // [ ] PosAlign     - Position is dependent on other views with `GroupId` AND `us.ContentSize`
                 // [x] PosView      - Position is dependent on `subview.Target` - it can cause a change in `us.ContentSize`
                 // [x] PosCombine   - Position is dependent if `Pos.Has [one of the above]` - it can cause a change in `us.ContentSize`
@@ -98,11 +98,11 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
                 // [ ] PosPercent   - Position is dependent `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
                 // [x] PosCombine   - Position is dependent if `Pos.Has [one of the above]` - it can cause a change in `us.ContentSize`
 
-                // -------------------- Pos types that are not dependent on either `us.Subviews` or `us.ContentSize`
+                // -------------------- Pos types that are not dependent on either `us.SubViews` or `us.ContentSize`
                 // [ ] PosAbsolute  - Position is fixed.
                 // [ ] PosFunc      - Position is internally calculated.
 
-                // -------------------- Dim types that are dependent on `us.Subviews`
+                // -------------------- Dim types that are dependent on `us.SubViews`
                 // [x] DimView      - Dimension is dependent on `subview.Target`
                 // [x] DimCombine   - Dimension is dependent if `Dim.Has [one of the above]` - it can cause a change in `us.ContentSize`
 
@@ -111,7 +111,7 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
                 // [ ] DimPercent   - Dimension is dependent on `us.ContentSize` - Will always be 0 if there is no other content that makes the superview have a size.
                 // [ ] DimCombine   - Dimension is dependent if `Dim.Has [one of the above]`
 
-                // -------------------- Dim types that are not dependent on either `us.Subviews` or `us.ContentSize`
+                // -------------------- Dim types that are not dependent on either `us.SubViews` or `us.ContentSize`
                 // [ ] DimAuto      - Dimension is internally calculated
                 // [ ] DimAbsolute  - Dimension is fixed
                 // [ ] DimFunc      - Dimension is internally calculated
@@ -128,7 +128,7 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
 
                 if (dimension == Dimension.Width)
                 {
-                    notDependentSubViews = includedSubviews.Where (
+                    notDependentSubViews = includedSubViews.Where (
                                                                    v => v.Width is { }
                                                                         && (v.X is PosAbsolute or PosFunc
                                                                             || v.Width is DimAuto
@@ -144,7 +144,7 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
                 }
                 else
                 {
-                    notDependentSubViews = includedSubviews.Where (
+                    notDependentSubViews = includedSubViews.Where (
                                                                    v => v.Height is { }
                                                                         && (v.Y is PosAbsolute or PosFunc
                                                                             || v.Height is DimAuto
@@ -197,11 +197,11 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
 
                 if (dimension == Dimension.Width)
                 {
-                    centeredSubViews = us.Subviews.Where (v => v.X.Has<PosCenter> (out _)).ToList ();
+                    centeredSubViews = us.InternalSubViews.Where (v => v.X.Has<PosCenter> (out _)).ToList ();
                 }
                 else
                 {
-                    centeredSubViews = us.Subviews.Where (v => v.Y.Has<PosCenter> (out _)).ToList ();
+                    centeredSubViews = us.InternalSubViews.Where (v => v.Y.Has<PosCenter> (out _)).ToList ();
                 }
 
                 viewsNeedingLayout.AddRange (centeredSubViews);
@@ -241,7 +241,7 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
                 var maxAlign = 0;
 
                 // Use Linq to get a list of distinct GroupIds from the subviews
-                List<int> groupIds = includedSubviews.Select (
+                List<int> groupIds = includedSubViews.Select (
                                                               v =>
                                                               {
                                                                   return dimension switch
@@ -259,7 +259,7 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
                 foreach (int groupId in groupIds.Where (g => g != -1))
                 {
                     // PERF: If this proves a perf issue, consider caching a ref to this list in each item
-                    List<PosAlign?> posAlignsInGroup = includedSubviews.Where (v => PosAlign.HasGroupId (v, dimension, groupId))
+                    List<PosAlign?> posAlignsInGroup = includedSubViews.Where (v => PosAlign.HasGroupId (v, dimension, groupId))
                                                                        .Select (v => dimension == Dimension.Width ? v.X as PosAlign : v.Y as PosAlign)
                                                                        .ToList ();
 
@@ -268,7 +268,7 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
                         continue;
                     }
 
-                    maxAlign = PosAlign.CalculateMinDimension (groupId, includedSubviews, dimension);
+                    maxAlign = PosAlign.CalculateMinDimension (groupId, includedSubViews, dimension);
                 }
 
                 maxCalculatedSize = int.Max (maxCalculatedSize, maxAlign);
@@ -282,11 +282,11 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
 
                 if (dimension == Dimension.Width)
                 {
-                    anchoredSubViews = includedSubviews.Where (v => v.X.Has<PosAnchorEnd> (out _)).ToList ();
+                    anchoredSubViews = includedSubViews.Where (v => v.X.Has<PosAnchorEnd> (out _)).ToList ();
                 }
                 else
                 {
-                    anchoredSubViews = includedSubviews.Where (v => v.Y.Has<PosAnchorEnd> (out _)).ToList ();
+                    anchoredSubViews = includedSubViews.Where (v => v.Y.Has<PosAnchorEnd> (out _)).ToList ();
                 }
 
                 viewsNeedingLayout.AddRange (anchoredSubViews);
@@ -324,11 +324,11 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
 
                 if (dimension == Dimension.Width)
                 {
-                    posViewSubViews = includedSubviews.Where (v => v.X.Has<PosView> (out _)).ToList ();
+                    posViewSubViews = includedSubViews.Where (v => v.X.Has<PosView> (out _)).ToList ();
                 }
                 else
                 {
-                    posViewSubViews = includedSubviews.Where (v => v.Y.Has<PosView> (out _)).ToList ();
+                    posViewSubViews = includedSubViews.Where (v => v.Y.Has<PosView> (out _)).ToList ();
                 }
 
                 for (var i = 0; i < posViewSubViews.Count; i++)
@@ -358,11 +358,11 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
 
                 if (dimension == Dimension.Width)
                 {
-                    dimViewSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has<DimView> (out _)).ToList ();
+                    dimViewSubViews = includedSubViews.Where (v => v.Width is { } && v.Width.Has<DimView> (out _)).ToList ();
                 }
                 else
                 {
-                    dimViewSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has<DimView> (out _)).ToList ();
+                    dimViewSubViews = includedSubViews.Where (v => v.Height is { } && v.Height.Has<DimView> (out _)).ToList ();
                 }
 
                 for (var i = 0; i < dimViewSubViews.Count; i++)
@@ -391,11 +391,11 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
 
                 if (dimension == Dimension.Width)
                 {
-                    dimAutoSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has<DimAuto> (out _)).ToList ();
+                    dimAutoSubViews = includedSubViews.Where (v => v.Width is { } && v.Width.Has<DimAuto> (out _)).ToList ();
                 }
                 else
                 {
-                    dimAutoSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has<DimAuto> (out _)).ToList ();
+                    dimAutoSubViews = includedSubViews.Where (v => v.Height is { } && v.Height.Has<DimAuto> (out _)).ToList ();
                 }
 
                 for (var i = 0; i < dimAutoSubViews.Count; i++)
@@ -423,11 +423,11 @@ public record DimAuto (Dim? MaximumContentDim, Dim? MinimumContentDim, DimAutoSt
 
                 //if (dimension == Dimension.Width)
                 //{
-                //    DimFillSubViews = includedSubviews.Where (v => v.Width is { } && v.Width.Has<DimFill> (out _)).ToList ();
+                //    DimFillSubViews = includedSubViews.Where (v => v.Width is { } && v.Width.Has<DimFill> (out _)).ToList ();
                 //}
                 //else
                 //{
-                //    DimFillSubViews = includedSubviews.Where (v => v.Height is { } && v.Height.Has<DimFill> (out _)).ToList ();
+                //    DimFillSubViews = includedSubViews.Where (v => v.Height is { } && v.Height.Has<DimFill> (out _)).ToList ();
                 //}
 
                 //for (var i = 0; i < DimFillSubViews.Count; i++)

+ 4 - 4
Terminal.Gui/View/Layout/DimAutoStyle.cs

@@ -10,12 +10,12 @@ namespace Terminal.Gui;
 public enum DimAutoStyle
 {
     /// <summary>
-    ///     The dimensions will be computed based on the View's <see cref="View.GetContentSize ()"/> and/or <see cref="View.Subviews"/>.
+    ///     The dimensions will be computed based on the View's <see cref="View.GetContentSize ()"/> and/or <see cref="View.SubViews"/>.
     ///     <para>
     ///         If <see cref="View.ContentSizeTracksViewport"/> is <see langword="true"/>, <see cref="View.GetContentSize ()"/> will be used to determine the dimension.
     ///     </para>
     ///     <para>
-    ///         Otherwise, the Subview in <see cref="View.Subviews"/> with the largest corresponding position plus dimension
+    ///         Otherwise, the SubView in <see cref="View.SubViews"/> with the largest corresponding position plus dimension
     ///         will determine the dimension.
     ///     </para>
     ///     <para>
@@ -31,7 +31,7 @@ public enum DimAutoStyle
     ///         will be used to determine the dimension.
     ///     </para>
     ///     <para>
-    ///         The corresponding dimensions of <see cref="View.GetContentSize ()"/> and/or <see cref="View.Subviews"/> will be ignored.
+    ///         The corresponding dimensions of <see cref="View.GetContentSize ()"/> and/or <see cref="View.SubViews"/> will be ignored.
     ///     </para>
     ///     <para>
     ///         If <see cref="DimAuto.MaximumContentDim"/> is set, the dimension will be the maximum of the formatted text and the
@@ -42,7 +42,7 @@ public enum DimAutoStyle
 
     /// <summary>
     ///     The dimension will be computed using the largest of the view's <see cref="View.Text"/>, <see cref="View.GetContentSize ()"/>, and
-    ///     <see cref="View.Subviews"/> corresponding dimension
+    ///     <see cref="View.SubViews"/> corresponding dimension
     /// </summary>
     Auto = Content | Text,
 }

+ 1 - 1
Terminal.Gui/View/Layout/LayoutEventArgs.cs

@@ -1,6 +1,6 @@
 namespace Terminal.Gui;
 
-/// <summary>Event arguments for the <see cref="View.SubviewsLaidOut"/> event.</summary>
+/// <summary>Event arguments for the <see cref="View.SubViewsLaidOut"/> event.</summary>
 public class LayoutEventArgs : EventArgs
 {
     /// <summary>Creates a new instance of the <see cref="Terminal.Gui.LayoutEventArgs"/> class.</summary>

+ 2 - 2
Terminal.Gui/View/Layout/PosAlign.cs

@@ -58,7 +58,7 @@ public record PosAlign : Pos
     /// <param name="views"></param>
     /// <param name="dimension"></param>
     /// <returns></returns>
-    public static int CalculateMinDimension (int groupId, IList<View> views, Dimension dimension)
+    public static int CalculateMinDimension (int groupId, IReadOnlyCollection<View> views, Dimension dimension)
     {
         int dimensionsSum = 0;
         foreach (var view in views)
@@ -117,7 +117,7 @@ public record PosAlign : Pos
         }
         else
         {
-            groupViews = us.SuperView!.Subviews.Where (v => HasGroupId (v, dimension, GroupId)).ToList ();
+            groupViews = us.SuperView!.SubViews.Where (v => HasGroupId (v, dimension, GroupId)).ToList ();
         }
 
         AlignAndUpdateGroup (GroupId, groupViews, dimension, superviewDimension);

+ 2 - 2
Terminal.Gui/View/SuperViewChangedEventArgs.cs

@@ -2,7 +2,7 @@
 
 /// <summary>
 ///     Args for events where the <see cref="View.SuperView"/> of a <see cref="View"/> is changed (e.g.
-///     <see cref="View.Removed"/> / <see cref="View.Added"/> events).
+///     <see cref="View.Removed"/> / <see cref="View.IsAddedChanged"/> events).
 /// </summary>
 public class SuperViewChangedEventArgs : EventArgs
 {
@@ -20,7 +20,7 @@ public class SuperViewChangedEventArgs : EventArgs
 
     /// <summary>
     ///     The parent.  For <see cref="View.Removed"/> this is the old parent (new parent now being null).  For
-    ///     <see cref="View.Added"/> it is the new parent to whom view now belongs.
+    ///     <see cref="View.IsAddedChanged"/> it is the new parent to whom view now belongs.
     /// </summary>
     public View SuperView { get; }
 }

+ 3 - 3
Terminal.Gui/View/View.Adornments.cs

@@ -60,7 +60,7 @@ public partial class View // Adornments
     ///     <para>
     ///         Changing the size of an adornment (<see cref="Margin"/>, <see cref="Border"/>, or <see cref="Padding"/>) will
     ///         change the size of <see cref="Frame"/> which will call <see cref="SetNeedsLayout"/> to update the layout of the
-    ///         <see cref="SuperView"/> and its <see cref="Subviews"/>.
+    ///         <see cref="SuperView"/> and its <see cref="SubViews"/>.
     ///     </para>
     /// </remarks>
     public Margin? Margin { get; private set; }
@@ -116,7 +116,7 @@ public partial class View // Adornments
     ///     <para>
     ///         Changing the size of an adornment (<see cref="Margin"/>, <see cref="Border"/>, or <see cref="Padding"/>) will
     ///         change the size of <see cref="Frame"/> which will call <see cref="SetNeedsLayout"/> to update the layout of the
-    ///         <see cref="SuperView"/> and its <see cref="Subviews"/>.
+    ///         <see cref="SuperView"/> and its <see cref="SubViews"/>.
     ///     </para>
     /// </remarks>
     public Border? Border { get; private set; }
@@ -233,7 +233,7 @@ public partial class View // Adornments
     ///     <para>
     ///         Changing the size of an adornment (<see cref="Margin"/>, <see cref="Border"/>, or <see cref="Padding"/>) will
     ///         change the size of <see cref="Frame"/> which will call <see cref="SetNeedsLayout"/> to update the layout of the
-    ///         <see cref="SuperView"/> and its <see cref="Subviews"/>.
+    ///         <see cref="SuperView"/> and its <see cref="SubViews"/>.
     ///     </para>
     /// </remarks>
     public Padding? Padding { get; private set; }

+ 2 - 2
Terminal.Gui/View/View.Command.cs

@@ -90,8 +90,8 @@ public partial class View // Command APIs
         //  - bubbled up the SuperView hierarchy.
         if (!args.Cancel)
         {
-            // If there's an IsDefault peer view in Subviews, try it
-            var isDefaultView = SuperView?.Subviews.FirstOrDefault (v => v is Button { IsDefault: true });
+            // If there's an IsDefault peer view in SubViews, try it
+            var isDefaultView = SuperView?.InternalSubViews.FirstOrDefault (v => v is Button { IsDefault: true });
 
             if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button)
             {

+ 4 - 4
Terminal.Gui/View/View.Content.cs

@@ -25,7 +25,7 @@ public partial class View
     ///     <para>
     ///         If not explicitly set, and the View has visible subviews, <see cref="GetContentSize ()"/> will return the
     ///         maximum
-    ///         position + dimension of the Subviews, supporting <see cref="Dim.Auto"/> with the
+    ///         position + dimension of the SubViews, supporting <see cref="Dim.Auto"/> with the
     ///         <see cref="DimAutoStyle.Content"/> flag set.
     ///     </para>
     ///     <para>
@@ -68,7 +68,7 @@ public partial class View
     ///     <para>
     ///         If the content size was not explicitly set by <see cref="SetContentSize"/>, and the View has visible subviews, <see cref="GetContentSize ()"/> will return the
     ///         maximum
-    ///         position + dimension of the Subviews, supporting <see cref="Dim.Auto"/> with the
+    ///         position + dimension of the SubViews, supporting <see cref="Dim.Auto"/> with the
     ///         <see cref="DimAutoStyle.Content"/> flag set.
     ///     </para>
     ///     <para>
@@ -109,7 +109,7 @@ public partial class View
     ///                     disabled.
     ///                 </para>
     ///                 <para>
-    ///                     The behavior of <see cref="DimAutoStyle.Content"/> will be to use position and size of the Subviews
+    ///                     The behavior of <see cref="DimAutoStyle.Content"/> will be to use position and size of the SubViews
     ///                     to
     ///                     determine the size of the view, ignoring <see cref="GetContentSize ()"/>.
     ///                 </para>
@@ -128,7 +128,7 @@ public partial class View
     ///                     The behavior of <see cref="DimAutoStyle.Content"/> will be to use <see cref="GetContentSize ()"/>
     ///                     to
     ///                     determine the
-    ///                     size of the view, ignoring the position and size of the Subviews.
+    ///                     size of the view, ignoring the position and size of the SubViews.
     ///                 </para>
     ///             </description>
     ///         </item>

+ 56 - 46
Terminal.Gui/View/View.Drawing.cs

@@ -27,6 +27,7 @@ public partial class View // Drawing APIs
             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 partial class View // Drawing APIs
         {
             // ------------------------------------
             // 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);
 
             // ------------------------------------
@@ -78,11 +79,11 @@ public partial class View // Drawing APIs
             DoClearViewport ();
 
             // ------------------------------------
-            // Draw the subviews first (order matters: Subviews, Text, Content)
+            // Draw the subviews first (order matters: SubViews, Text, Content)
             if (SubViewNeedsDraw)
             {
                 DoSetAttribute ();
-                DoDrawSubviews (context);
+                DoDrawSubViews (context);
             }
 
             // ------------------------------------
@@ -106,7 +107,7 @@ public partial class View // Drawing APIs
             // ------------------------------------
             // 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 partial class View // Drawing APIs
         }
 
         // ------------------------------------
-        // 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,12 +132,15 @@ public partial class View // Drawing APIs
 
     #region DrawAdornments
 
-    private void DoDrawBorderAndPaddingSubViews ()
+    private void DoDrawAdornmentsSubViews ()
     {
-        if (Border?.Subviews is { } && Border.Thickness != Thickness.Empty)
+
+        // 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.
-            foreach (View subview in Border.Subviews.Where (v => v.Visible || v.Id == "DrawIndicator"))
+            foreach (View subview in Border.SubViews.Where (v => v.Visible || v.Id == "DrawIndicator"))
             {
                 if (subview.Id != "DrawIndicator")
                 {
@@ -147,24 +151,24 @@ public partial class View // Drawing APIs
             }
 
             Region? saved = Border?.AddFrameToClip ();
-            Border?.DoDrawSubviews ();
+            Border?.DoDrawSubViews ();
             SetClip (saved);
         }
 
-        if (Padding?.Subviews is { } && Padding.Thickness != Thickness.Empty)
+        if (Padding?.SubViews is { } && Padding.Thickness != Thickness.Empty)
         {
-            foreach (View subview in Padding.Subviews)
+            foreach (View subview in Padding.SubViews)
             {
                 subview.SetNeedsDraw ();
             }
 
             Region? saved = Padding?.AddFrameToClip ();
-            Padding?.DoDrawSubviews ();
+            Padding?.DoDrawSubViews ();
             SetClip (saved);
         }
     }
 
-    private void DoDrawBorderAndPadding (Region? originalClip)
+    private void DoDrawAdornments (Region? originalClip)
     {
         if (this is Adornment)
         {
@@ -191,30 +195,31 @@ public partial class View // Drawing APIs
 
         if (SubViewNeedsDraw)
         {
-            // A Subview may add to the LineCanvas. This ensures any Adornment LineCanvas updates happen.
+            // 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 ();
     }
 
     /// <summary>
-    ///     Causes <see cref="Border"/> and <see cref="Padding"/> to be drawn.
+    ///     Causes <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/> to be drawn.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         <see cref="Margin"/> is drawn in a separate pass.
+    ///         <see cref="Margin"/> is drawn in a separate pass if <see cref="ShadowStyle"/> is set.
     ///     </para>
     /// </remarks>
-    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 partial class View // Drawing APIs
             Padding?.Draw ();
         }
 
+
+        if (Margin is { } && Margin.Thickness != Thickness.Empty && Margin.ShadowStyle == ShadowStyle.None)
+        {
+            Margin?.Draw ();
+        }
     }
 
     private void ClearFrame ()
@@ -255,7 +265,7 @@ public partial class View // Drawing APIs
     ///     false (the default), this method will cause the <see cref="LineCanvas"/> be prepared to be rendered.
     /// </summary>
     /// <returns><see langword="true"/> to stop further drawing of the Adornments.</returns>
-    protected virtual bool OnDrawingBorderAndPadding () { return false; }
+    protected virtual bool OnDrawingAdornments () { return false; }
 
     #endregion DrawAdornments
 
@@ -525,22 +535,22 @@ public partial class View // Drawing APIs
 
     #endregion DrawContent
 
-    #region DrawSubviews
+    #region DrawSubViews
 
-    private void DoDrawSubviews (DrawContext? context = null)
+    private void DoDrawSubViews (DrawContext? context = null)
     {
-        if (OnDrawingSubviews (context))
+        if (OnDrawingSubViews (context))
         {
             return;
         }
 
-        if (OnDrawingSubviews ())
+        if (OnDrawingSubViews ())
         {
             return;
         }
 
         var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context);
-        DrawingSubviews?.Invoke (this, dev);
+        DrawingSubViews?.Invoke (this, dev);
 
         if (dev.Cancel)
         {
@@ -552,44 +562,44 @@ public partial class View // Drawing APIs
             return;
         }
 
-        DrawSubviews (context);
+        DrawSubViews (context);
     }
 
     /// <summary>
-    ///     Called when the <see cref="Subviews"/> are to be drawn.
+    ///     Called when the <see cref="SubViews"/> are to be drawn.
     /// </summary>
     /// <param name="context">The draw context to report drawn areas to, or null if not tracking.</param>
-    /// <returns><see langword="true"/> to stop further drawing of <see cref="Subviews"/>.</returns>
-    protected virtual bool OnDrawingSubviews (DrawContext? context) { return false; }
+    /// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns>
+    protected virtual bool OnDrawingSubViews (DrawContext? context) { return false; }
 
     /// <summary>
-    ///     Called when the <see cref="Subviews"/> are to be drawn.
+    ///     Called when the <see cref="SubViews"/> are to be drawn.
     /// </summary>
-    /// <returns><see langword="true"/> to stop further drawing of <see cref="Subviews"/>.</returns>
-    protected virtual bool OnDrawingSubviews () { return false; }
+    /// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns>
+    protected virtual bool OnDrawingSubViews () { return false; }
 
-    /// <summary>Raised when the <see cref="Subviews"/> are to be drawn.</summary>
+    /// <summary>Raised when the <see cref="SubViews"/> are to be drawn.</summary>
     /// <remarks>
     /// </remarks>
     /// <returns>
     ///     Set <see cref="CancelEventArgs.Cancel"/> to <see langword="true"/> to stop further drawing of
-    ///     <see cref="Subviews"/>.
+    ///     <see cref="SubViews"/>.
     /// </returns>
-    public event EventHandler<DrawEventArgs>? DrawingSubviews;
+    public event EventHandler<DrawEventArgs>? DrawingSubViews;
 
     /// <summary>
-    ///     Draws the <see cref="Subviews"/>.
+    ///     Draws the <see cref="SubViews"/>.
     /// </summary>
     /// <param name="context">The draw context to report drawn areas to, or null if not tracking.</param>
-    public void DrawSubviews (DrawContext? context = null)
+    public void DrawSubViews (DrawContext? context = null)
     {
-        if (_subviews is null)
+        if (InternalSubViews.Count == 0)
         {
             return;
         }
 
         // Draw the subviews in reverse order to leverage clipping.
-        foreach (View view in _subviews.Where (view => view.Visible).Reverse ())
+        foreach (View view in InternalSubViews.Where (view => view.Visible).Reverse ())
         {
             // TODO: HACK - This forcing of SetNeedsDraw with SuperViewRendersLineCanvas enables auto line join to work, but is brute force.
             if (view.SuperViewRendersLineCanvas || view.ViewportSettings.HasFlag (ViewportSettings.Transparent))
@@ -606,7 +616,7 @@ public partial class View // Drawing APIs
         }
     }
 
-    #endregion DrawSubviews
+    #endregion DrawSubViews
 
     #region DrawLineCanvas
 
@@ -635,7 +645,7 @@ public partial class View // Drawing APIs
     /// <summary>
     ///     Gets or sets whether this View will use it's SuperView's <see cref="LineCanvas"/> for rendering any
     ///     lines. If <see langword="true"/> the rendering of any borders drawn by this Frame will be done by its parent's
-    ///     SuperView. If <see langword="false"/> (the default) this View's <see cref="OnDrawingBorderAndPadding"/> method will
+    ///     SuperView. If <see langword="false"/> (the default) this View's <see cref="OnDrawingAdornments"/> method will
     ///     be
     ///     called to render the borders.
     /// </summary>
@@ -772,7 +782,7 @@ public partial class View // Drawing APIs
         }
     }
 
-    /// <summary>Gets whether any Subviews need to be redrawn.</summary>
+    /// <summary>Gets whether any SubViews need to be redrawn.</summary>
     public bool SubViewNeedsDraw { get; private set; }
 
     /// <summary>Sets that the <see cref="Viewport"/> of this View needs to be redrawn.</summary>
@@ -844,7 +854,7 @@ public partial class View // Drawing APIs
         }
 
         // There was multiple enumeration error here, so calling ToArray - probably a stop gap
-        foreach (View subview in Subviews.ToArray ())
+        foreach (View subview in SubViews.ToArray ())
         {
             if (subview.Frame.IntersectsWith (viewPortRelativeRegion))
             {
@@ -898,7 +908,7 @@ public partial class View // Drawing APIs
             Padding?.ClearNeedsDraw ();
         }
 
-        foreach (View subview in Subviews)
+        foreach (View subview in SubViews)
         {
             subview.ClearNeedsDraw ();
         }

+ 188 - 97
Terminal.Gui/View/View.Hierarchy.cs

@@ -7,64 +7,115 @@ namespace Terminal.Gui;
 public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.)
 {
     [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "<Pending>")]
-    private static readonly IList<View> _empty = new List<View> (0).AsReadOnly ();
+    private static readonly IReadOnlyCollection<View> _empty = [];
 
-    private List<View>? _subviews; // This is null, and allocated on demand.
+    private readonly List<View>? _subviews = [];
 
-    // Internally, we use InternalSubviews rather than subviews, as we do not expect us
-    // to make the same mistakes our users make when they poke at the Subviews.
-    internal IList<View> InternalSubviews => _subviews ?? _empty;
+    // Internally, we use InternalSubViews rather than subviews, as we do not expect us
+    // to make the same mistakes our users make when they poke at the SubViews.
+    internal IList<View> InternalSubViews => _subviews ?? [];
 
-    /// <summary>This returns a list of the subviews contained by this view.</summary>
-    /// <value>The subviews.</value>
-    public IList<View> Subviews => _subviews?.AsReadOnly () ?? _empty;
+    /// <summary>Gets the list of SubViews.</summary>
+    /// <remarks>
+    ///     Use <see cref="Add(Terminal.Gui.View?)"/> and <see cref="Remove(Terminal.Gui.View?)"/> to add or remove subviews.
+    /// </remarks>
+    public IReadOnlyCollection<View> SubViews => InternalSubViews?.AsReadOnly () ?? _empty;
 
     private View? _superView;
 
-    /// <summary>Returns the container for this view, or null if this view has not been added to a container.</summary>
-    /// <value>The super view.</value>
-    public virtual View? SuperView
+    /// <summary>
+    ///     Gets this Views SuperView (the View's container), or <see langword="null"/> if this view has not been added as a
+    ///     SubView.
+    /// </summary>
+    /// <seealso cref="OnSuperViewChanged"/>
+    /// <seealso cref="SuperViewChanged"/>
+    public View? SuperView
     {
         get => _superView!;
-        set => throw new NotImplementedException ();
+        private set => SetSuperView (value);
     }
 
-    #region AddRemove
+    private void SetSuperView (View? value)
+    {
+        if (_superView == value)
+        {
+            return;
+        }
+
+        _superView = value;
+        RaiseSuperViewChanged ();
+    }
+
+    private void RaiseSuperViewChanged ()
+    {
+        SuperViewChangedEventArgs args = new (SuperView, this);
+        OnSuperViewChanged (args);
+
+        SuperViewChanged?.Invoke (this, args);
+    }
+
+    /// <summary>
+    ///     Called when the SuperView of this View has changed.
+    /// </summary>
+    /// <param name="e"></param>
+    protected virtual void OnSuperViewChanged (SuperViewChangedEventArgs e) { }
+
+    /// <summary>Raised when the SuperView of this View has changed.</summary>
+    public event EventHandler<SuperViewChangedEventArgs>? SuperViewChanged;
 
-    /// <summary>Indicates whether the view was added to <see cref="SuperView"/>.</summary>
-    public bool IsAdded { get; private set; }
+    #region AddRemove
 
-    /// <summary>Adds a subview (child) to this view.</summary>
+    /// <summary>Adds a SubView (child) to this view.</summary>
     /// <remarks>
     ///     <para>
-    ///         The Views that have been added to this view can be retrieved via the <see cref="Subviews"/> property. See also
-    ///         <seealso cref="Remove(View)"/> <seealso cref="RemoveAll"/>
+    ///         The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property. 
+    ///     </para>
+    ///     <para>
+    ///         To check if a View has been added to this View, compare it's <see cref="SuperView"/> property to this View.
     ///     </para>
     ///     <para>
-    ///         Subviews will be disposed when this View is disposed. In other-words, calling this method causes
+    ///         SubViews will be disposed when this View is disposed. In other-words, calling this method causes
     ///         the lifecycle of the subviews to be transferred to this View.
     ///     </para>
+    ///     <para>
+    ///         Calls/Raises the <see cref="OnSubViewAdded"/>/<see cref="SubViewAdded"/> event.
+    ///     </para>
+    ///     <para>
+    ///         The <see cref="OnSuperViewChanged"/>/<see cref="SuperViewChanged"/> event will be raised on the added View.
+    ///     </para>
     /// </remarks>
     /// <param name="view">The view to add.</param>
     /// <returns>The view that was added.</returns>
+    /// <seealso cref="Remove(View)"/>
+    /// <seealso cref="RemoveAll"/>
+    /// <seealso cref="OnSubViewAdded"/>
+    /// <seealso cref="SubViewAdded"/>
+
     public virtual View? Add (View? view)
     {
         if (view is null)
         {
             return null;
         }
-        if (_subviews is null)
+
+        //Debug.Assert (view.SuperView is null, $"{view} already has a SuperView: {view.SuperView}.");
+        if (view.SuperView is {})
         {
-            _subviews = [];
+            Logging.Warning ($"{view} already has a SuperView: {view.SuperView}.");
         }
 
-        Debug.WriteLineIf (_subviews.Contains (view), $"WARNING: {view} has already been added to {this}.");
+        //Debug.Assert (!InternalSubViews.Contains (view), $"{view} has already been Added to {this}.");
+        if (InternalSubViews.Contains (view))
+        {
+            Logging.Warning ($"{view} has already been Added to {this}.");
+        }
 
         // TileView likes to add views that were previously added and have HasFocus = true. No bueno.
         view.HasFocus = false;
 
-        _subviews.Add (view);
-        view._superView = this;
+        // TODO: Make this thread safe
+        InternalSubViews.Add (view);
+        view.SuperView = this;
 
         if (view is { Enabled: true, Visible: true, CanFocus: true })
         {
@@ -80,7 +131,9 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
             view.Enabled = false;
         }
 
-        OnAdded (new (this, view));
+        // Raise event indicating a subview has been added
+        // We do this before Init.
+        RaiseSubViewAdded (view);
 
         if (IsInitialized && !view.IsInitialized)
         {
@@ -94,15 +147,15 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
         return view;
     }
 
-    /// <summary>Adds the specified views (children) to the view.</summary>
+    /// <summary>Adds the specified SubView (children) to the view.</summary>
     /// <param name="views">Array of one or more views (can be optional parameter).</param>
     /// <remarks>
     ///     <para>
-    ///         The Views that have been added to this view can be retrieved via the <see cref="Subviews"/> property. See also
+    ///         The Views that have been added to this view can be retrieved via the <see cref="SubViews"/> property. See also
     ///         <seealso cref="Remove(View)"/> and <seealso cref="RemoveAll"/>.
     ///     </para>
     ///     <para>
-    ///         Subviews will be disposed when this View is disposed. In other-words, calling this method causes
+    ///         SubViews will be disposed when this View is disposed. In other-words, calling this method causes
     ///         the lifecycle of the subviews to be transferred to this View.
     ///     </para>
     /// </remarks>
@@ -119,38 +172,46 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
         }
     }
 
-    /// <summary>Event fired when this view is added to another.</summary>
-    public event EventHandler<SuperViewChangedEventArgs>? Added;
-
-    /// <summary>Method invoked when a subview is being added to this view.</summary>
-    /// <param name="e">Event where <see cref="ViewEventArgs.View"/> is the subview being added.</param>
-    public virtual void OnAdded (SuperViewChangedEventArgs e)
+    internal void RaiseSubViewAdded (View view)
     {
-        View view = e.SubView;
-        view.IsAdded = true;
-        view.Added?.Invoke (this, e);
+        OnSubViewAdded (view);
+        SubViewAdded?.Invoke (this, new (this, view));
     }
 
-    /// <summary>Method invoked when a subview is being removed from this view.</summary>
-    /// <param name="e">Event args describing the subview being removed.</param>
-    public virtual void OnRemoved (SuperViewChangedEventArgs e)
-    {
-        View view = e.SubView;
-        view.IsAdded = false;
-        view.Removed?.Invoke (this, e);
-    }
+    /// <summary>
+    ///     Called when a SubView has been added to this View.
+    /// </summary>
+    /// <remarks>
+    ///     If the SubView has not been initialized, this happens before BeginInit/EndInit is called.
+    /// </remarks>
+    /// <param name="view"></param>
+    protected virtual void OnSubViewAdded (View view) { }
 
-    /// <summary>Removes a subview added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
+    /// <summary>Raised when a SubView has been added to this View.</summary>
+    /// <remarks>
+    ///     If the SubView has not been initialized, this happens before BeginInit/EndInit is called.
+    /// </remarks>
+    public event EventHandler<SuperViewChangedEventArgs>? SubViewAdded;
+
+    /// <summary>Removes a SubView added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
     /// <remarks>
     ///     <para>
-    ///         Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
-    ///         Subview's
+    ///         Normally SubViews will be disposed when this View is disposed. Removing a SubView causes ownership of the
+    ///         SubView's
     ///         lifecycle to be transferred to the caller; the caller must call <see cref="Dispose()"/>.
     ///     </para>
+    ///     <para>
+    ///         Calls/Raises the <see cref="OnSubViewRemoved"/>/<see cref="SubViewRemoved"/> event.
+    ///     </para>
+    ///     <para>
+    ///         The <see cref="OnSuperViewChanged"/>/<see cref="SuperViewChanged"/> event will be raised on the removed View.
+    ///     </para>
     /// </remarks>
     /// <returns>
     ///     The removed View. <see langword="null"/> if the View could not be removed.
     /// </returns>
+    /// <seealso cref="OnSubViewRemoved"/>
+    /// <seealso cref="SubViewRemoved"/>"/>
     public virtual View? Remove (View? view)
     {
         if (view is null)
@@ -158,9 +219,24 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
             return null;
         }
 
-        if (_subviews is null)
+        if (InternalSubViews.Count == 0)
+        {
+           return view;
+        }
+
+        if (view.SuperView is null)
         {
-            return view;
+            Logging.Warning ($"{view} cannot be Removed. SuperView is null.");
+        }
+
+        if (view.SuperView != this)
+        {
+            Logging.Warning ($"{view} cannot be Removed. SuperView is not this ({view.SuperView}.");
+        }
+
+        if (!InternalSubViews.Contains (view))
+        {
+            Logging.Warning ($"{view} cannot be Removed. It has not been added to {this}.");
         }
 
         Rectangle touched = view.Frame;
@@ -172,22 +248,25 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
         {
             view.CanFocus = false; // If view had focus, this will ensure it doesn't and it stays that way
         }
+
         Debug.Assert (!view.HasFocus);
 
-        _subviews.Remove (view);
+        InternalSubViews.Remove (view);
 
         // Clean up focus stuff
         _previouslyFocused = null;
-        if (view._superView is { } && view._superView._previouslyFocused == this)
+
+        if (view.SuperView is { } && view.SuperView._previouslyFocused == this)
         {
-            view._superView._previouslyFocused = null;
+            view.SuperView._previouslyFocused = null;
         }
-        view._superView = null;
+
+        view.SuperView = null;
 
         SetNeedsLayout ();
         SetNeedsDraw ();
 
-        foreach (View v in _subviews)
+        foreach (View v in InternalSubViews)
         {
             if (v.Frame.IntersectsWith (touched))
             {
@@ -202,44 +281,56 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
             _previouslyFocused = null;
         }
 
-        OnRemoved (new (this, view));
+        RaiseSubViewRemoved (view);
 
         return view;
     }
 
+    internal void RaiseSubViewRemoved (View view)
+    {
+        OnSubViewRemoved (view);
+        SubViewRemoved?.Invoke (this, new (this, view));
+    }
+
     /// <summary>
-    ///     Removes all subviews (children) added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
+    ///     Called when a SubView has been removed from this View.
+    /// </summary>
+    /// <param name="view"></param>
+    protected virtual void OnSubViewRemoved (View view) { }
+
+    /// <summary>Raised when a SubView has been added to this View.</summary>
+    public event EventHandler<SuperViewChangedEventArgs>? SubViewRemoved;
+
+    /// <summary>
+    ///     Removes all SubView (children) added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
-    ///         Subview's
+    ///         Normally SubViews will be disposed when this View is disposed. Removing a SubView causes ownership of the
+    ///         SubView's
     ///         lifecycle to be transferred to the caller; the caller must call <see cref="Dispose()"/> on any Views that were
     ///         added.
     ///     </para>
     /// </remarks>
     public virtual void RemoveAll ()
     {
-        if (_subviews is null)
-        {
-            return;
-        }
-
-        while (_subviews.Count > 0)
+        while (InternalSubViews.Count > 0)
         {
-            Remove (_subviews [0]);
+            Remove (InternalSubViews [0]);
         }
     }
 
-    /// <summary>Event fired when this view is removed from another.</summary>
+#pragma warning disable CS0067 // The event is never used
+    /// <summary>Raised when a SubView has been removed from this View.</summary>
     public event EventHandler<SuperViewChangedEventArgs>? Removed;
+#pragma warning restore CS0067 // The event is never used   
 
     #endregion AddRemove
 
-    // TODO: Mark as internal. Or nuke.
+    // TODO: This drives a weird coupling of Application.Top and View. It's not clear why this is needed.
     /// <summary>Get the top superview of a given <see cref="View"/>.</summary>
     /// <returns>The superview view.</returns>
-    public View? GetTopSuperView (View? view = null, View? superview = null)
+    internal View? GetTopSuperView (View? view = null, View? superview = null)
     {
         View? top = superview ?? Application.Top;
 
@@ -257,7 +348,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     }
 
     /// <summary>
-    ///     Gets whether <paramref name="view"/> is in the Subview hierarchy of <paramref name="start"/>.
+    ///     Gets whether <paramref name="view"/> is in the SubView hierarchy of <paramref name="start"/>.
     /// </summary>
     /// <param name="start">The View at the start of the hierarchy.</param>
     /// <param name="view">The View to test.</param>
@@ -275,7 +366,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
             return true;
         }
 
-        foreach (View subView in start.Subviews)
+        foreach (View subView in start.InternalSubViews)
         {
             if (view == subView)
             {
@@ -320,87 +411,87 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     #region SubViewOrdering
 
     /// <summary>
-    ///     Moves <paramref name="subview"/> one position towards the end of the <see cref="Subviews"/> list.
+    ///     Moves <paramref name="subview"/> one position towards the end of the <see cref="SubViews"/> list.
     /// </summary>
     /// <param name="subview">The subview to move.</param>
-    public void MoveSubviewTowardsEnd (View subview)
+    public void MoveSubViewTowardsEnd (View subview)
     {
-        PerformActionForSubview (
+        PerformActionForSubView (
                                  subview,
                                  x =>
                                  {
-                                     int idx = _subviews!.IndexOf (x);
+                                     int idx = InternalSubViews!.IndexOf (x);
 
-                                     if (idx + 1 < _subviews.Count)
+                                     if (idx + 1 < InternalSubViews.Count)
                                      {
-                                         _subviews.Remove (x);
-                                         _subviews.Insert (idx + 1, x);
+                                         InternalSubViews.Remove (x);
+                                         InternalSubViews.Insert (idx + 1, x);
                                      }
                                  }
                                 );
     }
 
     /// <summary>
-    ///     Moves <paramref name="subview"/> to the end of the <see cref="Subviews"/> list.
+    ///     Moves <paramref name="subview"/> to the end of the <see cref="SubViews"/> list.
     /// </summary>
     /// <param name="subview">The subview to move.</param>
-    public void MoveSubviewToEnd (View subview)
+    public void MoveSubViewToEnd (View subview)
     {
-        PerformActionForSubview (
+        PerformActionForSubView (
                                  subview,
                                  x =>
                                  {
-                                     _subviews!.Remove (x);
-                                     _subviews.Add (x);
+                                     InternalSubViews!.Remove (x);
+                                     InternalSubViews.Add (x);
                                  }
                                 );
     }
 
     /// <summary>
-    ///     Moves <paramref name="subview"/> one position towards the start of the <see cref="Subviews"/> list.
+    ///     Moves <paramref name="subview"/> one position towards the start of the <see cref="SubViews"/> list.
     /// </summary>
     /// <param name="subview">The subview to move.</param>
-    public void MoveSubviewTowardsStart (View subview)
+    public void MoveSubViewTowardsStart (View subview)
     {
-        PerformActionForSubview (
+        PerformActionForSubView (
                                  subview,
                                  x =>
                                  {
-                                     int idx = _subviews!.IndexOf (x);
+                                     int idx = InternalSubViews!.IndexOf (x);
 
                                      if (idx > 0)
                                      {
-                                         _subviews.Remove (x);
-                                         _subviews.Insert (idx - 1, x);
+                                         InternalSubViews.Remove (x);
+                                         InternalSubViews.Insert (idx - 1, x);
                                      }
                                  }
                                 );
     }
 
     /// <summary>
-    ///     Moves <paramref name="subview"/> to the start of the <see cref="Subviews"/> list.
+    ///     Moves <paramref name="subview"/> to the start of the <see cref="SubViews"/> list.
     /// </summary>
     /// <param name="subview">The subview to move.</param>
-    public void MoveSubviewToStart (View subview)
+    public void MoveSubViewToStart (View subview)
     {
-        PerformActionForSubview (
+        PerformActionForSubView (
                                  subview,
                                  x =>
                                  {
-                                     _subviews!.Remove (x);
-                                     _subviews.Insert (0, subview);
+                                     InternalSubViews!.Remove (x);
+                                     InternalSubViews.Insert (0, subview);
                                  }
                                 );
     }
 
     /// <summary>
-    ///     Internal API that runs <paramref name="action"/> on a subview if it is part of the <see cref="Subviews"/> list.
+    ///     Internal API that runs <paramref name="action"/> on a subview if it is part of the <see cref="SubViews"/> list.
     /// </summary>
     /// <param name="subview"></param>
     /// <param name="action"></param>
-    private void PerformActionForSubview (View subview, Action<View> action)
+    private void PerformActionForSubView (View subview, Action<View> action)
     {
-        if (_subviews!.Contains (subview))
+        if (InternalSubViews.Contains (subview))
         {
             action (subview);
         }

+ 3 - 3
Terminal.Gui/View/View.Keyboard.cs

@@ -562,12 +562,12 @@ public partial class View // Keyboard APIs
             return true;
         }
 
-        if (adornment?.Subviews is null)
+        if (adornment?.InternalSubViews is null)
         {
             return false;
         }
 
-        foreach (View subview in adornment.Subviews)
+        foreach (View subview in adornment.InternalSubViews)
         {
             bool? subViewHandled = subview.InvokeCommands (key);
 
@@ -604,7 +604,7 @@ public partial class View // Keyboard APIs
         }
 
         // Now, process any HotKey bindings in the subviews
-        foreach (View subview in Subviews)
+        foreach (View subview in InternalSubViews)
         {
             if (subview == Focused)
             {

+ 33 - 33
Terminal.Gui/View/View.Layout.cs

@@ -416,7 +416,7 @@ public partial class View // Layout APIs
     {
         if (SetRelativeLayout (contentSize))
         {
-            LayoutSubviews ();
+            LayoutSubViews ();
 
             // Debug.Assert(!NeedsLayout);
             return true;
@@ -475,7 +475,7 @@ public partial class View // Layout APIs
 
         CheckDimAuto ();
 
-        // TODO: Should move to View.LayoutSubviews?
+        // TODO: Should move to View.LayoutSubViews?
         SetTextFormatterSize ();
 
         int newX, newW, newY, newH;
@@ -594,9 +594,9 @@ public partial class View // Layout APIs
     ///         The position and dimensions of the view are indeterminate until the view has been initialized. Therefore, the
     ///         behavior of this method is indeterminate if <see cref="IsInitialized"/> is <see langword="false"/>.
     ///     </para>
-    ///     <para>Raises the <see cref="SubviewsLaidOut"/> event before it returns.</para>
+    ///     <para>Raises the <see cref="SubViewsLaidOut"/> event before it returns.</para>
     /// </remarks>
-    internal void LayoutSubviews ()
+    internal void LayoutSubViews ()
     {
         if (!NeedsLayout)
         {
@@ -607,23 +607,23 @@ public partial class View // Layout APIs
 
         Size contentSize = GetContentSize ();
 
-        OnSubviewLayout (new (contentSize));
-        SubviewLayout?.Invoke (this, new (contentSize));
+        OnSubViewLayout (new (contentSize));
+        SubViewLayout?.Invoke (this, new (contentSize));
 
         // The Adornments already have their Frame's set by SetRelativeLayout so we call LayoutSubViews vs. Layout here.
-        if (Margin is { Subviews.Count: > 0 })
+        if (Margin is { SubViews.Count: > 0 })
         {
-            Margin.LayoutSubviews ();
+            Margin.LayoutSubViews ();
         }
 
-        if (Border is { Subviews.Count: > 0 })
+        if (Border is { SubViews.Count: > 0 })
         {
-            Border.LayoutSubviews ();
+            Border.LayoutSubViews ();
         }
 
-        if (Padding is { Subviews.Count: > 0 })
+        if (Padding is { SubViews.Count: > 0 })
         {
-            Padding.LayoutSubviews ();
+            Padding.LayoutSubViews ();
         }
 
         // Sort out the dependencies of the X, Y, Width, Height properties
@@ -669,44 +669,44 @@ public partial class View // Layout APIs
 
         NeedsLayout = layoutStillNeeded;
 
-        OnSubviewsLaidOut (new (contentSize));
-        SubviewsLaidOut?.Invoke (this, new (contentSize));
+        OnSubViewsLaidOut (new (contentSize));
+        SubViewsLaidOut?.Invoke (this, new (contentSize));
     }
 
     /// <summary>
-    ///     Called from <see cref="LayoutSubviews"/> before any subviews
+    ///     Called from <see cref="LayoutSubViews"/> before any subviews
     ///     have been laid out.
     /// </summary>
     /// <remarks>
     ///     Override to perform tasks when the layout is changing.
     /// </remarks>
-    protected virtual void OnSubviewLayout (LayoutEventArgs args) { }
+    protected virtual void OnSubViewLayout (LayoutEventArgs args) { }
 
     /// <summary>
-    ///     Raised by <see cref="LayoutSubviews"/> before any subviews
+    ///     Raised by <see cref="LayoutSubViews"/> before any subviews
     ///     have been laid out.
     /// </summary>
     /// <remarks>
     ///     Subscribe to this event to perform tasks when the layout is changing.
     /// </remarks>
-    public event EventHandler<LayoutEventArgs>? SubviewLayout;
+    public event EventHandler<LayoutEventArgs>? SubViewLayout;
 
     /// <summary>
-    ///     Called from <see cref="LayoutSubviews"/> after all sub-views
+    ///     Called from <see cref="LayoutSubViews"/> after all sub-views
     ///     have been laid out.
     /// </summary>
     /// <remarks>
     ///     Override to perform tasks after the <see cref="View"/> has been resized or the layout has
     ///     otherwise changed.
     /// </remarks>
-    protected virtual void OnSubviewsLaidOut (LayoutEventArgs args) { }
+    protected virtual void OnSubViewsLaidOut (LayoutEventArgs args) { }
 
     /// <summary>Raised after all sub-views have been laid out.</summary>
     /// <remarks>
     ///     Subscribe to this event to perform tasks after the <see cref="View"/> has been resized or the layout has
     ///     otherwise changed.
     /// </remarks>
-    public event EventHandler<LayoutEventArgs>? SubviewsLaidOut;
+    public event EventHandler<LayoutEventArgs>? SubViewsLaidOut;
 
     #endregion Core Layout API
 
@@ -743,23 +743,23 @@ public partial class View // Layout APIs
     {
         NeedsLayout = true;
 
-        if (Margin is { Subviews.Count: > 0 })
+        if (Margin is { SubViews.Count: > 0 })
         {
             Margin.SetNeedsLayout ();
         }
 
-        if (Border is { Subviews.Count: > 0 })
+        if (Border is { SubViews.Count: > 0 })
         {
             Border.SetNeedsLayout ();
         }
 
-        if (Padding is { Subviews.Count: > 0 })
+        if (Padding is { SubViews.Count: > 0 })
         {
             Padding.SetNeedsLayout ();
         }
 
         // Use a stack to avoid recursion
-        Stack<View> stack = new (Subviews);
+        Stack<View> stack = new (SubViews);
 
         while (stack.Count > 0)
         {
@@ -769,22 +769,22 @@ public partial class View // Layout APIs
             {
                 current.NeedsLayout = true;
 
-                if (current.Margin is { Subviews.Count: > 0 })
+                if (current.Margin is { SubViews.Count: > 0 })
                 {
                     current.Margin.SetNeedsLayout ();
                 }
 
-                if (current.Border is { Subviews.Count: > 0 })
+                if (current.Border is { SubViews.Count: > 0 })
                 {
                     current.Border.SetNeedsLayout ();
                 }
 
-                if (current.Padding is { Subviews.Count: > 0 })
+                if (current.Padding is { SubViews.Count: > 0 })
                 {
                     current.Padding.SetNeedsLayout ();
                 }
 
-                foreach (View subview in current.Subviews)
+                foreach (View subview in current.SubViews)
                 {
                     stack.Push (subview);
                 }
@@ -833,7 +833,7 @@ public partial class View // Layout APIs
     /// </param>
     internal void CollectAll (View from, ref HashSet<View> nNodes, ref HashSet<(View, View)> nEdges)
     {
-        foreach (View? v in from.InternalSubviews)
+        foreach (View? v in from.InternalSubViews)
         {
             nNodes.Add (v);
             CollectPos (v.X, v, ref nNodes, ref nEdges);
@@ -1035,7 +1035,7 @@ public partial class View // Layout APIs
     /// <param name="ny">The new y location that will ensure <paramref name="viewToMove"/> will be fully visible.</param>
     /// <returns>
     ///     Either <see cref="Application.Top"/> (if <paramref name="viewToMove"/> does not have a Super View) or
-    ///     <paramref name="viewToMove"/>'s SuperView. This can be used to ensure LayoutSubviews is called on the correct View.
+    ///     <paramref name="viewToMove"/>'s SuperView. This can be used to ensure LayoutSubViews is called on the correct View.
     /// </returns>
     internal static View? GetLocationEnsuringFullVisibility (
         View viewToMove,
@@ -1184,7 +1184,7 @@ public partial class View // Layout APIs
     /// <summary>Gets or sets whether validation of <see cref="Pos"/> and <see cref="Dim"/> occurs.</summary>
     /// <remarks>
     ///     Setting this to <see langword="true"/> will enable validation of <see cref="X"/>, <see cref="Y"/>,
-    ///     <see cref="Width"/>, and <see cref="Height"/> during set operations and in <see cref="LayoutSubviews"/>. If invalid
+    ///     <see cref="Width"/>, and <see cref="Height"/> during set operations and in <see cref="LayoutSubViews"/>. If invalid
     ///     settings are discovered exceptions will be thrown indicating the error. This will impose a performance penalty and
     ///     thus should only be used for debugging.
     /// </remarks>
@@ -1207,7 +1207,7 @@ public partial class View // Layout APIs
         var heightAuto = Height as DimAuto;
 
         // Verify none of the subviews are using Dim objects that depend on the SuperView's dimensions.
-        foreach (View view in Subviews)
+        foreach (View view in SubViews)
         {
             if (widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Content) && ContentSizeTracksViewport)
             {

+ 6 - 6
Terminal.Gui/View/View.Mouse.cs

@@ -113,7 +113,7 @@ public partial class View // Mouse APIs
     }
 
     /// <summary>
-    ///     Called when the mouse moves over the View's <see cref="Frame"/> and no other non-Subview occludes it.
+    ///     Called when the mouse moves over the View's <see cref="Frame"/> and no other non-SubView occludes it.
     ///     <see cref="MouseLeave"/> will
     ///     be raised when the mouse is no longer over the <see cref="Frame"/>.
     /// </summary>
@@ -808,13 +808,13 @@ public partial class View // Mouse APIs
 
             View? subview = null;
 
-            for (int i = start.InternalSubviews.Count - 1; i >= 0; i--)
+            for (int i = start.InternalSubViews.Count - 1; i >= 0; i--)
             {
-                if (start.InternalSubviews [i].Visible
-                    && start.InternalSubviews [i].Contains (new (startOffsetX + start.Viewport.X, startOffsetY + start.Viewport.Y))
-                    && (!ignoreTransparent || !start.InternalSubviews [i].ViewportSettings.HasFlag (ViewportSettings.TransparentMouse)))
+                if (start.InternalSubViews [i].Visible
+                    && start.InternalSubViews [i].Contains (new (startOffsetX + start.Viewport.X, startOffsetY + start.Viewport.Y))
+                    && (!ignoreTransparent || !start.InternalSubViews [i].ViewportSettings.HasFlag (ViewportSettings.TransparentMouse)))
                 {
-                    subview = start.InternalSubviews [i];
+                    subview = start.InternalSubViews [i];
                     currentLocation.X = startOffsetX + start.Viewport.X;
                     currentLocation.Y = startOffsetY + start.Viewport.Y;
 

+ 14 - 14
Terminal.Gui/View/View.Navigation.cs

@@ -265,7 +265,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
     public event EventHandler? CanFocusChanged;
 
     /// <summary>
-    ///     Focuses the deepest focusable Subview if one exists. If there are no focusable Subviews then the focus is set to
+    ///     Focuses the deepest focusable SubView if one exists. If there are no focusable SubViews then the focus is set to
     ///     the view itself.
     /// </summary>
     /// <param name="direction"></param>
@@ -283,12 +283,12 @@ public partial class View // Focus and cross-view navigation management (TabStop
         return SetFocus ();
     }
 
-    /// <summary>Gets the currently focused Subview or Adornment of this view, or <see langword="null"/> if nothing is focused.</summary>
+    /// <summary>Gets the currently focused SubView or Adornment of this view, or <see langword="null"/> if nothing is focused.</summary>
     public View? Focused
     {
         get
         {
-            View? focused = Subviews.FirstOrDefault (v => v.HasFocus);
+            View? focused = SubViews.FirstOrDefault (v => v.HasFocus);
 
             if (focused is { })
             {
@@ -319,9 +319,9 @@ public partial class View // Focus and cross-view navigation management (TabStop
     public bool IsCurrentTop => Application.Top == this;
 
     /// <summary>
-    ///     Returns the most focused Subview down the subview-hierarchy.
+    ///     Returns the most focused SubView down the subview-hierarchy.
     /// </summary>
-    /// <value>The most focused Subview, or <see langword="null"/> if no Subview is focused.</value>
+    /// <value>The most focused SubView, or <see langword="null"/> if no SubView is focused.</value>
     public View? MostFocused
     {
         get
@@ -589,7 +589,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
 
         if (Arrangement.HasFlag (ViewArrangement.Overlapped))
         {
-            SuperView?.MoveSubviewToEnd (this);
+            SuperView?.MoveSubViewToEnd (this);
         }
 
         // Focus work is done. Notify.
@@ -902,39 +902,39 @@ public partial class View // Focus and cross-view navigation management (TabStop
     /// <returns></returns>
     internal View [] GetFocusChain (NavigationDirection direction, TabBehavior? behavior)
     {
-        IEnumerable<View>? filteredSubviews;
+        IEnumerable<View>? filteredSubViews;
 
         if (behavior.HasValue)
         {
-            filteredSubviews = _subviews?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true });
+            filteredSubViews = InternalSubViews?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true });
         }
         else
         {
-            filteredSubviews = _subviews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true });
+            filteredSubViews = InternalSubViews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true });
         }
 
         // How about in Adornments? 
         if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior)
         {
-            filteredSubviews = filteredSubviews?.Append (Padding);
+            filteredSubViews = filteredSubViews?.Append (Padding);
         }
 
         if (Border is { CanFocus: true, Visible: true, Enabled: true } && Border.TabStop == behavior)
         {
-            filteredSubviews = filteredSubviews?.Append (Border);
+            filteredSubViews = filteredSubViews?.Append (Border);
         }
 
         if (Margin is { CanFocus: true, Visible: true, Enabled: true } && Margin.TabStop == behavior)
         {
-            filteredSubviews = filteredSubviews?.Append (Margin);
+            filteredSubViews = filteredSubViews?.Append (Margin);
         }
 
         if (direction == NavigationDirection.Backward)
         {
-            filteredSubviews = filteredSubviews?.Reverse ();
+            filteredSubViews = filteredSubViews?.Reverse ();
         }
 
-        return filteredSubviews?.ToArray () ?? Array.Empty<View> ();
+        return filteredSubViews?.ToArray () ?? Array.Empty<View> ();
     }
 
     private TabBehavior? _tabStop;

+ 10 - 18
Terminal.Gui/View/View.cs

@@ -81,9 +81,9 @@ public partial class View : IDisposable, ISupportInitializeNotification
         DisposeAdornments ();
         DisposeScrollBars ();
 
-        for (int i = InternalSubviews.Count - 1; i >= 0; i--)
+        for (int i = InternalSubViews.Count - 1; i >= 0; i--)
         {
-            View subview = InternalSubviews [i];
+            View subview = InternalSubViews [i];
             Remove (subview);
             subview.Dispose ();
         }
@@ -98,7 +98,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
             _disposedValue = true;
         }
 
-        Debug.Assert (InternalSubviews.Count == 0);
+        Debug.Assert (InternalSubViews.Count == 0);
     }
 
     #region Constructors and Initialization
@@ -196,9 +196,9 @@ public partial class View : IDisposable, ISupportInitializeNotification
 
         BeginInitAdornments ();
 
-        if (_subviews?.Count > 0)
+        if (InternalSubViews?.Count > 0)
         {
-            foreach (View view in _subviews)
+            foreach (View view in InternalSubViews)
             {
                 if (!view.IsInitialized)
                 {
@@ -213,7 +213,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
 
     /// <summary>Signals the View that initialization is ending. See <see cref="ISupportInitialize"/>.</summary>
     /// <remarks>
-    ///     <para>Initializes all Subviews and Invokes the <see cref="Initialized"/> event.</para>
+    ///     <para>Initializes all SubViews and Invokes the <see cref="Initialized"/> event.</para>
     /// </remarks>
     public virtual void EndInit ()
     {
@@ -232,14 +232,11 @@ public partial class View : IDisposable, ISupportInitializeNotification
         UpdateTextDirection (TextDirection);
         UpdateTextFormatterText ();
 
-        if (_subviews is { })
+        foreach (View view in InternalSubViews)
         {
-            foreach (View view in _subviews)
+            if (!view.IsInitialized)
             {
-                if (!view.IsInitialized)
-                {
-                    view.EndInit ();
-                }
+                view.EndInit ();
             }
         }
 
@@ -295,12 +292,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
                 Border.Enabled = _enabled;
             }
 
-            if (_subviews is null)
-            {
-                return;
-            }
-
-            foreach (View view in _subviews)
+            foreach (View view in InternalSubViews)
             {
                 view.Enabled = Enabled;
             }

+ 1 - 1
Terminal.Gui/View/ViewArrangement.cs

@@ -62,7 +62,7 @@ public enum ViewArrangement
     Resizable = LeftResizable | RightResizable | TopResizable | BottomResizable,
 
     /// <summary>
-    ///     The view overlaps other views (the order of <see cref="View.Subviews"/> dicates the Z-order). If this flag is not
+    ///     The view overlaps other views (the order of <see cref="View.SubViews"/> dicates the Z-order). If this flag is not
     ///     set the view will operate in tiled mode.
     ///     <para>
     ///         When set, Tab and Shift-Tab will be constrained to the subviews of the view (normally, they will navigate to

+ 3 - 3
Terminal.Gui/View/ViewportSettings.cs

@@ -142,9 +142,9 @@ public enum ViewportSettings
 
     /// <summary>
     ///     If set the View will be transparent: The <see cref="View.Viewport"/> will not be cleared when the View is drawn and the clip region
-    ///     will be set to clip the View's <see cref="View.Text"/> and <see cref="View.Subviews"/>.
+    ///     will be set to clip the View's <see cref="View.Text"/> and <see cref="View.SubViews"/>.
     ///     <para>
-    ///         Only the topmost View in a Subview Hierarchy can be transparent. Any subviews of the topmost transparent view
+    ///         Only the topmost View in a SubView Hierarchy can be transparent. Any subviews of the topmost transparent view
     ///         will have indeterminate draw behavior.
     ///     </para>
     ///     <para>
@@ -154,7 +154,7 @@ public enum ViewportSettings
     Transparent = 0b_0001_0000_0000,
 
     /// <summary>
-    ///     If set the View will be transparent to mouse events: Any mouse event that occurs over the View (and it's Subviews) will be passed to the
+    ///     If set the View will be transparent to mouse events: Any mouse event that occurs over the View (and it's SubViews) will be passed to the
     ///     Views below it.
     ///     <para>
     ///         Combine this with <see cref="Transparent"/> to get a view that is both visually transparent and transparent to the mouse.

+ 16 - 16
Terminal.Gui/Views/Bar.cs

@@ -144,12 +144,12 @@ public class Bar : View, IOrientation, IDesignable
     }
 
     // TODO: Move this to View
-    /// <summary>Inserts a <see cref="Shortcut"/> in the specified index of <see cref="View.Subviews"/>.</summary>
+    /// <summary>Inserts a <see cref="Shortcut"/> in the specified index of <see cref="View.SubViews"/>.</summary>
     /// <param name="index">The zero-based index at which item should be inserted.</param>
     /// <param name="item">The item to insert.</param>
     public void AddShortcutAt (int index, Shortcut item)
     {
-        List<View> savedSubViewList = Subviews.ToList ();
+        List<View> savedSubViewList = SubViews.ToList ();
         int count = savedSubViewList.Count;
         RemoveAll ();
 
@@ -172,18 +172,18 @@ public class Bar : View, IOrientation, IDesignable
 
     // TODO: Move this to View
 
-    /// <summary>Removes a <see cref="Shortcut"/> at specified index of <see cref="View.Subviews"/>.</summary>
+    /// <summary>Removes a <see cref="Shortcut"/> at specified index of <see cref="View.SubViews"/>.</summary>
     /// <param name="index">The zero-based index of the item to remove.</param>
     /// <returns>The <see cref="Shortcut"/> removed.</returns>
     public Shortcut? RemoveShortcut (int index)
     {
         View? toRemove = null;
 
-        for (var i = 0; i < Subviews.Count; i++)
+        for (var i = 0; i < SubViews.Count; i++)
         {
             if (i == index)
             {
-                toRemove = Subviews [i];
+                toRemove = SubViews.ElementAt (i);
             }
         }
 
@@ -198,7 +198,7 @@ public class Bar : View, IOrientation, IDesignable
     }
 
     /// <inheritdoc />
-    protected override void OnSubviewLayout (LayoutEventArgs args)
+    protected override void OnSubViewLayout (LayoutEventArgs args)
     {
         LayoutBarItems (args.OldContentSize);
     }
@@ -210,9 +210,9 @@ public class Bar : View, IOrientation, IDesignable
         switch (Orientation)
         {
             case Orientation.Horizontal:
-                for (var index = 0; index < Subviews.Count; index++)
+                for (var index = 0; index < SubViews.Count; index++)
                 {
-                    View barItem = Subviews [index];
+                    View barItem = SubViews.ElementAt (index);
 
                     barItem.ColorScheme = ColorScheme;
                     barItem.X = Pos.Align (Alignment.Start, AlignmentModes);
@@ -227,7 +227,7 @@ public class Bar : View, IOrientation, IDesignable
 
                     var minKeyWidth = 0;
 
-                    List<Shortcut> shortcuts = Subviews.Where (s => s is Shortcut && s.Visible).Cast<Shortcut> ().ToList ();
+                    List<Shortcut> shortcuts = SubViews.Where (s => s is Shortcut && s.Visible).Cast<Shortcut> ().ToList ();
 
                     foreach (Shortcut shortcut in shortcuts)
                     {
@@ -235,11 +235,11 @@ public class Bar : View, IOrientation, IDesignable
                         minKeyWidth = int.Max (minKeyWidth, shortcut.KeyView.Text.GetColumns ());
                     }
 
-                    var _maxBarItemWidth = 0;
+                    var maxBarItemWidth = 0;
 
-                    for (var index = 0; index < Subviews.Count; index++)
+                    for (var index = 0; index < SubViews.Count; index++)
                     {
-                        View barItem = Subviews [index];
+                        View barItem = SubViews.ElementAt (index);
 
 
                         barItem.ColorScheme = ColorScheme;
@@ -255,7 +255,7 @@ public class Bar : View, IOrientation, IDesignable
                             scBarItem.MinimumKeyTextSize = minKeyWidth;
                             scBarItem.Width = scBarItem.GetWidthDimAuto ();
                             barItem.Layout (Application.Screen.Size);
-                            _maxBarItemWidth = Math.Max (_maxBarItemWidth, barItem.Frame.Width);
+                            maxBarItemWidth = Math.Max (maxBarItemWidth, barItem.Frame.Width);
                         }
 
                         if (prevBarItem == null)
@@ -274,17 +274,17 @@ public class Bar : View, IOrientation, IDesignable
 
                     }
 
-                    foreach (var subView in Subviews)
+                    foreach (var subView in SubViews)
                     {
                         if (subView is not Line)
                         {
-                            subView.Width = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: _maxBarItemWidth);
+                            subView.Width = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: maxBarItemWidth);
                         }
                     }
                 }
                 else
                 {
-                    foreach (var subView in Subviews)
+                    foreach (var subView in SubViews)
                     {
                         if (subView is not Line)
                         {

+ 7 - 7
Terminal.Gui/Views/ComboBox.cs

@@ -57,14 +57,14 @@ public class ComboBox : View, IDesignable
         Initialized += (s, e) => ProcessLayout ();
 
         // On resize
-        SubviewsLaidOut += (sender, a) => ProcessLayout ();
+        SubViewsLaidOut += (sender, a) => ProcessLayout ();
 
-        Added += (s, e) =>
+        SuperViewChanged += (s, e) =>
                  {
                      // Determine if this view is hosted inside a dialog and is the only control
                      for (View view = SuperView; view != null; view = view.SuperView)
                      {
-                         if (view is Dialog && SuperView is { } && SuperView.Subviews.Count == 1 && SuperView.Subviews [0] == this)
+                         if (view is Dialog && SuperView is { } && SuperView.SubViews.Count == 1 && SuperView.SubViews.ElementAt (0) == this)
                          {
                              _autoHide = false;
 
@@ -199,7 +199,7 @@ public class ComboBox : View, IDesignable
             _source = value;
 
             // Only need to refresh list if its been added to a container view
-            if (SuperView is { } && SuperView.Subviews.Contains (this))
+            if (SuperView is { } && SuperView.SubViews.Contains (this))
             {
                 Text = string.Empty;
                 SetNeedsDraw ();
@@ -513,7 +513,7 @@ public class ComboBox : View, IDesignable
         Reset (true);
         _listview.ClearViewport ();
         _listview.TabStop = TabBehavior.NoStop;
-        SuperView?.MoveSubviewToStart (this);
+        SuperView?.MoveSubViewToStart (this);
 
         // BUGBUG: SetNeedsDraw takes Viewport relative coordinates, not Screen
         Rectangle rect = _listview.ViewportToScreen (_listview.IsInitialized ? _listview.Viewport : Rectangle.Empty);
@@ -658,7 +658,7 @@ public class ComboBox : View, IDesignable
         _listview.SetSource (_searchSet);
         _listview.Height = CalculateHeight ();
 
-        if (Subviews.Count > 0 && HasFocus)
+        if (SubViews.Count > 0 && HasFocus)
         {
             _search.SetFocus ();
         }
@@ -814,7 +814,7 @@ public class ComboBox : View, IDesignable
 
         _listview.ClearViewport ();
         _listview.Height = CalculateHeight ();
-        SuperView?.MoveSubviewToStart (this);
+        SuperView?.MoveSubViewToStart (this);
     }
 
     private bool UnixEmulation ()

+ 1 - 1
Terminal.Gui/Views/FileDialog.cs

@@ -505,7 +505,7 @@ public class FileDialog : Dialog, IDesignable
         {
             _btnCancel.X = Pos.Func (CalculateOkButtonPosX);
             _btnOk.X = Pos.Right (_btnCancel) + 1;
-            MoveSubviewTowardsStart (_btnCancel);
+            MoveSubViewTowardsStart (_btnCancel);
         }
 
         SetNeedsDraw ();

+ 1 - 1
Terminal.Gui/Views/GraphView/Annotations.cs

@@ -150,7 +150,7 @@ public class LegendAnnotation : View, IAnnotation
 
         if (BorderStyle != LineStyle.None)
         {
-            DrawBorderAndPadding ();
+            DrawAdornments ();
             RenderLineCanvas ();
         }
 

+ 2 - 2
Terminal.Gui/Views/HexView.cs

@@ -106,10 +106,10 @@ public class HexView : View, IDesignable
         MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
         MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
 
-        SubviewsLaidOut += HexViewSubviewsLaidOut;
+        SubViewsLaidOut += HexViewSubViewsLaidOut;
     }
 
-    private void HexViewSubviewsLaidOut (object? sender, LayoutEventArgs e)
+    private void HexViewSubViewsLaidOut (object? sender, LayoutEventArgs e)
     {
         SetBytesPerLine ();
         SetContentSize (new (GetLeftSideStartColumn () + BytesPerLine / NUM_BYTES_PER_HEX_COLUMN * HEX_COLUMN_WIDTH + BytesPerLine - 1, (int)((GetEditedSize ()) / BytesPerLine) + 1));

+ 6 - 6
Terminal.Gui/Views/Label.cs

@@ -1,10 +1,10 @@
 namespace Terminal.Gui;
 
 /// <summary>
-///     The Label <see cref="View"/> displays text that describes the View next in the <see cref="View.Subviews"/>. When
+///     The Label <see cref="View"/> displays text that describes the View next in the <see cref="View.SubViews"/>. When
 ///     Label
 ///     receives a <see cref="Command.HotKey"/> command it will pass it to the next <see cref="View"/> in
-///     <see cref="View.Subviews"/>.
+///     <see cref="View.SubViews"/>.
 /// </summary>
 /// <remarks>
 ///     <para>
@@ -13,7 +13,7 @@
 ///     <para>
 ///         If <see cref="View.CanFocus"/> is <see langword="false"/> and the use clicks on the Label,
 ///         the <see cref="Command.HotKey"/> will be invoked on the next <see cref="View"/> in
-///         <see cref="View.Subviews"/>.
+///         <see cref="View.SubViews"/>.
 ///     </para>
 /// </remarks>
 public class Label : View, IDesignable
@@ -75,12 +75,12 @@ public class Label : View, IDesignable
 
         if (HotKey.IsValid)
         {
-            int me = SuperView?.Subviews.IndexOf (this) ?? -1;
+            int me = SuperView?.SubViews.IndexOf (this) ?? -1;
 
-            if (me != -1 && me < SuperView?.Subviews.Count - 1)
+            if (me != -1 && me < SuperView?.SubViews.Count - 1)
             {
 
-                return SuperView?.Subviews [me + 1].InvokeCommand (Command.HotKey) == true;
+                return SuperView?.SubViews.ElementAt (me + 1).InvokeCommand (Command.HotKey) == true;
             }
         }
 

+ 30 - 12
Terminal.Gui/Views/Menu/ContextMenu.cs

@@ -104,6 +104,7 @@ public sealed class ContextMenu : IDisposable
         if (_menuBar is { })
         {
             _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
+            _container?.Remove (_menuBar);
         }
         Application.UngrabMouse ();
         _menuBar?.Dispose ();
@@ -177,16 +178,16 @@ public sealed class ContextMenu : IDisposable
         }
 
         MenuItems = menuItems;
-        _container = Application.Top;
+        _container = GetTopSuperView (Host);
         _container!.Closing += Container_Closing;
         _container.Deactivate += Container_Deactivate;
         _container.Disposing += Container_Disposing;
-        Rectangle frame = Application.Screen;
+        Rectangle viewport = _container.Viewport;
         Point position = Position;
 
         if (Host is { })
         {
-            Point pos = Host.ViewportToScreen (frame).Location;
+            Point pos = Host.Frame.Location;
             pos.Y += Host.Frame.Height > 0 ? Host.Frame.Height - 1 : 0;
 
             if (position != pos)
@@ -197,11 +198,11 @@ public sealed class ContextMenu : IDisposable
 
         Rectangle rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children);
 
-        if (rect.Right >= frame.Right)
+        if (rect.Right >= viewport.Right)
         {
-            if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero)
+            if (viewport.Right - rect.Width >= 0 || !ForceMinimumPosToZero)
             {
-                position.X = frame.Right - rect.Width;
+                position.X = viewport.Right - rect.Width;
             }
             else if (ForceMinimumPosToZero)
             {
@@ -213,17 +214,17 @@ public sealed class ContextMenu : IDisposable
             position.X = 0;
         }
 
-        if (rect.Bottom >= frame.Bottom)
+        if (rect.Bottom >= viewport.Bottom)
         {
-            if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero)
+            if (viewport.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero)
             {
                 if (Host is null)
                 {
-                    position.Y = frame.Bottom - rect.Height - 1;
+                    position.Y = viewport.Bottom - rect.Height - 1;
                 }
                 else
                 {
-                    Point pos = Host.ViewportToScreen (frame).Location;
+                    Point pos = Host.Frame.Location;
                     position.Y = pos.Y - rect.Height - 1;
                 }
             }
@@ -251,12 +252,29 @@ public sealed class ContextMenu : IDisposable
         _menuBar._isContextMenuLoading = true;
         _menuBar.MenuAllClosed += MenuBar_MenuAllClosed;
 
-        _menuBar.BeginInit ();
-        _menuBar.EndInit ();
+        _container.Add (_menuBar);
         IsShow = true;
         _menuBar.OpenMenu ();
     }
 
+    internal static Toplevel? GetTopSuperView (View? view)
+    {
+        if (view is Toplevel toplevel)
+        {
+            return toplevel;
+        }
+
+        for (View? sv = view?.SuperView; sv != null; sv = sv.SuperView)
+        {
+            if (sv is Toplevel top)
+            {
+                return top;
+            }
+        }
+
+        return (Toplevel?)view?.SuperView ?? Application.Top;
+    }
+
     private void Container_Closing (object? sender, ToplevelClosingEventArgs obj) { Hide (); }
     private void Container_Deactivate (object? sender, ToplevelEventArgs e) { Hide (); }
     private void Container_Disposing (object? sender, EventArgs e) { Dispose (); }

+ 11 - 1
Terminal.Gui/Views/Menu/Menu.cs

@@ -17,6 +17,7 @@ internal sealed class Menu : View
         }
 
         Application.MouseEvent += Application_RootMouseEvent;
+        Application.UnGrabbedMouse += Application_UnGrabbedMouse;
 
         // Things this view knows how to do
         AddCommand (Command.Up, () => MoveUp ());
@@ -235,6 +236,7 @@ internal sealed class Menu : View
         }
 
         Application.MouseEvent -= Application_RootMouseEvent;
+        Application.UnGrabbedMouse -= Application_UnGrabbedMouse;
         base.Dispose (disposing);
     }
 
@@ -522,6 +524,14 @@ internal sealed class Menu : View
         }
     }
 
+    private void Application_UnGrabbedMouse (object? sender, ViewEventArgs a)
+    {
+        if (_host.IsMenuOpen)
+        {
+            _host.CloseAllMenus ();
+        }
+    }
+
     private void CloseAllMenus ()
     {
         Application.UngrabMouse ();
@@ -821,7 +831,7 @@ internal sealed class Menu : View
             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.

+ 60 - 39
Terminal.Gui/Views/Menu/MenuBar.cs

@@ -73,7 +73,7 @@ public class MenuBar : View, IDesignable
         Y = 0;
         Width = Dim.Fill ();
         Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
-        Menus = new MenuBarItem [] { };
+        Menus = [];
 
         //CanFocus = true;
         _selected = -1;
@@ -84,7 +84,7 @@ public class MenuBar : View, IDesignable
         WantMousePositionReports = true;
         IsMenuOpen = false;
 
-        Added += MenuBar_Added;
+        SuperViewChanged += MenuBar_SuperViewChanged;
 
         // Things this view knows how to do
         AddCommand (
@@ -111,9 +111,14 @@ public class MenuBar : View, IDesignable
                     Command.Cancel,
                     () =>
                     {
-                        CloseMenuBar ();
+                        if (IsMenuOpen)
+                        {
+                            CloseMenuBar ();
 
-                        return true;
+                            return true;
+                        }
+
+                        return false;
                     }
                    );
 
@@ -436,7 +441,15 @@ public class MenuBar : View, IDesignable
             return;
         }
 
-        Application.GrabMouse (this);
+        if (_isContextMenuLoading)
+        {
+            Application.GrabMouse (_openMenu);
+            _isContextMenuLoading = false;
+        }
+        else
+        {
+            Application.GrabMouse (this);
+        }
     }
 
     /// <inheritdoc/>
@@ -488,6 +501,11 @@ public class MenuBar : View, IDesignable
 
     internal void CleanUp ()
     {
+        if (_isCleaning)
+        {
+            return;
+        }
+
         _isCleaning = true;
 
         if (_openMenu is { })
@@ -556,10 +574,10 @@ public class MenuBar : View, IDesignable
 
     private void CloseOtherOpenedMenuBar ()
     {
-        if (Application.Top is { })
+        if (SuperView is { })
         {
             // Close others menu bar opened
-            Menu? menu = Application.Top.Subviews.FirstOrDefault (v => v is Menu m && m.Host != this && m.Host.IsMenuOpen) as Menu;
+            Menu? menu = SuperView.SubViews.FirstOrDefault (v => v is Menu m && m.Host != this && m.Host.IsMenuOpen) as Menu;
             menu?.Host.CleanUp ();
         }
     }
@@ -595,7 +613,7 @@ public class MenuBar : View, IDesignable
             case false:
                 if (_openMenu is { })
                 {
-                    Application.Top?.Remove (_openMenu);
+                    SuperView?.Remove (_openMenu);
                 }
 
                 SetNeedsDraw ();
@@ -634,7 +652,7 @@ public class MenuBar : View, IDesignable
 
                     if (OpenCurrentMenu is { })
                     {
-                        Application.Top?.Remove (OpenCurrentMenu);
+                        SuperView?.Remove (OpenCurrentMenu);
                         if (Application.MouseGrabView == OpenCurrentMenu)
                         {
                             Application.UngrabMouse ();
@@ -725,6 +743,11 @@ public class MenuBar : View, IDesignable
                     return;
                 }
 
+                if (_selected == -1)
+                {
+                    return;
+                }
+
                 OpenMenu (_selected);
 
                 SelectEnabledItem (
@@ -822,7 +845,7 @@ public class MenuBar : View, IDesignable
 
                 if (_openMenu is { })
                 {
-                    Application.Top?.Remove (_openMenu);
+                    SuperView?.Remove (_openMenu);
                     if (Application.MouseGrabView == _openMenu)
                     {
                         Application.UngrabMouse ();
@@ -838,34 +861,23 @@ public class MenuBar : View, IDesignable
                     pos += Menus [i].TitleLength + (Menus [i].Help.GetColumns () > 0 ? Menus [i].Help.GetColumns () + 2 : 0) + _leftPadding + _rightPadding;
                 }
 
-                var locationOffset = Point.Empty;
 
-                // if SuperView is null then it's from a ContextMenu
-                if (SuperView is null)
-                {
-                    locationOffset = GetScreenOffset ();
-                }
 
-                if (SuperView is { } && SuperView != Application.Top)
-                {
-                    locationOffset.X += SuperView.Border.Thickness.Left;
-                    locationOffset.Y += SuperView.Border.Thickness.Top;
-                }
 
                 _openMenu = new ()
                 {
                     Host = this,
-                    X = Frame.X + pos + locationOffset.X,
-                    Y = Frame.Y + 1 + locationOffset.Y,
+                    X = Frame.X + pos,
+                    Y = Frame.Y + 1,
                     BarItems = Menus [index],
                     Parent = null
                 };
                 OpenCurrentMenu = _openMenu;
                 OpenCurrentMenu._previousSubFocused = _openMenu;
 
-                if (Application.Top is { })
+                if (SuperView is { })
                 {
-                    Application.Top.Add (_openMenu);
+                    SuperView.Add (_openMenu);
                    // _openMenu.SetRelativeLayout (Application.Screen.Size);
                 }
                 else
@@ -894,13 +906,11 @@ public class MenuBar : View, IDesignable
 
                     if (!UseSubMenusSingleFrame)
                     {
-                        locationOffset = GetLocationOffset ();
-
                         OpenCurrentMenu = new ()
                         {
                             Host = this,
-                            X = last!.Frame.Left + last.Frame.Width + locationOffset.X,
-                            Y = last.Frame.Top + locationOffset.Y + last._currentChild,
+                            X = last!.Frame.Left + last.Frame.Width,
+                            Y = last.Frame.Top + last._currentChild + 1,
                             BarItems = subMenu,
                             Parent = last
                         };
@@ -931,7 +941,7 @@ public class MenuBar : View, IDesignable
 
                     OpenCurrentMenu._previousSubFocused = last._previousSubFocused;
                     _openSubMenu.Add (OpenCurrentMenu);
-                    Application.Top?.Add (OpenCurrentMenu);
+                    SuperView?.Add (OpenCurrentMenu);
 
                     if (!OpenCurrentMenu.IsInitialized)
                     {
@@ -979,6 +989,11 @@ public class MenuBar : View, IDesignable
                     return;
                 }
 
+                if (_selected == -1)
+                {
+                    return;
+                }
+
                 OpenMenu (_selected);
 
                 if (!SelectEnabledItem (
@@ -1014,7 +1029,7 @@ public class MenuBar : View, IDesignable
         {
             foreach (Menu item in _openSubMenu)
             {
-                Application.Top!.Remove (item);
+                SuperView?.Remove (item);
                 if (Application.MouseGrabView == item)
                 {
                     Application.UngrabMouse ();
@@ -1157,10 +1172,10 @@ public class MenuBar : View, IDesignable
         return new (-2, 0);
     }
 
-    private void MenuBar_Added (object? sender, SuperViewChangedEventArgs e)
+    private void MenuBar_SuperViewChanged (object? sender, SuperViewChangedEventArgs _)
     {
         _initialCanFocus = CanFocus;
-        Added -= MenuBar_Added;
+        SuperViewChanged -= MenuBar_SuperViewChanged;
     }
 
     private void MoveLeft ()
@@ -1263,7 +1278,7 @@ public class MenuBar : View, IDesignable
             if (_openSubMenu is { })
             {
                 menu = _openSubMenu [i];
-                Application.Top!.Remove (menu);
+                SuperView!.Remove (menu);
                 _openSubMenu.Remove (menu);
 
                 if (Application.MouseGrabView == menu)
@@ -1456,9 +1471,9 @@ public class MenuBar : View, IDesignable
                             Activate (i);
                         }
                     }
-                    else if (me.Flags == MouseFlags.Button1Pressed
-                             || me.Flags == MouseFlags.Button1DoubleClicked
-                             || me.Flags == MouseFlags.Button1TripleClicked)
+                    else if (me.Flags.HasFlag (MouseFlags.Button1Pressed)
+                             || me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
+                             || me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
                     {
                         if (IsMenuOpen && !Menus [i].IsTopLevel)
                         {
@@ -1542,9 +1557,16 @@ public class MenuBar : View, IDesignable
                     }
                 }
 
+                if (Application.MouseGrabView != me.View)
+                {
+                    View v = me.View;
+                    Application.GrabMouse (v);
+
+                    return true;
+                }
+
                 if (me.View != current)
                 {
-                    Application.UngrabMouse ();
                     View v = me.View;
                     Application.GrabMouse (v);
                     MouseEventArgs nme;
@@ -1592,7 +1614,6 @@ public class MenuBar : View, IDesignable
             else
             {
                 _handled = false;
-                _isContextMenuLoading = false;
 
                 return false;
             }

+ 4 - 9
Terminal.Gui/Views/MenuBarv2.cs

@@ -22,7 +22,7 @@ public class MenuBarv2 : Bar
         ColorScheme = Colors.ColorSchemes ["Menu"];
         Orientation = Orientation.Horizontal;
 
-        SubviewLayout += MenuBarv2_LayoutStarted;
+        SubViewLayout += MenuBarv2_LayoutStarted;
     }
 
     // MenuBarv2 arranges the items horizontally.
@@ -34,14 +34,11 @@ public class MenuBarv2 : Bar
     }
 
     /// <inheritdoc/>
-    public override View Add (View view)
+    protected override void OnSubViewAdded (View subView)
     {
-        // Call base first, because otherwise it resets CanFocus to true
-        base.Add (view);
+        subView.CanFocus = false;
 
-        view.CanFocus = true;
-
-        if (view is Shortcut shortcut)
+        if (subView is Shortcut shortcut)
         {
             // TODO: not happy about using AlignmentModes for this. Too implied.
             // TODO: instead, add a property (a style enum?) to Shortcut to control this
@@ -50,7 +47,5 @@ public class MenuBarv2 : Bar
             shortcut.KeyView.Visible = false;
             shortcut.HelpView.Visible = false;
         }
-
-        return view;
     }
 }

+ 7 - 10
Terminal.Gui/Views/Menuv2.cs

@@ -47,11 +47,11 @@ public class Menuv2 : Bar
     // The first item has no left border, the last item has no right border.
     // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart).
     /// <inheritdoc />
-    protected override void OnSubviewLayout (LayoutEventArgs args)
+    protected override void OnSubViewLayout (LayoutEventArgs args)
     {
-        for (int index = 0; index < Subviews.Count; index++)
+        for (int index = 0; index < SubViews.Count; index++)
         {
-            View barItem = Subviews [index];
+            View barItem = SubViews.ElementAt (index);
 
             if (!barItem.Visible)
             {
@@ -59,15 +59,14 @@ public class Menuv2 : Bar
             }
 
         }
-        base.OnSubviewLayout (args);
+        base.OnSubViewLayout (args);
     }
 
     /// <inheritdoc/>
-    public override View Add (View view)
+    /// 
+    protected override void OnSubViewAdded (View subView)
     {
-        base.Add (view);
-
-        if (view is Shortcut shortcut)
+        if (subView is Shortcut shortcut)
         {
             shortcut.CanFocus = true;
             shortcut.Orientation = Orientation.Vertical;
@@ -95,7 +94,5 @@ public class Menuv2 : Bar
                 //}
             }
         }
-
-        return view;
     }
 }

+ 4 - 0
Terminal.Gui/Views/MessageBox.cs

@@ -374,6 +374,10 @@ public static class MessageBox
                                        {
                                            Clicked = (int)btn.Data!;
                                        }
+                                       else
+                                       {
+                                           Clicked = defaultButton;
+                                       }
 
                                        e.Cancel = true;
                                        Application.RequestStop ();

+ 1 - 1
Terminal.Gui/Views/RadioGroup.cs

@@ -42,7 +42,7 @@ public class RadioGroup : View, IDesignable, IOrientation
         // By default, single click is already bound to Command.Select
         MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);
 
-        SubviewLayout += RadioGroup_LayoutStarted;
+        SubViewLayout += RadioGroup_LayoutStarted;
 
         HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
     }

+ 2 - 2
Terminal.Gui/Views/ScrollBar/ScrollBar.cs

@@ -113,7 +113,7 @@ public class ScrollBar : View, IOrientation, IDesignable
         _slider.Position = _sliderPosition.Value;
     }
 
-    private void PositionSubviews ()
+    private void PositionSubViews ()
     {
         if (Orientation == Orientation.Vertical)
         {
@@ -180,7 +180,7 @@ public class ScrollBar : View, IOrientation, IDesignable
         TextAlignment = Alignment.Center;
         VerticalTextAlignment = Alignment.Center;
         _slider.Orientation = newOrientation;
-        PositionSubviews ();
+        PositionSubViews ();
 
         OrientationChanged?.Invoke (this, new (newOrientation));
     }

+ 5 - 5
Terminal.Gui/Views/Shortcut.cs

@@ -132,7 +132,7 @@ public class Shortcut : View, IOrientation, IDesignable
 
         Action = action;
 
-        SubviewLayout += OnLayoutStarted;
+        SubViewLayout += OnLayoutStarted;
 
         ShowHide ();
     }
@@ -221,7 +221,7 @@ public class Shortcut : View, IOrientation, IDesignable
         HelpView.SetRelativeLayout (Application.Screen.Size);
         KeyView.SetRelativeLayout (Application.Screen.Size);
 
-        _minimumNaturalWidth = PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width);
+        _minimumNaturalWidth = PosAlign.CalculateMinDimension (0, SubViews, Dimension.Width);
 
         // Reset our relative layout
         SetRelativeLayout (SuperView?.GetContentSize () ?? Application.Screen.Size);
@@ -824,17 +824,17 @@ public class Shortcut : View, IOrientation, IDesignable
         {
             TitleChanged -= Shortcut_TitleChanged;
 
-            if (CommandView?.IsAdded == false)
+            if (CommandView.SuperView is null)
             {
                 CommandView.Dispose ();
             }
 
-            if (HelpView?.IsAdded == false)
+            if (HelpView.SuperView is null)
             {
                 HelpView.Dispose ();
             }
 
-            if (KeyView?.IsAdded == false)
+            if (KeyView.SuperView is null)
             {
                 KeyView.Dispose ();
             }

+ 1 - 1
Terminal.Gui/Views/Slider.cs

@@ -59,7 +59,7 @@ public class Slider<T> : View, IOrientation
         // BUGBUG: This should not be needed - Need to ensure SetRelativeLayout gets called during EndInit
         Initialized += (s, e) => { SetContentSize (); };
 
-        SubviewLayout += (s, e) => { SetContentSize (); };
+        SubViewLayout += (s, e) => { SetContentSize (); };
     }
 
     // TODO: Make configurable via ConfigurationManager

+ 19 - 31
Terminal.Gui/Views/SpinnerView/SpinnerView.cs

@@ -1,16 +1,15 @@
-//------------------------------------------------------------------------------
+#nullable enable
+//------------------------------------------------------------------------------
 // Windows Terminal supports Unicode and Emoji characters, but by default
 // conhost shells (e.g., PowerShell and cmd.exe) do not. See
 // <https://spectreconsole.net/best-practices>.
 //------------------------------------------------------------------------------
 
-using System.Diagnostics;
-
 namespace Terminal.Gui;
 
 /// <summary>A <see cref="View"/> which displays (by default) a spinning line character.</summary>
 /// <remarks>
-///     By default animation only occurs when you call <see cref="SpinnerView.AdvanceAnimation(bool)"/>. Use
+///     By default, animation only occurs when you call <see cref="SpinnerView.AdvanceAnimation(bool)"/>. Use
 ///     <see cref="AutoSpin"/> to make the automate calls to <see cref="SpinnerView.AdvanceAnimation(bool)"/>.
 /// </remarks>
 public class SpinnerView : View, IDesignable
@@ -25,7 +24,7 @@ public class SpinnerView : View, IDesignable
     private DateTime _lastRender = DateTime.MinValue;
     private string [] _sequence = DEFAULT_STYLE.Sequence;
     private SpinnerStyle _style = DEFAULT_STYLE;
-    private object _timeout;
+    private object? _timeout;
 
     /// <summary>Creates a new instance of the <see cref="SpinnerView"/> class.</summary>
     public SpinnerView ()
@@ -134,14 +133,7 @@ public class SpinnerView : View, IDesignable
                 {
                     if (SpinBounce)
                     {
-                        if (SpinReverse)
-                        {
-                            _bounceReverse = false;
-                        }
-                        else
-                        {
-                            _bounceReverse = true;
-                        }
+                        _bounceReverse = !SpinReverse;
 
                         _currentIdx = Sequence.Length - 1;
                     }
@@ -155,14 +147,7 @@ public class SpinnerView : View, IDesignable
                 {
                     if (SpinBounce)
                     {
-                        if (SpinReverse)
-                        {
-                            _bounceReverse = true;
-                        }
-                        else
-                        {
-                            _bounceReverse = false;
-                        }
+                        _bounceReverse = SpinReverse;
 
                         _currentIdx = 1;
                     }
@@ -182,25 +167,26 @@ public class SpinnerView : View, IDesignable
         }
     }
 
-    /// <inheritdoc />
+    /// <inheritdoc/>
     protected override bool OnClearingViewport () { return true; }
 
-    /// <inheritdoc />
+    /// <inheritdoc/>
     protected override bool OnDrawingContent ()
     {
         Render ();
+
         return true;
     }
 
     /// <summary>
-    ///    Renders the current frame of the spinner.
+    ///     Renders the current frame of the spinner.
     /// </summary>
     public void Render ()
     {
         if (Sequence is { Length: > 0 } && _currentIdx < Sequence.Length)
         {
             Move (Viewport.X, Viewport.Y);
-            View.Driver?.AddStr (Sequence [_currentIdx]);
+            Driver?.AddStr (Sequence [_currentIdx]);
         }
     }
 
@@ -214,7 +200,8 @@ public class SpinnerView : View, IDesignable
 
     private void AddAutoSpinTimeout ()
     {
-        if (_timeout is { })
+        // Only add timeout if we are initialized and not already spinning
+        if (_timeout is { } || !Application.Initialized)
         {
             return;
         }
@@ -223,7 +210,7 @@ public class SpinnerView : View, IDesignable
                                            TimeSpan.FromMilliseconds (SpinDelay),
                                            () =>
                                            {
-                                               Application.Invoke (() => AdvanceAnimation());
+                                               Application.Invoke (() => AdvanceAnimation ());
 
                                                return true;
                                            }
@@ -237,7 +224,7 @@ public class SpinnerView : View, IDesignable
             return false;
         }
 
-        if (_sequence is { } && _sequence.Length > 0)
+        if (_sequence is { Length: > 0 })
         {
             foreach (string frame in _sequence)
             {
@@ -260,7 +247,7 @@ public class SpinnerView : View, IDesignable
     {
         var max = 0;
 
-        if (_sequence is { } && _sequence.Length > 0)
+        if (_sequence is { Length: > 0 })
         {
             foreach (string frame in _sequence)
             {
@@ -295,7 +282,7 @@ public class SpinnerView : View, IDesignable
 
     private void SetSequence (string [] frames)
     {
-        if (frames is { } && frames.Length > 0)
+        if (frames is { Length: > 0 })
         {
             _style = new SpinnerStyle.Custom ();
             _sequence = frames;
@@ -303,7 +290,7 @@ public class SpinnerView : View, IDesignable
         }
     }
 
-    private void SetStyle (SpinnerStyle style)
+    private void SetStyle (SpinnerStyle? style)
     {
         if (style is { })
         {
@@ -320,6 +307,7 @@ public class SpinnerView : View, IDesignable
         Style = new SpinnerStyle.Points ();
         SpinReverse = true;
         AutoSpin = true;
+
         return true;
     }
 }

+ 7 - 12
Terminal.Gui/Views/StatusBar.cs

@@ -26,7 +26,7 @@ public class StatusBar : Bar, IDesignable
         BorderStyle = LineStyle.Dashed;
         ColorScheme = Colors.ColorSchemes ["Menu"];
 
-        SubviewLayout += StatusBar_LayoutStarted;
+        SubViewLayout += StatusBar_LayoutStarted;
     }
 
     // StatusBar arranges the items horizontally.
@@ -34,13 +34,13 @@ public class StatusBar : Bar, IDesignable
     // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart).
     private void StatusBar_LayoutStarted (object sender, LayoutEventArgs e)
     {
-        for (int index = 0; index < Subviews.Count; index++)
+        for (int index = 0; index < SubViews.Count; index++)
         {
-            View barItem = Subviews [index];
+            View barItem = SubViews.ElementAt (index);
 
             barItem.BorderStyle = BorderStyle;
 
-            if (index == Subviews.Count - 1)
+            if (index == SubViews.Count - 1)
             {
                 barItem.Border.Thickness = new Thickness (0, 0, 0, 0);
             }
@@ -57,21 +57,16 @@ public class StatusBar : Bar, IDesignable
     }
 
     /// <inheritdoc/>
-    public override View Add (View view)
+    protected override void OnSubViewAdded (View subView)
     {
-        // Call base first, because otherwise it resets CanFocus to true
-        base.Add (view);
+        subView.CanFocus = false;
 
-        view.CanFocus = false;
-
-        if (view is Shortcut shortcut)
+        if (subView is Shortcut shortcut)
         {
             // TODO: not happy about using AlignmentModes for this. Too implied.
             // TODO: instead, add a property (a style enum?) to Shortcut to control this
             shortcut.AlignmentModes = AlignmentModes.EndToStart;
         }
-
-        return view;
     }
 
     /// <inheritdoc />

+ 4 - 4
Terminal.Gui/Views/TabView/TabRow.cs

@@ -115,7 +115,7 @@ internal class TabRow : View
     }
 
     /// <inheritdoc/>
-    protected override void OnSubviewLayout (LayoutEventArgs args)
+    protected override void OnSubViewLayout (LayoutEventArgs args)
     {
         _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
 
@@ -123,7 +123,7 @@ internal class TabRow : View
 
         RenderUnderline ();
 
-        base.OnSubviewLayout (args);
+        base.OnSubViewLayout (args);
     }
 
     /// <inheritdoc />
@@ -765,7 +765,7 @@ internal class TabRow : View
             _leftScrollIndicator.Visible = true;
 
             // Ensures this is clicked instead of the first tab
-            MoveSubviewToEnd (_leftScrollIndicator);
+            MoveSubViewToEnd (_leftScrollIndicator);
         }
         else
         {
@@ -782,7 +782,7 @@ internal class TabRow : View
             _rightScrollIndicator.Visible = true;
 
             // Ensures this is clicked instead of the last tab if under this
-            MoveSubviewToStart (_rightScrollIndicator);
+            MoveSubViewToStart (_rightScrollIndicator);
         }
         else
         {

+ 91 - 3
Terminal.Gui/Views/TabView/TabView.cs

@@ -84,6 +84,92 @@ public class TabView : View
                     }
                    );
 
+        AddCommand (
+                    Command.Up,
+                    () =>
+                    {
+                        if (_style.TabsOnBottom)
+                        {
+                            if (_tabsBar is { HasFocus: true } && _containerView.CanFocus)
+                            {
+                                _containerView.SetFocus ();
+
+                                return true;
+                            }
+                        }
+                        else
+                        {
+                            if (_containerView is { HasFocus: true })
+                            {
+                                var mostFocused = _containerView.MostFocused;
+
+                                if (mostFocused is { })
+                                {
+                                    for (int? i = mostFocused.SuperView?.SubViews.IndexOf (mostFocused) - 1; i > -1; i--)
+                                    {
+                                        var view = mostFocused.SuperView?.SubViews.ElementAt ((int)i);
+
+                                        if (view is { CanFocus: true, Enabled: true, Visible: true })
+                                        {
+                                            // Let toplevel handle it
+                                            return false;
+                                        }
+                                    }
+                                }
+
+                                SelectedTab?.SetFocus ();
+
+                                return true;
+                            }
+                        }
+
+                        return false;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Down,
+                    () =>
+                    {
+                        if (_style.TabsOnBottom)
+                        {
+                            if (_containerView is { HasFocus: true })
+                            {
+                                var mostFocused = _containerView.MostFocused;
+
+                                if (mostFocused is { })
+                                {
+                                    for (int? i = mostFocused.SuperView?.SubViews.IndexOf (mostFocused) + 1; i < mostFocused.SuperView?.SubViews.Count; i++)
+                                    {
+                                        var view = mostFocused.SuperView?.SubViews.ElementAt ((int)i);
+
+                                        if (view is { CanFocus: true, Enabled: true, Visible: true })
+                                        {
+                                            // Let toplevel handle it
+                                            return false;
+                                        }
+                                    }
+                                }
+
+                                SelectedTab?.SetFocus ();
+
+                                return true;
+                            }
+                        }
+                        else
+                        {
+                            if (_tabsBar is { HasFocus: true } && _containerView.CanFocus)
+                            {
+                                _containerView.SetFocus ();
+
+                                return true;
+                            }
+                        }
+
+                        return false;
+                    }
+                   );
+
         // Default keybindings for this view
         KeyBindings.Add (Key.CursorLeft, Command.Left);
         KeyBindings.Add (Key.CursorRight, Command.Right);
@@ -91,6 +177,8 @@ public class TabView : View
         KeyBindings.Add (Key.End, Command.RightEnd);
         KeyBindings.Add (Key.PageDown, Command.PageDown);
         KeyBindings.Add (Key.PageUp, Command.PageUp);
+        KeyBindings.Add (Key.CursorUp, Command.Up);
+        KeyBindings.Add (Key.CursorDown, Command.Down);
     }
 
     /// <summary>
@@ -155,12 +243,12 @@ public class TabView : View
 
     private bool TabCanSetFocus ()
     {
-        return IsInitialized && SelectedTab is { } && (_selectedTabHasFocus || !_containerView.CanFocus);
+        return IsInitialized && SelectedTab is { } && (HasFocus || (bool)_containerView?.HasFocus) && (_selectedTabHasFocus || !_containerView.CanFocus);
     }
 
     private void ContainerViewCanFocus (object sender, EventArgs eventArgs)
     {
-        _containerView.CanFocus = _containerView.Subviews.Count (v => v.CanFocus) > 0;
+        _containerView.CanFocus = _containerView.SubViews.Count (v => v.CanFocus) > 0;
     }
 
     private TabStyle _style = new ();
@@ -518,7 +606,7 @@ public class TabView : View
         {
             SelectedTab?.SetFocus ();
         }
-        else
+        else if (HasFocus)
         {
             SelectedTab?.View?.SetFocus ();
         }

+ 13 - 10
Terminal.Gui/Views/TextField.cs

@@ -45,9 +45,7 @@ public class TextField : View
 
         Initialized += TextField_Initialized;
 
-        Added += TextField_Added;
-
-        Removed += TextField_Removed;
+        SuperViewChanged += TextField_SuperViewChanged;
 
         // Things this view knows how to do
         AddCommand (
@@ -1182,7 +1180,7 @@ public class TextField : View
 
     private void Adjust ()
     {
-        if (!IsAdded)
+        if (SuperView is null)
         {
             return;
         }
@@ -1820,17 +1818,22 @@ public class TextField : View
         ContextMenu.Show (BuildContextMenuBarItem ());
     }
 
-    private void TextField_Added (object sender, SuperViewChangedEventArgs e)
+    private void TextField_SuperViewChanged (object sender, SuperViewChangedEventArgs e)
     {
-        if (Autocomplete.HostControl is null)
+        if (e.SuperView is {})
         {
-            Autocomplete.HostControl = this;
-            Autocomplete.PopupInsideContainer = false;
+            if (Autocomplete.HostControl is null)
+            {
+                Autocomplete.HostControl = this;
+                Autocomplete.PopupInsideContainer = false;
+            }
+        }
+        else
+        {
+            Autocomplete.HostControl = null;
         }
     }
 
-    private void TextField_Removed (object sender, SuperViewChangedEventArgs e) { Autocomplete.HostControl = null; }
-
     private void TextField_Initialized (object sender, EventArgs e)
     {
         _cursorPosition = Text.GetRuneCount ();

+ 12 - 7
Terminal.Gui/Views/TextView.cs

@@ -1899,9 +1899,9 @@ public class TextView : View
 
         Initialized += TextView_Initialized!;
 
-        Added += TextView_Added!;
+        SuperViewChanged += TextView_SuperViewChanged!;
 
-        SubviewsLaidOut += TextView_LayoutComplete;
+        SubViewsLaidOut += TextView_LayoutComplete;
 
         // Things this view knows how to do
 
@@ -2416,8 +2416,6 @@ public class TextView : View
         KeyBindings.Add (ContextMenu.Key, Command.Context);
     }
 
-    private void TextView_Added1 (object? sender, SuperViewChangedEventArgs e) { throw new NotImplementedException (); }
-
     // BUGBUG: AllowsReturn is mis-named. It should be EnterKeyAccepts.
     /// <summary>
     ///     Gets or sets whether pressing ENTER in a <see cref="TextView"/> creates a new line of text
@@ -6444,11 +6442,18 @@ public class TextView : View
         return StringExtensions.ToString (encoded);
     }
 
-    private void TextView_Added (object sender, SuperViewChangedEventArgs e)
+    private void TextView_SuperViewChanged (object sender, SuperViewChangedEventArgs e)
     {
-        if (Autocomplete.HostControl is null)
+        if (e.SuperView is {})
         {
-            Autocomplete.HostControl = this;
+            if (Autocomplete.HostControl is null)
+            {
+                Autocomplete.HostControl = this;
+            }
+        }
+        else
+        {
+            Autocomplete.HostControl = null;
         }
     }
 

+ 7 - 7
Terminal.Gui/Views/TileView.cs

@@ -23,7 +23,7 @@ public class TileView : View
         CanFocus = true;
         RebuildForTileCount (tiles);
 
-        SubviewLayout += (_, _) =>
+        SubViewLayout += (_, _) =>
                          {
                              Rectangle viewport = Viewport;
 
@@ -100,14 +100,14 @@ public class TileView : View
                 return i;
             }
 
-            if (v.Subviews.Contains (toFind))
+            if (v.SubViews.Contains (toFind))
             {
                 return i;
             }
 
             if (recursive)
             {
-                if (RecursiveContains (v.Subviews, toFind))
+                if (RecursiveContains (v.SubViews, toFind))
                 {
                     return i;
                 }
@@ -173,7 +173,7 @@ public class TileView : View
 
     /// <summary>Overridden so no Frames get drawn</summary>
     /// <returns></returns>
-    protected override bool OnDrawingBorderAndPadding () { return true; }
+    protected override bool OnDrawingAdornments () { return true; }
 
     /// <inheritdoc/>
     protected override bool OnRenderingLineCanvas () { return false; }
@@ -485,7 +485,7 @@ public class TileView : View
         };
 
         // Take everything out of the View we are moving
-        View [] childViews = toMove!.Subviews.ToArray ();
+        View [] childViews = toMove!.SubViews.ToArray ();
         toMove.RemoveAll ();
 
         // Remove the view itself and replace it with the new TileView
@@ -533,7 +533,7 @@ public class TileView : View
     {
         List<TileViewLineView> lines = new ();
 
-        foreach (View sub in v.Subviews)
+        foreach (View sub in v.SubViews)
         {
             if (sub is TileViewLineView s)
             {
@@ -756,7 +756,7 @@ public class TileView : View
                 return true;
             }
 
-            if (RecursiveContains (v.Subviews, needle))
+            if (RecursiveContains (v.SubViews, needle))
             {
                 return true;
             }

+ 14 - 14
Terminal.Gui/Views/Toplevel.cs

@@ -33,7 +33,7 @@ public partial class Toplevel : View
         Arrangement = ViewArrangement.Overlapped;
         Width = Dim.Fill ();
         Height = Dim.Fill ();
-        ColorScheme = Colors.ColorSchemes ["TopLevel"];
+        base.ColorScheme = Colors.ColorSchemes ["TopLevel"];
         MouseClick += Toplevel_MouseClick;
     }
 
@@ -66,15 +66,15 @@ public partial class Toplevel : View
 
     #endregion
 
-    #region Subviews
+    #region SubViews
 
     // TODO: Deprecate - Any view can host a menubar in v2
     /// <summary>Gets the latest <see cref="MenuBar"/> added into this Toplevel.</summary>
-    public MenuBar? MenuBar => (MenuBar?)Subviews?.LastOrDefault (s => s is MenuBar);
+    public MenuBar? MenuBar => (MenuBar?)SubViews?.LastOrDefault (s => s is MenuBar);
 
     //// TODO: Deprecate - Any view can host a statusbar in v2
     ///// <summary>Gets the latest <see cref="StatusBar"/> added into this Toplevel.</summary>
-    //public StatusBar? StatusBar => (StatusBar?)Subviews?.LastOrDefault (s => s is StatusBar);
+    //public StatusBar? StatusBar => (StatusBar?)SubViews?.LastOrDefault (s => s is StatusBar);
 
     #endregion
 
@@ -127,7 +127,7 @@ public partial class Toplevel : View
     {
         IsLoaded = true;
 
-        foreach (var view in Subviews.Where (v => v is Toplevel))
+        foreach (var view in SubViews.Where (v => v is Toplevel))
         {
             var tl = (Toplevel)view;
             tl.OnLoaded ();
@@ -180,7 +180,7 @@ public partial class Toplevel : View
     /// </summary>
     internal virtual void OnReady ()
     {
-        foreach (var view in Subviews.Where (v => v is Toplevel))
+        foreach (var view in SubViews.Where (v => v is Toplevel))
         {
             var tl = (Toplevel)view;
             tl.OnReady ();
@@ -192,7 +192,7 @@ public partial class Toplevel : View
     /// <summary>Called from <see cref="Application.End(RunState)"/> before the <see cref="Toplevel"/> is disposed.</summary>
     internal virtual void OnUnloaded ()
     {
-        foreach (var view in Subviews.Where (v => v is Toplevel))
+        foreach (var view in SubViews.Where (v => v is Toplevel))
         {
             var tl = (Toplevel)view;
             tl.OnUnloaded ();
@@ -202,7 +202,7 @@ public partial class Toplevel : View
     }
 
     #endregion
-    
+
     #region Size / Position Management
 
     // TODO: Make cancelable?
@@ -235,7 +235,7 @@ public partial class Toplevel : View
             return;
         }
 
-        //var layoutSubviews = false;
+        //var layoutSubViews = false;
         var maxWidth = 0;
 
         if (superView.Margin is { } && superView == top.SuperView)
@@ -251,25 +251,25 @@ public partial class Toplevel : View
             if (top?.X is null or PosAbsolute && top?.Frame.X != nx)
             {
                 top!.X = nx;
-                //layoutSubviews = true;
+                //layoutSubViews = true;
             }
 
             if (top?.Y is null or PosAbsolute && top?.Frame.Y != ny)
             {
                 top!.Y = ny;
-                //layoutSubviews = true;
+                //layoutSubViews = true;
             }
         }
 
 
-        //if (superView.IsLayoutNeeded () || layoutSubviews)
+        //if (superView.IsLayoutNeeded () || layoutSubViews)
         //{
-        //    superView.LayoutSubviews ();
+        //    superView.LayoutSubViews ();
         //}
 
         //if (IsLayoutNeeded ())
         //{
-        //    LayoutSubviews ();
+        //    LayoutSubViews ();
         //}
     }
 

+ 3 - 3
Terminal.Gui/Views/Wizard/Wizard.cs

@@ -357,7 +357,7 @@ public class Wizard : Dialog
         UpdateButtonsAndTitle ();
 
         // Set focus on the contentview
-        newStep?.Subviews.ToArray () [0].SetFocus ();
+        newStep?.SubViews.ToArray () [0].SetFocus ();
 
         if (OnStepChanged (oldStep, _currentStep))
         {
@@ -501,7 +501,7 @@ public class Wizard : Dialog
             step.Height = Dim.Fill (
                                     Dim.Func (
                                               () => IsInitialized
-                                                        ? Subviews.First (view => view.Y.Has<PosAnchorEnd> (out _)).Frame.Height + 1
+                                                        ? SubViews.First (view => view.Y.Has<PosAnchorEnd> (out _)).Frame.Height + 1
                                                         : 1)); // for button frame (+1 for lineView)
             step.Width = Dim.Fill ();
         }
@@ -513,7 +513,7 @@ public class Wizard : Dialog
             step.Height = Dim.Fill (
                                     Dim.Func (
                                               () => IsInitialized
-                                                        ? Subviews.First (view => view.Y.Has<PosAnchorEnd> (out _)).Frame.Height + 1
+                                                        ? SubViews.First (view => view.Y.Has<PosAnchorEnd> (out _)).Frame.Height + 1
                                                         : 2)); // for button frame (+1 for lineView)
             step.Width = Dim.Fill ();
         }

+ 4 - 4
Terminal.Gui/Views/Wizard/WizardStep.cs

@@ -88,7 +88,7 @@ public class WizardStep : View
         //		scrollBar.OtherScrollBarView.Size = helpTextView.Maxlength;
         //		scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn;
         //	}
-        //	scrollBar.LayoutSubviews ();
+        //	scrollBar.LayoutSubViews ();
         //	scrollBar.Refresh ();
         //};
         //base.Add (scrollBar);
@@ -151,7 +151,7 @@ public class WizardStep : View
             container?.Remove (view);
         }
 
-        if (_contentView.InternalSubviews.Count < 1)
+        if (_contentView.InternalSubViews.Count < 1)
         {
             CanFocus = false;
         }
@@ -176,7 +176,7 @@ public class WizardStep : View
         _helpTextView.Height = Dim.Height(_contentView);
         _helpTextView.Width = Dim.Fill ();
 
-        if (_contentView.InternalSubviews?.Count > 0)
+        if (_contentView.InternalSubViews?.Count > 0)
         {
             if (_helpTextView.Text.Length > 0)
             {
@@ -199,7 +199,7 @@ public class WizardStep : View
             // Error - no pane shown
         }
 
-        _contentView.Visible = _contentView.InternalSubviews?.Count > 0;
+        _contentView.Visible = _contentView.InternalSubViews?.Count > 0;
         _helpTextView.Visible = _helpTextView.Text.Length > 0;
     }
 } // end of WizardStep class

+ 7 - 0
Terminal.sln

@@ -1,3 +1,4 @@
+
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 17
 VisualStudioVersion = 17.2.32427.441
@@ -62,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StressTests", "Tests\Stress
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", "Tests\UnitTestsParallelizable\UnitTests.Parallelizable.csproj", "{DE780834-190A-8277-51FD-750CC666E82D}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -116,6 +119,10 @@ Global
 		{DE780834-190A-8277-51FD-750CC666E82D}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{DE780834-190A-8277-51FD-750CC666E82D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 2 - 0
Terminal.sln.DotSettings

@@ -410,8 +410,10 @@
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
 	<s:Int64 x:Key="/Default/Environment/UnitTesting/ParallelProcessesCount/@EntryValue">5</s:Int64>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Gonek/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Justifier/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=langword/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=ogonek/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Roslynator/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevel/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevels/@EntryIndexedValue">True</s:Boolean>

+ 89 - 0
TerminalGuiFluentTesting/ClassDiagram1.cd

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ClassDiagram MajorVersion="1" MinorVersion="1">
+  <Class Name="TerminalGuiFluentTesting.With" Collapsed="true">
+    <Position X="0.5" Y="1.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAIAAAAAA=</HashCode>
+      <FileName>With.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.FakeInput&lt;T&gt;" Collapsed="true">
+    <Position X="7" Y="1.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AQAAAAAAACAAAQEAAAAgAAAAAAAAAAAAAAAAAAAAAAI=</HashCode>
+      <FileName>FakeInput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.FakeNetInput" Collapsed="true">
+    <Position X="8.25" Y="2.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>FakeNetInput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.FakeWindowsInput" Collapsed="true">
+    <Position X="6" Y="2.75" Width="1.75" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>FakeWindowsInput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.FakeOutput" Collapsed="true" BaseTypeListCollapsed="true">
+    <Position X="5.5" Y="0.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAgCAAgAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode>
+      <FileName>FakeOutput.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.GuiTestContext" BaseTypeListCollapsed="true">
+    <Position X="2.25" Y="0.5" Width="2.25" />
+    <Compartments>
+      <Compartment Name="Fields" Collapsed="true" />
+    </Compartments>
+    <TypeIdentifier>
+      <HashCode>ABJAAAIAACBACRAAg4IAAAAgAJIEgQQAKACIBACAIgI=</HashCode>
+      <FileName>GuiTestContext.cs</FileName>
+    </TypeIdentifier>
+    <ShowAsAssociation>
+      <Field Name="_output" />
+      <Field Name="_winInput" />
+      <Field Name="_netInput" />
+    </ShowAsAssociation>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.TextWriterLoggerProvider" Collapsed="true">
+    <Position X="10" Y="2.75" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAIAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>TextWriterLoggerProvider.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.TextWriterLogger" Collapsed="true">
+    <Position X="10" Y="1.75" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAEAAAAAAAgAAAAAAAAIAAAAAAA=</HashCode>
+      <FileName>TextWriterLogger.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
+  <Class Name="TerminalGuiFluentTesting.NetSequences">
+    <Position X="11" Y="4.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAACACAAAAAAgI=</HashCode>
+      <FileName>NetSequences.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Enum Name="TerminalGuiFluentTesting.V2TestDriver">
+    <Position X="9.25" Y="4.5" Width="1.5" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAACAAA=</HashCode>
+      <FileName>V2TestDriver.cs</FileName>
+    </TypeIdentifier>
+  </Enum>
+  <Font Name="Segoe UI" Size="9" />
+</ClassDiagram>

+ 34 - 0
TerminalGuiFluentTesting/FakeInput.cs

@@ -0,0 +1,34 @@
+using System.Collections.Concurrent;
+using Terminal.Gui;
+
+namespace TerminalGuiFluentTesting;
+
+internal class FakeInput<T> : IConsoleInput<T>
+{
+    private readonly CancellationToken _hardStopToken;
+
+    private readonly CancellationTokenSource _timeoutCts;
+
+    public FakeInput (CancellationToken hardStopToken)
+    {
+        _hardStopToken = hardStopToken;
+
+        // Create a timeout-based cancellation token too to prevent tests ever fully hanging
+        _timeoutCts = new (With.Timeout);
+    }
+
+    /// <inheritdoc/>
+    public void Dispose () { }
+
+    /// <inheritdoc/>
+    public void Initialize (ConcurrentQueue<T> inputBuffer) { InputBuffer = inputBuffer; }
+
+    public ConcurrentQueue<T> InputBuffer { get; set; }
+
+    /// <inheritdoc/>
+    public void Run (CancellationToken token)
+    {
+        // Blocks until either the token or the hardStopToken is cancelled.
+        WaitHandle.WaitAny (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle });
+    }
+}

+ 6 - 0
TerminalGuiFluentTesting/FakeNetInput.cs

@@ -0,0 +1,6 @@
+using Terminal.Gui;
+
+namespace TerminalGuiFluentTesting;
+
+internal class FakeNetInput (CancellationToken hardStopToken) : FakeInput<ConsoleKeyInfo> (hardStopToken), INetInput
+{ }

+ 28 - 0
TerminalGuiFluentTesting/FakeOutput.cs

@@ -0,0 +1,28 @@
+using System.Drawing;
+using Terminal.Gui;
+
+namespace TerminalGuiFluentTesting;
+
+internal class FakeOutput : IConsoleOutput
+{
+    public IOutputBuffer LastBuffer { get; set; }
+    public Size Size { get; set; }
+
+    /// <inheritdoc/>
+    public void Dispose () { }
+
+    /// <inheritdoc/>
+    public void Write (ReadOnlySpan<char> text) { }
+
+    /// <inheritdoc/>
+    public void Write (IOutputBuffer buffer) { LastBuffer = buffer; }
+
+    /// <inheritdoc/>
+    public Size GetWindowSize () { return Size; }
+
+    /// <inheritdoc/>
+    public void SetCursorVisibility (CursorVisibility visibility) { }
+
+    /// <inheritdoc/>
+    public void SetCursorPosition (int col, int row) { }
+}

+ 6 - 0
TerminalGuiFluentTesting/FakeWindowsInput.cs

@@ -0,0 +1,6 @@
+using Terminal.Gui;
+
+namespace TerminalGuiFluentTesting;
+
+internal class FakeWindowsInput (CancellationToken hardStopToken) : FakeInput<WindowsConsole.InputRecord> (hardStopToken), IWindowsInput
+{ }

+ 551 - 0
TerminalGuiFluentTesting/GuiTestContext.cs

@@ -0,0 +1,551 @@
+using System.Text;
+using Microsoft.Extensions.Logging;
+using Terminal.Gui;
+using Terminal.Gui.ConsoleDrivers;
+
+namespace TerminalGuiFluentTesting;
+
+/// <summary>
+/// Fluent API context for testing a Terminal.Gui application. Create
+/// an instance using <see cref="With"/> static class.
+/// </summary>
+public class GuiTestContext : IDisposable
+{
+    private readonly CancellationTokenSource _cts = new ();
+    private readonly CancellationTokenSource _hardStop = new (With.Timeout);
+    private readonly Task _runTask;
+    private Exception _ex;
+    private readonly FakeOutput _output = new ();
+    private readonly FakeWindowsInput _winInput;
+    private readonly FakeNetInput _netInput;
+    private View? _lastView;
+    private readonly StringBuilder _logsSb;
+    private readonly V2TestDriver _driver;
+
+    internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver)
+    {
+        IApplication origApp = ApplicationImpl.Instance;
+        ILogger? origLogger = Logging.Logger;
+        _logsSb = new ();
+        _driver = driver;
+
+        _netInput = new (_cts.Token);
+        _winInput = new (_cts.Token);
+
+        _output.Size = new (width, height);
+
+        var v2 = new ApplicationV2 (
+                                    () => _netInput,
+                                    () => _output,
+                                    () => _winInput,
+                                    () => _output);
+
+        var booting = new SemaphoreSlim (0, 1);
+
+        // Start the application in a background thread
+        _runTask = Task.Run (
+                             () =>
+                             {
+                                 try
+                                 {
+                                     ApplicationImpl.ChangeInstance (v2);
+
+                                     ILogger logger = LoggerFactory.Create (
+                                                                            builder =>
+                                                                                builder.SetMinimumLevel (LogLevel.Trace)
+                                                                                       .AddProvider (new TextWriterLoggerProvider (new StringWriter (_logsSb))))
+                                                                   .CreateLogger ("Test Logging");
+                                     Logging.Logger = logger;
+
+                                     v2.Init (null, GetDriverName());
+
+                                     booting.Release ();
+
+                                     Toplevel t = topLevelBuilder ();
+
+                                     Application.Run (t); // This will block, but it's on a background thread now
+
+                                     Application.Shutdown ();
+                                 }
+                                 catch (OperationCanceledException)
+                                 { }
+                                 catch (Exception ex)
+                                 {
+                                     _ex = ex;
+                                 }
+                                 finally
+                                 {
+                                     ApplicationImpl.ChangeInstance (origApp);
+                                     Logging.Logger = origLogger;
+                                 }
+                             },
+                             _cts.Token);
+
+        // Wait for booting to complete with a timeout to avoid hangs
+        if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result)
+        {
+            throw new TimeoutException ("Application failed to start within the allotted time.");
+        }
+
+        WaitIteration ();
+    }
+
+    private string GetDriverName ()
+    {
+        return _driver switch
+               {
+                   V2TestDriver.V2Win => "v2win",
+                   V2TestDriver.V2Net => "v2net",
+                   _ =>
+                       throw new ArgumentOutOfRangeException ()
+               };
+    }
+
+    /// <summary>
+    ///     Stops the application and waits for the background thread to exit.
+    /// </summary>
+    public GuiTestContext Stop ()
+    {
+        if (_runTask.IsCompleted)
+        {
+            return this;
+        }
+
+        Application.Invoke (() => Application.RequestStop ());
+
+        // Wait for the application to stop, but give it a 1-second timeout
+        if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
+        {
+            _cts.Cancel ();
+
+            // Timeout occurred, force the task to stop
+            _hardStop.Cancel ();
+
+            throw new TimeoutException ("Application failed to stop within the allotted time.");
+        }
+
+        _cts.Cancel ();
+
+        if (_ex != null)
+        {
+            throw _ex; // Propagate any exception that happened in the background task
+        }
+
+        return this;
+    }
+
+    /// <summary>
+    /// Cleanup to avoid state bleed between tests
+    /// </summary>
+    public void Dispose ()
+    {
+        Stop ();
+
+        if (_hardStop.IsCancellationRequested)
+        {
+            throw new (
+                       "Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test");
+        }
+
+        _hardStop.Cancel ();
+    }
+
+    /// <summary>
+    ///     Adds the given <paramref name="v"/> to the current top level view
+    ///     and performs layout.
+    /// </summary>
+    /// <param name="v"></param>
+    /// <returns></returns>
+    public GuiTestContext Add (View v)
+    {
+        WaitIteration (
+                       () =>
+                       {
+                           Toplevel top = Application.Top ?? throw new ("Top was null so could not add view");
+                           top.Add (v);
+                           top.Layout ();
+                           _lastView = v;
+                       });
+
+        return this;
+    }
+
+    /// <summary>
+    /// Simulates changing the console size e.g. by resizing window in your operating system
+    /// </summary>
+    /// <param name="width">new Width for the console.</param>
+    /// <param name="height">new Height for the console.</param>
+    /// <returns></returns>
+    public GuiTestContext ResizeConsole (int width, int height)
+    {
+        _output.Size = new (width, height);
+
+        return WaitIteration ();
+    }
+
+    public GuiTestContext ScreenShot (string title, TextWriter writer)
+    {
+        writer.WriteLine (title + ":");
+        var text = Application.ToString ();
+
+        writer.WriteLine (text);
+
+        return WaitIteration ();
+    }
+
+    /// <summary>
+    /// Writes all Terminal.Gui engine logs collected so far to the <paramref name="writer"/>
+    /// </summary>
+    /// <param name="writer"></param>
+    /// <returns></returns>
+    public GuiTestContext WriteOutLogs (TextWriter writer)
+    {
+        writer.WriteLine (_logsSb.ToString ());
+
+        return WaitIteration ();
+    }
+
+    /// <summary>
+    /// Waits until the end of the current iteration of the main loop. Optionally
+    /// running a given <paramref name="a"/> action on the UI thread at that time.
+    /// </summary>
+    /// <param name="a"></param>
+    /// <returns></returns>
+    public GuiTestContext WaitIteration (Action? a = null)
+    {
+        a ??= () => { };
+        var ctsLocal = new CancellationTokenSource ();
+
+        Application.Invoke (
+                            () =>
+                            {
+                                a ();
+                                ctsLocal.Cancel ();
+                            });
+
+        // Blocks until either the token or the hardStopToken is cancelled.
+        WaitHandle.WaitAny (
+                            new []
+                            {
+                                _cts.Token.WaitHandle,
+                                _hardStop.Token.WaitHandle,
+                                ctsLocal.Token.WaitHandle
+                            });
+
+        return this;
+    }
+
+    /// <summary>
+    /// Performs the supplied <paramref name="doAction"/> immediately.
+    /// Enables running commands without breaking the Fluent API calls.
+    /// </summary>
+    /// <param name="doAction"></param>
+    /// <returns></returns>
+    public GuiTestContext Then (Action doAction)
+    {
+        doAction ();
+
+        return this;
+    }
+
+    /// <summary>
+    /// Simulates a right click at the given screen coordinates on the current driver.
+    /// This is a raw input event that goes through entire processing pipeline as though
+    /// user had pressed the mouse button physically.
+    /// </summary>
+    /// <param name="screenX">0 indexed screen coordinates</param>
+    /// <param name="screenY">0 indexed screen coordinates</param>
+    /// <returns></returns>
+    public GuiTestContext RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); }
+
+    /// <summary>
+    /// Simulates a left click at the given screen coordinates on the current driver.
+    /// This is a raw input event that goes through entire processing pipeline as though
+    /// user had pressed the mouse button physically.
+    /// </summary>
+    /// <param name="screenX">0 indexed screen coordinates</param>
+    /// <param name="screenY">0 indexed screen coordinates</param>
+    /// <returns></returns>
+    public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); }
+
+    private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+
+                _winInput.InputBuffer.Enqueue (
+                                               new ()
+                                               {
+                                                   EventType = WindowsConsole.EventType.Mouse,
+                                                   MouseEvent = new ()
+                                                   {
+                                                       ButtonState = btn,
+                                                       MousePosition = new ((short)screenX, (short)screenY)
+                                                   }
+                                               });
+
+                _winInput.InputBuffer.Enqueue (
+                                               new ()
+                                               {
+                                                   EventType = WindowsConsole.EventType.Mouse,
+                                                   MouseEvent = new ()
+                                                   {
+                                                       ButtonState = WindowsConsole.ButtonState.NoButtonPressed,
+                                                       MousePosition = new ((short)screenX, (short)screenY)
+                                                   }
+                                               });
+                break;
+            case V2TestDriver.V2Net:
+
+                int netButton = btn switch
+                                {
+                                    WindowsConsole.ButtonState.Button1Pressed => 0,
+                                    WindowsConsole.ButtonState.Button2Pressed => 1,
+                                    WindowsConsole.ButtonState.Button3Pressed => 2,
+                                    WindowsConsole.ButtonState.RightmostButtonPressed => 2,
+                                    _ => throw new ArgumentOutOfRangeException(nameof(btn))
+                                };
+                foreach (var k in NetSequences.Click(netButton,screenX,screenY))
+                {
+                    SendNetKey (k);
+                }
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+        WaitIteration ();
+
+        return this;
+    }
+
+    public GuiTestContext Down ()
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+                SendWindowsKey (ConsoleKeyMapping.VK.DOWN);
+                WaitIteration ();
+                break;
+            case V2TestDriver.V2Net:
+                foreach (var k in NetSequences.Down)
+                {
+                    SendNetKey (k);
+                }
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+
+        return this;
+    }
+
+    /// <summary>
+    /// Simulates the Right cursor key
+    /// </summary>
+    /// <returns></returns>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    public GuiTestContext Right ()
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+                SendWindowsKey (ConsoleKeyMapping.VK.RIGHT);
+                WaitIteration ();
+                break;
+            case V2TestDriver.V2Net:
+                foreach (var k in NetSequences.Right)
+                {
+                    SendNetKey (k);
+                }
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+        return this;
+    }
+
+    /// <summary>
+    /// Simulates the Left cursor key
+    /// </summary>
+    /// <returns></returns>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    public GuiTestContext Left ()
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+                SendWindowsKey (ConsoleKeyMapping.VK.LEFT);
+                WaitIteration ();
+                break;
+            case V2TestDriver.V2Net:
+                foreach (var k in NetSequences.Left)
+                {
+                    SendNetKey (k);
+                }
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+        return this;
+    }
+
+    /// <summary>
+    /// Simulates the up cursor key
+    /// </summary>
+    /// <returns></returns>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    public GuiTestContext Up ()
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+                SendWindowsKey (ConsoleKeyMapping.VK.UP);
+                WaitIteration ();
+                break;
+            case V2TestDriver.V2Net:
+                foreach (var k in NetSequences.Up)
+                {
+                    SendNetKey (k);
+                }
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+        return this;
+    }
+
+    /// <summary>
+    /// Simulates pressing the Return/Enter (newline) key.
+    /// </summary>
+    /// <returns></returns>
+    /// <exception cref="ArgumentOutOfRangeException"></exception>
+    public GuiTestContext Enter ()
+    {
+        switch (_driver)
+        {
+            case V2TestDriver.V2Win:
+                SendWindowsKey (
+                                new WindowsConsole.KeyEventRecord
+                                {
+                                    UnicodeChar = '\r',
+                                    dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
+                                    wRepeatCount = 1,
+                                    wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
+                                    wVirtualScanCode = 28
+                                });
+                break;
+            case V2TestDriver.V2Net:
+                SendNetKey (new ('\r', ConsoleKey.Enter, false, false, false));
+                break;
+            default:
+                throw new ArgumentOutOfRangeException ();
+        }
+
+        return this;
+    }
+
+    /// <summary>
+    /// Registers a right click handler on the <see cref="LastView"/> added view (or root view) that
+    /// will open the supplied <paramref name="menuItems"/>.
+    /// </summary>
+    /// <param name="ctx"></param>
+    /// <param name="menuItems"></param>
+    /// <returns></returns>
+    public GuiTestContext WithContextMenu (ContextMenu ctx, MenuBarItem menuItems)
+    {
+        LastView.MouseEvent += (s, e) =>
+                               {
+                                   if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
+                                   {
+                                       ctx.Show (menuItems);
+                                   }
+                               };
+
+        return this;
+    }
+
+    /// <summary>
+    /// The last view added (e.g. with <see cref="Add"/>) or the root/current top.
+    /// </summary>
+    public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to");
+
+    /// <summary>
+    ///     Send a full windows OS key including both down and up.
+    /// </summary>
+    /// <param name="fullKey"></param>
+    private void SendWindowsKey (WindowsConsole.KeyEventRecord fullKey)
+    {
+        WindowsConsole.KeyEventRecord down = fullKey;
+        WindowsConsole.KeyEventRecord up = fullKey; // because struct this is new copy
+
+        down.bKeyDown = true;
+        up.bKeyDown = false;
+
+        _winInput.InputBuffer.Enqueue (
+                                       new ()
+                                       {
+                                           EventType = WindowsConsole.EventType.Key,
+                                           KeyEvent = down
+                                       });
+
+        _winInput.InputBuffer.Enqueue (
+                                       new ()
+                                       {
+                                           EventType = WindowsConsole.EventType.Key,
+                                           KeyEvent = up
+                                       });
+
+        WaitIteration ();
+    }
+
+
+    private void SendNetKey (ConsoleKeyInfo consoleKeyInfo)
+    {
+        _netInput.InputBuffer.Enqueue (consoleKeyInfo);
+    }
+
+    /// <summary>
+    ///     Sends a special key e.g. cursor key that does not map to a specific character
+    /// </summary>
+    /// <param name="specialKey"></param>
+    private void SendWindowsKey (ConsoleKeyMapping.VK specialKey)
+    {
+        _winInput.InputBuffer.Enqueue (
+                                       new ()
+                                       {
+                                           EventType = WindowsConsole.EventType.Key,
+                                           KeyEvent = new ()
+                                           {
+                                               bKeyDown = true,
+                                               wRepeatCount = 0,
+                                               wVirtualKeyCode = specialKey,
+                                               wVirtualScanCode = 0,
+                                               UnicodeChar = '\0',
+                                               dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
+                                           }
+                                       });
+
+        _winInput.InputBuffer.Enqueue (
+                                       new ()
+                                       {
+                                           EventType = WindowsConsole.EventType.Key,
+                                           KeyEvent = new ()
+                                           {
+                                               bKeyDown = false,
+                                               wRepeatCount = 0,
+                                               wVirtualKeyCode = specialKey,
+                                               wVirtualScanCode = 0,
+                                               UnicodeChar = '\0',
+                                               dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
+                                           }
+                                       });
+
+        WaitIteration ();
+    }
+}

+ 53 - 0
TerminalGuiFluentTesting/NetSequences.cs

@@ -0,0 +1,53 @@
+namespace TerminalGuiFluentTesting;
+class NetSequences
+{
+    public static ConsoleKeyInfo [] Down = new []
+    {
+        new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
+        new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
+        new ConsoleKeyInfo('B', ConsoleKey.None, false, false, false),
+    };
+
+    public static ConsoleKeyInfo [] Up = new []
+    {
+        new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
+        new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
+        new ConsoleKeyInfo('A', ConsoleKey.None, false, false, false),
+    };
+
+    public static ConsoleKeyInfo [] Left = new []
+    {
+        new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
+        new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
+        new ConsoleKeyInfo('D', ConsoleKey.None, false, false, false),
+    };
+
+    public static ConsoleKeyInfo [] Right = new []
+    {
+        new ConsoleKeyInfo('\x1B', ConsoleKey.Enter, false, false, false),
+        new ConsoleKeyInfo('[', ConsoleKey.None, false, false, false),
+        new ConsoleKeyInfo('C', ConsoleKey.None, false, false, false),
+    };
+
+    public static IEnumerable<ConsoleKeyInfo> Click (int button, int screenX, int screenY)
+    {
+        // Adjust for 1-based coordinates
+        int adjustedX = screenX + 1;
+        int adjustedY = screenY + 1;
+
+        // Mouse press sequence
+        var sequence = $"\x1B[<{button};{adjustedX};{adjustedY}M";
+        foreach (char c in sequence)
+        {
+            yield return new ConsoleKeyInfo (c, ConsoleKey.None, false, false, false);
+        }
+
+        // Mouse release sequence
+        sequence = $"\x1B[<{button};{adjustedX};{adjustedY}m";
+        foreach (char c in sequence)
+        {
+            yield return new ConsoleKeyInfo (c, ConsoleKey.None, false, false, false);
+        }
+    }
+
+}

+ 14 - 0
TerminalGuiFluentTesting/TerminalGuiFluentTesting.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+	<PropertyGroup>
+		<TargetFramework>net8.0</TargetFramework>
+		<ImplicitUsings>enable</ImplicitUsings>
+		<Nullable>enable</Nullable>
+		<DocumentationFile>bin\$(Configuration)\$(AssemblyName).xml</DocumentationFile>
+	</PropertyGroup>
+
+	<ItemGroup>
+		<ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
+	</ItemGroup>
+
+</Project>

+ 21 - 0
TerminalGuiFluentTesting/TextWriterLogger.cs

@@ -0,0 +1,21 @@
+using Microsoft.Extensions.Logging;
+
+namespace TerminalGuiFluentTesting;
+
+internal class TextWriterLogger (TextWriter writer) : ILogger
+{
+    public IDisposable? BeginScope<TState> (TState state) { return null; }
+
+    public bool IsEnabled (LogLevel logLevel) { return true; }
+
+    public void Log<TState> (
+        LogLevel logLevel,
+        EventId eventId,
+        TState state,
+        Exception? ex,
+        Func<TState, Exception?, string> formatter
+    )
+    {
+        writer.WriteLine (formatter (state, ex));
+    }
+}

部分文件因为文件数量过多而无法显示