浏览代码

Merge branch 'v2_develop' into copilot/enable-menubar-replacement

Tig 1 周之前
父节点
当前提交
23565b7eb1
共有 100 个文件被更改,包括 3022 次插入1186 次删除
  1. 2 11
      Examples/Example/Example.cs
  2. 35 0
      Examples/PowershellExample.ps1
  3. 1 1
      Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs
  4. 2 2
      Examples/UICatalog/Scenarios/ColorPicker.cs
  5. 1 4
      Examples/UICatalog/Scenarios/CombiningMarks.cs
  6. 17 16
      Examples/UICatalog/Scenarios/Images.cs
  7. 7 10
      Examples/UICatalog/Scenarios/LineDrawing.cs
  8. 14 2
      Examples/UICatalog/Scenarios/Menus.cs
  9. 1 1
      Examples/UICatalog/Scenarios/RegionScenario.cs
  10. 1 1
      Examples/UICatalog/Scenarios/Snake.cs
  11. 1 1
      Examples/UICatalog/Scenarios/TextEffectsScenario.cs
  12. 102 24
      Examples/UICatalog/Scenarios/Transparent.cs
  13. 9 5
      Examples/UICatalog/UICatalog.cs
  14. 52 46
      Examples/UICatalog/UICatalogRunnable.cs
  15. 9 25
      Terminal.Gui/App/Application.Driver.cs
  16. 12 8
      Terminal.Gui/App/ApplicationImpl.Driver.cs
  17. 4 24
      Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
  18. 27 44
      Terminal.Gui/App/ApplicationImpl.Screen.cs
  19. 0 13
      Terminal.Gui/App/ApplicationImpl.cs
  20. 0 13
      Terminal.Gui/App/IApplication.cs
  21. 12 48
      Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs
  22. 2 8
      Terminal.Gui/App/Mouse/MouseImpl.cs
  23. 1 1
      Terminal.Gui/Drawing/Cell.cs
  24. 23 0
      Terminal.Gui/Drawing/GraphemeHelper.cs
  25. 12 4
      Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs
  26. 5 4
      Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs
  27. 1 1
      Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs
  28. 31 0
      Terminal.Gui/Drivers/Driver.cs
  29. 181 163
      Terminal.Gui/Drivers/DriverImpl.cs
  30. 16 5
      Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs
  31. 164 123
      Terminal.Gui/Drivers/IDriver.cs
  32. 13 2
      Terminal.Gui/Drivers/IOutput.cs
  33. 1 1
      Terminal.Gui/Drivers/ISizeMonitor.cs
  34. 84 25
      Terminal.Gui/Drivers/OutputBase.cs
  35. 1 1
      Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs
  36. 75 99
      Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs
  37. 5 5
      Terminal.Gui/Resources/config.json
  38. 76 26
      Terminal.Gui/Text/TextFormatter.cs
  39. 2 2
      Terminal.Gui/ViewBase/Adornment/Adornment.cs
  40. 1 3
      Terminal.Gui/ViewBase/Adornment/Border.cs
  41. 10 10
      Terminal.Gui/ViewBase/Adornment/Margin.cs
  42. 1 1
      Terminal.Gui/ViewBase/Adornment/ShadowView.cs
  43. 58 6
      Terminal.Gui/ViewBase/DrawContext.cs
  44. 1 1
      Terminal.Gui/ViewBase/View.Content.cs
  45. 98 212
      Terminal.Gui/ViewBase/View.Drawing.cs
  46. 168 0
      Terminal.Gui/ViewBase/View.NeedsDraw.cs
  47. 1 1
      Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs
  48. 1 1
      Terminal.Gui/Views/CharMap/CharMap.cs
  49. 1 1
      Terminal.Gui/Views/Color/ColorBar.cs
  50. 1 1
      Terminal.Gui/Views/Color/ColorPicker.16.cs
  51. 6 6
      Terminal.Gui/Views/Color/ColorPicker.Prompt.cs
  52. 1 1
      Terminal.Gui/Views/Color/ColorPicker.cs
  53. 2 2
      Terminal.Gui/Views/ComboBox.cs
  54. 1 1
      Terminal.Gui/Views/FileDialogs/FileDialog.cs
  55. 1 1
      Terminal.Gui/Views/GraphView/GraphView.cs
  56. 1 1
      Terminal.Gui/Views/HexView.cs
  57. 1 1
      Terminal.Gui/Views/Line.cs
  58. 2 2
      Terminal.Gui/Views/ListView.cs
  59. 1 1
      Terminal.Gui/Views/ProgressBar.cs
  60. 3 3
      Terminal.Gui/Views/Shortcut.cs
  61. 1 1
      Terminal.Gui/Views/Slider/Slider.cs
  62. 1 1
      Terminal.Gui/Views/SpinnerView/SpinnerView.cs
  63. 1 1
      Terminal.Gui/Views/TableView/TableView.cs
  64. 3 2
      Terminal.Gui/Views/TextInput/TextField.cs
  65. 6 1
      Terminal.Gui/Views/TextInput/TextModel.cs
  66. 1 1
      Terminal.Gui/Views/TextInput/TextValidateField.cs
  67. 2 2
      Terminal.Gui/Views/TextInput/TextView.cs
  68. 1 1
      Terminal.Gui/Views/TreeView/TreeView.cs
  69. 3 0
      Terminal.sln.DotSettings
  70. 1 1
      Tests/UnitTests/Application/SynchronizatonContextTests.cs
  71. 28 0
      Tests/UnitTests/Configuration/SourcesManagerTests.cs
  72. 1 1
      Tests/UnitTests/FakeDriverBase.cs
  73. 1 1
      Tests/UnitTests/View/Draw/DrawTests.cs
  74. 1 1
      Tests/UnitTests/Views/ComboBoxTests.cs
  75. 3 4
      Tests/UnitTestsParallelizable/Application/IApplicationScreenChangedTests.cs
  76. 1 1
      Tests/UnitTestsParallelizable/Application/NestedRunTimeoutTests.cs
  77. 0 25
      Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs
  78. 2 2
      Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs
  79. 2 1
      Tests/UnitTestsParallelizable/Drawing/CellTests.cs
  80. 1 1
      Tests/UnitTestsParallelizable/Drawing/Lines/StraightLineExtensionsTests.cs
  81. 1 1
      Tests/UnitTestsParallelizable/Drawing/Lines/StraightLineTests.cs
  82. 115 5
      Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs
  83. 228 0
      Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs
  84. 62 0
      Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs
  85. 252 0
      Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs
  86. 5 5
      Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs
  87. 3 3
      Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs
  88. 1 1
      Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs
  89. 1 1
      Tests/UnitTestsParallelizable/Drivers/DriverTests.cs
  90. 54 0
      Tests/UnitTestsParallelizable/Drivers/LegacyConsoleTests.cs
  91. 218 0
      Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs
  92. 20 20
      Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs
  93. 3 3
      Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs
  94. 2 0
      Tests/UnitTestsParallelizable/Text/StringTests.cs
  95. 4 4
      Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs
  96. 1 1
      Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentTests.cs
  97. 51 37
      Tests/UnitTestsParallelizable/ViewBase/Draw/ClearViewportTests.cs
  98. 378 4
      Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs
  99. 201 0
      Tests/UnitTestsParallelizable/ViewBase/Draw/StaticDrawTests.cs
  100. 0 28
      Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs

+ 2 - 11
Examples/Example/Example.cs

@@ -8,8 +8,8 @@ using Terminal.Gui.Configuration;
 using Terminal.Gui.ViewBase;
 using Terminal.Gui.Views;
 
-// Override the default configuration for the application to use the Light theme
-ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }""";
+// Override the default configuration for the application to use the Amber Phosphor theme
+ConfigurationManager.RuntimeConfig = """{ "Theme": "Amber Phosphor" }""";
 ConfigurationManager.Enable (ConfigLocations.All);
 
 IApplication app = Application.Create ();
@@ -90,14 +90,5 @@ public sealed class ExampleWindow : Window
 
         // Add the views to the Window
         Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin);
-
-        var lv = new ListView
-        {
-            Y = Pos.AnchorEnd (),
-            Height = Dim.Auto (),
-            Width = Dim.Auto ()
-        };
-        lv.SetSource (["One", "Two", "Three", "Four"]);
-        Add (lv);
     }
 }

+ 35 - 0
Examples/PowershellExample.ps1

@@ -0,0 +1,35 @@
+using namespace Terminal.Gui.App        
+using namespace Terminal.Gui.ViewBase
+using namespace Terminal.Gui.Views
+
+$dllFolder = "..\Terminal.Gui\bin\Debug\net8.0"
+
+# For this to work all dependent DLLs need to be in the $dllFolder folder
+# Do this first:
+#   dotnet build -c Debug /p:CopyLocalLockFileAssemblies=true
+
+Get-ChildItem $dllFolder -Filter *.dll | ForEach-Object {
+    Add-Type -Path $_.FullName -ErrorAction SilentlyContinue
+}
+
+$app = [Application]::Create()
+
+$app.Init()
+
+$win = [Window]@{
+    Title  = "Terminal.Gui in Powershell"
+    Width  = [Dim]::Fill()
+    Height = [Dim]::Fill()
+}
+
+$lbl = [Label]@{
+    Text = "Hello from PowerShell + Terminal.Gui!`nPress ESC to quit"
+    X    = [Pos]::Center()
+    Y    = [Pos]::Center()
+}
+$win.Add($lbl)
+
+$app.Run($win)
+
+$win.Dispose()
+$app.Dispose()

+ 1 - 1
Examples/UICatalog/Scenarios/AnimationScenario/AnimationScenario.cs

@@ -173,7 +173,7 @@ public class AnimationScenario : Scenario
         private Rectangle _oldSize = Rectangle.Empty;
         public void NextFrame () { _currentFrame = (_currentFrame + 1) % _frameCount; }
 
-        protected override bool OnDrawingContent ()
+        protected override bool OnDrawingContent (DrawContext? context)
         {
             if (_frameCount == 0)
             {

+ 2 - 2
Examples/UICatalog/Scenarios/ColorPicker.cs

@@ -186,11 +186,11 @@ public class ColorPickers : Scenario
         {
             X = Pos.Right (cbSupportsTrueColor) + 1,
             Y = Pos.Top (lblDriverName),
-            CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
+            CheckedState = Application.Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
             Enabled = canTrueColor,
             Text = "Force16Colors"
         };
-        cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.Result == CheckState.Checked; };
+        cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Driver!.Force16Colors = evt.Result == CheckState.Checked; };
         app.Add (lblDriverName, cbSupportsTrueColor, cbUseTrueColor);
 
         // Set default colors.

+ 1 - 4
Examples/UICatalog/Scenarios/CombiningMarks.cs

@@ -10,11 +10,8 @@ public class CombiningMarks : Scenario
         Application.Init ();
         var top = new Runnable ();
 
-        top.DrawComplete += (s, e) =>
+        top.DrawingContent += (s, e) =>
         {
-            // Forces reset _lineColsOffset because we're dealing with direct draw
-            Application.TopRunnableView!.SetNeedsDraw ();
-
             var i = -1;
             top.Move (0, ++i);
             top.AddStr ("Terminal.Gui supports all combining sequences that can be rendered as an unique grapheme.");

+ 17 - 16
Examples/UICatalog/Scenarios/Images.cs

@@ -122,11 +122,11 @@ public class Images : Scenario
         {
             X = Pos.Right (cbSupportsTrueColor) + 2,
             Y = 0,
-            CheckedState = !Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
+            CheckedState = !Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
             Enabled = canTrueColor,
             Text = "Use true color"
         };
-        cbUseTrueColor.CheckedStateChanging += (_, evt) => Application.Force16Colors = evt.Result == CheckState.UnChecked;
+        cbUseTrueColor.CheckedStateChanging += (_, evt) => Driver.Force16Colors = evt.Result == CheckState.UnChecked;
         _win.Add (cbUseTrueColor);
 
         var btnOpenImage = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 0, Text = "Open Image" };
@@ -219,18 +219,21 @@ public class Images : Scenario
         Color [,] bmp = _fire.GetFirePixels ();
 
         // TODO: Static way of doing this, suboptimal
-        if (_fireSixel != null)
+        // ConcurrentQueue doesn't support Remove, so we update the existing object
+        if (_fireSixel == null)
         {
-            Application.Sixel.Remove (_fireSixel);
+            _fireSixel = new ()
+            {
+                SixelData = _fireEncoder.EncodeSixel (bmp),
+                ScreenPosition = new (0, 0)
+            };
+            Application.GetSixels ().Enqueue (_fireSixel);
         }
-
-        _fireSixel = new ()
+        else
         {
-            SixelData = _fireEncoder.EncodeSixel (bmp),
-            ScreenPosition = new (0, 0)
-        };
-
-        Application.Sixel.Add (_fireSixel);
+            _fireSixel.SixelData = _fireEncoder.EncodeSixel (bmp);
+            _fireSixel.ScreenPosition = new (0, 0);
+        }
 
         _win.SetNeedsDraw ();
 
@@ -245,8 +248,6 @@ public class Images : Scenario
         _sixelNotSupported.Dispose ();
         _sixelSupported.Dispose ();
         _isDisposed = true;
-
-        Application.Sixel.Clear ();
     }
 
     private void OpenImage (object sender, CommandEventArgs e)
@@ -513,7 +514,7 @@ public class Images : Scenario
                 ScreenPosition = _screenLocationForSixel
             };
 
-            Application.Sixel.Add (_sixelImage);
+            Application.GetSixels ().Enqueue (_sixelImage);
         }
         else
         {
@@ -631,7 +632,7 @@ public class Images : Scenario
         public Image<Rgba32> FullResImage;
         private Image<Rgba32> _matchSize;
 
-        protected override bool OnDrawingContent ()
+        protected override bool OnDrawingContent (DrawContext context)
         {
             if (FullResImage == null)
             {
@@ -708,7 +709,7 @@ public class Images : Scenario
             return (columns, rows);
         }
 
-        protected override bool OnDrawingContent ()
+        protected override bool OnDrawingContent (DrawContext context)
         {
             if (_palette == null || _palette.Count == 0)
             {

+ 7 - 10
Examples/UICatalog/Scenarios/LineDrawing.cs

@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using System.Text;
 
 namespace UICatalog.Scenarios;
 
@@ -136,14 +133,14 @@ public class LineDrawing : Scenario
         var d = new Dialog
         {
             Title = title,
-            Width = Application.Force16Colors ? 35 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)),
+            Width = Driver.Force16Colors ? 35 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)),
             Height = 10
         };
 
         var btnOk = new Button
         {
             X = Pos.Center () - 5,
-            Y = Application.Force16Colors ? 6 : 4,
+            Y = Driver.Force16Colors ? 6 : 4,
             Text = "Ok",
             Width = Dim.Auto (),
             IsDefault = true
@@ -177,7 +174,7 @@ public class LineDrawing : Scenario
         d.AddButton (btnCancel);
 
         View cp;
-        if (Application.Force16Colors)
+        if (Driver.Force16Colors)
         {
             cp = new ColorPicker16
             {
@@ -200,7 +197,7 @@ public class LineDrawing : Scenario
 
         Application.Run (d);
         d.Dispose ();
-        newColor = Application.Force16Colors ? ((ColorPicker16)cp).SelectedColor : ((ColorPicker)cp).SelectedColor;
+        newColor = Driver.Force16Colors ? ((ColorPicker16)cp).SelectedColor : ((ColorPicker)cp).SelectedColor;
 
         return accept;
     }
@@ -273,7 +270,7 @@ public class DrawingArea : View
     public ITool CurrentTool { get; set; } = new DrawLineTool ();
     public DrawingArea () { AddLayer (); }
 
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
         foreach (LineCanvas canvas in Layers)
         {
@@ -380,7 +377,7 @@ public class AttributeView : View
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
         Color fg = Value.Foreground;
         Color bg = Value.Background;

+ 14 - 2
Examples/UICatalog/Scenarios/Menus.cs

@@ -253,7 +253,13 @@ public class Menus : Scenario
 
             // The source of truth is our status CB; any time it changes, update the menu item
             var enableOverwriteMenuItemCb = menuBar.GetMenuItemsWithTitle ("Overwrite").FirstOrDefault ()?.CommandView as CheckBox;
-            enableOverwriteStatusCb.CheckedStateChanged += (_, _) => enableOverwriteMenuItemCb!.CheckedState = enableOverwriteStatusCb.CheckedState;
+            enableOverwriteStatusCb.CheckedStateChanged += (_, _) =>
+                                                           {
+                                                               if (enableOverwriteMenuItemCb is { })
+                                                               {
+                                                                   enableOverwriteMenuItemCb.CheckedState = enableOverwriteStatusCb.CheckedState;
+                                                               }
+                                                           };
 
             menuBar.Accepted += (o, args) =>
                                 {
@@ -298,7 +304,13 @@ public class Menus : Scenario
 
             // The source of truth is our status CB; any time it changes, update the menu item
             var editModeMenuItemCb = menuBar.GetMenuItemsWithTitle ("EditMode").FirstOrDefault ()?.CommandView as CheckBox;
-            editModeStatusCb.CheckedStateChanged += (_, _) => editModeMenuItemCb!.CheckedState = editModeStatusCb.CheckedState;
+            editModeStatusCb.CheckedStateChanged += (_, _) =>
+                                                       {
+                                                           if (editModeMenuItemCb is { })
+                                                           {
+                                                               editModeMenuItemCb.CheckedState = editModeStatusCb.CheckedState;
+                                                           }
+                                                       };
 
             menuBar.Accepted += (o, args) =>
                                 {

+ 1 - 1
Examples/UICatalog/Scenarios/RegionScenario.cs

@@ -268,7 +268,7 @@ public class AttributeView : View
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         Color fg = Value?.Foreground ?? Color.Black;
         Color bg = Value?.Background ?? Color.Black;

+ 1 - 1
Examples/UICatalog/Scenarios/Snake.cs

@@ -322,7 +322,7 @@ public class Snake : Scenario
 
         private SnakeState State { get; }
 
-        protected override bool OnDrawingContent ()
+        protected override bool OnDrawingContent (DrawContext context)
         {
             SetAttribute (white);
             ClearViewport ();

+ 1 - 1
Examples/UICatalog/Scenarios/TextEffectsScenario.cs

@@ -109,7 +109,7 @@ internal class GradientsView : View
     private const int LABEL_HEIGHT = 1;
     private const int GRADIENT_WITH_LABEL_HEIGHT = GRADIENT_HEIGHT + LABEL_HEIGHT + 1; // +1 for spacing
 
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
         DrawTopLineGradient (Viewport);
 

+ 102 - 24
Examples/UICatalog/Scenarios/Transparent.cs

@@ -1,8 +1,9 @@
+// ReSharper disable AccessToDisposedClosure
 #nullable enable
 
 namespace UICatalog.Scenarios;
 
-[ScenarioMetadata ("Transparent", "Testing Transparency")]
+[ScenarioMetadata ("Transparent", "Demonstrates View Transparency")]
 public sealed class Transparent : Scenario
 {
     public override void Main ()
@@ -53,11 +54,19 @@ public sealed class Transparent : Scenario
 
         var tv = new TransparentView ()
         {
-            X = 3,
-            Y = 3,
-            Width = 50,
-            Height = 15
+            X = 2,
+            Y = 2,
+            Width = Dim.Fill (10),
+            Height = Dim.Fill (10)
         };
+
+        appWindow.ViewportChanged += (sender, args) =>
+                                      {
+                                          // Little hack to convert the Dim.Fill to actual size
+                                          // So resizing works
+                                          tv.Width = appWindow!.Frame.Width - 10;
+                                          tv.Height = appWindow!.Frame.Height - 10;
+                                      };
         appWindow.Add (tv);
 
         // Run - Start the application.
@@ -72,34 +81,31 @@ public sealed class Transparent : Scenario
     {
         public TransparentView ()
         {
-            Title = "Transparent View";
-            //base.Text = "View.Text.\nThis should be opaque.\nNote how clipping works?";
-            TextFormatter.Alignment = Alignment.Center;
-            TextFormatter.VerticalAlignment = Alignment.Center;
+            Title = "Transparent View - Move and Resize To See Transparency In Action";
+            base.Text = "View.Text.\nThis should be opaque. Note how clipping works?";
             Arrangement = ViewArrangement.Overlapped | ViewArrangement.Resizable | ViewArrangement.Movable;
-            ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.Transparent | Terminal.Gui.ViewBase.ViewportSettingsFlags.TransparentMouse;
+            ViewportSettings |= ViewportSettingsFlags.Transparent | ViewportSettingsFlags.TransparentMouse;
             BorderStyle = LineStyle.RoundedDotted;
-            //SchemeName = "Base";
+            SchemeName = "Base";
 
             var transparentSubView = new View ()
             {
-                Text = "Sizable/Movable View with border. Should be opaque. No Shadow.",
+                Text = "Sizable/Movable SubView with border and shadow.",
                 Id = "transparentSubView",
-                X = 4,
-                Y = 8,
+                X = Pos.Center (),
+                Y = Pos.Center (),
                 Width = 20,
                 Height = 8,
                 BorderStyle = LineStyle.Dashed,
                 Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
-                // ShadowStyle = ShadowStyle.Transparent,
+                ShadowStyle = ShadowStyle.Transparent,
             };
             transparentSubView.Border!.Thickness = new (1, 1, 1, 1);
             transparentSubView.SchemeName = "Dialog";
-            //transparentSubView.Visible = false;
 
             Button button = new Button ()
             {
-                Title = "_Opaque Shadows No Worky",
+                Title = "_Opaque Shadow",
                 X = Pos.Center (),
                 Y = 2,
                 SchemeName = "Dialog",
@@ -109,8 +115,6 @@ public sealed class Transparent : Scenario
                                     MessageBox.Query (App, "Clicked!", "Button in Transparent View", "_Ok");
                                     args.Handled = true;
                                 };
-            //button.Visible = false;
-
 
             var shortcut = new Shortcut ()
             {
@@ -121,7 +125,6 @@ public sealed class Transparent : Scenario
                 HelpText = "Help!",
                 Key = Key.F11,
                 SchemeName = "Base"
-
             };
 
             button.ClearingViewport += (sender, args) =>
@@ -129,16 +132,91 @@ public sealed class Transparent : Scenario
                                            args.Cancel = true;
                                        };
 
+            // Subscribe to DrawingContent event to draw "TUI" 
+            DrawingContent += TransparentView_DrawingContent;
 
             base.Add (button);
             base.Add (shortcut);
             base.Add (transparentSubView);
 
-            //Padding.Thickness = new (1);
-            //Padding.SchemeName = "Error";
+            Padding!.Thickness = new (1);
+            Padding.Text = "This is the Padding";
+        }
+
+        private void TransparentView_DrawingContent (object? sender, DrawEventArgs e)
+        {
+            // Draw "TUI" text using rectangular regions, positioned after "Hi"
+            // Letter "T"
+            Rectangle tTop = new (20, 5, 7, 2);      // Top horizontal bar
+            Rectangle tStem = new (23, 7, 2, 8);     // Vertical stem
+
+            // Letter "U"
+            Rectangle uLeft = new (30, 5, 2, 8);     // Left vertical bar
+            Rectangle uBottom = new (32, 13, 3, 2);  // Bottom horizontal bar
+            Rectangle uRight = new (35, 5, 2, 8);    // Right vertical bar
+
+            // Letter "I"
+            Rectangle iTop = new (39, 5, 4, 2);      // Bar on top
+            Rectangle iStem = new (40, 7, 2, 6);     // Vertical stem
+            Rectangle iBottom = new (39, 13, 4, 2);      // Bar on Bottom
+
+            // Draw "TUI" using the HotActive attribute
+            SetAttributeForRole (VisualRole.HotActive);
+            FillRect (tTop, Glyphs.BlackCircle);
+            FillRect (tStem, Glyphs.BlackCircle);
+            FillRect (uLeft, Glyphs.BlackCircle);
+            FillRect (uBottom, Glyphs.BlackCircle);
+            FillRect (uRight, Glyphs.BlackCircle);
+            FillRect (iTop, Glyphs.BlackCircle);
+            FillRect (iStem, Glyphs.BlackCircle);
+            FillRect (iBottom, Glyphs.BlackCircle);
+
+            Region tuiRegion = new Region (ViewportToScreen (tTop));
+            tuiRegion.Union (ViewportToScreen (tStem));
+            tuiRegion.Union (ViewportToScreen (uLeft));
+            tuiRegion.Union (ViewportToScreen (uBottom));
+            tuiRegion.Union (ViewportToScreen (uRight));
+            tuiRegion.Union (ViewportToScreen (iTop));
+            tuiRegion.Union (ViewportToScreen (iStem));
+            tuiRegion.Union (ViewportToScreen (iBottom));
+
+            // Register the drawn region for "TUI" to enable transparency effects
+            e.DrawContext?.AddDrawnRegion (tuiRegion);
+        }
 
-            Margin!.Thickness = new (1);
-           // Margin.ViewportSettings |= Terminal.Gui.ViewportSettingsFlags.Transparent;
+        /// <inheritdoc />
+        protected override bool OnDrawingContent (DrawContext? context)
+        {
+            base.OnDrawingContent (context);
+
+            // Draw "Hi" text using rectangular regions
+            // Letter "H"
+            Rectangle hLeft = new (5, 5, 2, 10);      // Left vertical bar
+            Rectangle hMiddle = new (7, 9, 3, 2);     // Middle horizontal bar
+            Rectangle hRight = new (10, 5, 2, 10);    // Right vertical bar
+
+            // Letter "i" (with some space between H and i)
+            Rectangle iDot = new (15, 5, 2, 2);       // Dot on top
+            Rectangle iStem = new (15, 9, 2, 6);      // Vertical stem
+
+            // Draw "Hi" using the Highlight attribute
+            SetAttributeForRole (VisualRole.Highlight);
+            FillRect (hLeft, Glyphs.BlackCircle);
+            FillRect (hMiddle, Glyphs.BlackCircle);
+            FillRect (hRight, Glyphs.BlackCircle);
+            FillRect (iDot, Glyphs.BlackCircle);
+            FillRect (iStem, Glyphs.BlackCircle);
+
+            // Register the drawn region for "Hi" to enable transparency effects
+            Region hiRegion = new Region (ViewportToScreen (hLeft));
+            hiRegion.Union (ViewportToScreen (hMiddle));
+            hiRegion.Union (ViewportToScreen (hRight));
+            hiRegion.Union (ViewportToScreen (iDot));
+            hiRegion.Union (ViewportToScreen (iStem));
+            context?.AddDrawnRegion (hiRegion);
+
+            // Return false to allow DrawingContent event to fire
+            return false;
         }
 
         /// <inheritdoc />

+ 9 - 5
Examples/UICatalog/UICatalog.cs

@@ -196,7 +196,7 @@ public class UICatalog
 
         UICatalogMain (Options);
 
-        Debug.Assert (Application.ForceDriver == string.Empty);
+        Application.ForceDriver = string.Empty;
 
         return 0;
     }
@@ -433,8 +433,10 @@ public class UICatalog
 
             // This call to Application.Shutdown brackets the Application.Init call
             // made by Scenario.Init() above
-            // TODO: Throw if shutdown was not called already
-            Application.Shutdown ();
+            if (Application.Driver is { })
+            {
+                Application.Shutdown ();
+            }
 
             VerifyObjectsWereDisposed ();
 
@@ -482,8 +484,10 @@ public class UICatalog
 
         scenario.Dispose ();
 
-        // TODO: Throw if shutdown was not called already
-        Application.Shutdown ();
+        if (Application.Driver is { })
+        {
+            Application.Shutdown ();
+        }
 
         return results;
     }

+ 52 - 46
Examples/UICatalog/UICatalogRunnable.cs

@@ -43,9 +43,12 @@ public class UICatalogRunnable : Runnable
         IsRunningChanged += IsRunningChangedHandler;
 
         // Restore previous selections
-        if (_categoryList.Source?.Count > 0) {
+        if (_categoryList.Source?.Count > 0)
+        {
             _categoryList.SelectedItem = _cachedCategoryIndex ?? 0;
-        } else {
+        }
+        else
+        {
             _categoryList.SelectedItem = null;
         }
         _scenarioList.SelectedRow = _cachedScenarioIndex;
@@ -176,7 +179,7 @@ public class UICatalogRunnable : Runnable
             _force16ColorsMenuItemCb = new ()
             {
                 Title = "Force _16 Colors",
-                CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
+                CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
                 // Best practice for CheckBoxes in menus is to disable focus and highlight states
                 CanFocus = false,
                 HighlightStates = MouseState.None
@@ -184,7 +187,7 @@ public class UICatalogRunnable : Runnable
 
             _force16ColorsMenuItemCb.CheckedStateChanging += (sender, args) =>
                                                              {
-                                                                 if (Application.Force16Colors
+                                                                 if (Application.Driver!.Force16Colors
                                                                      && args.Result == CheckState.UnChecked
                                                                      && !Application.Driver!.SupportsTrueColor)
                                                                  {
@@ -194,10 +197,10 @@ public class UICatalogRunnable : Runnable
 
             _force16ColorsMenuItemCb.CheckedStateChanged += (sender, args) =>
                                                             {
-                                                                Application.Force16Colors = args.Value == CheckState.Checked;
+                                                                Application.Driver!.Force16Colors = args.Value == CheckState.Checked;
 
                                                                 _force16ColorsShortcutCb!.CheckedState = args.Value;
-                                                                Application.LayoutAndDraw ();
+                                                                SetNeedsDraw ();
                                                             };
 
             menuItems.Add (
@@ -295,18 +298,25 @@ public class UICatalogRunnable : Runnable
             _diagnosticFlagsSelector.UsedHotKeys.Add (Key.D);
             _diagnosticFlagsSelector.AssignHotKeys = true;
             _diagnosticFlagsSelector.Value = Diagnostics;
-            _diagnosticFlagsSelector.ValueChanged += (sender, args) =>
-                                                     {
-                                                         _diagnosticFlags = (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value;
-                                                         Diagnostics = _diagnosticFlags;
-                                                     };
+            _diagnosticFlagsSelector.Selecting += (sender, args) =>
+                                                  {
+                                                      _diagnosticFlags = (ViewDiagnosticFlags)((int)args.Context!.Source!.Data!);// (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value;
+                                                      Diagnostics = _diagnosticFlags;
+                                                  };
 
-            menuItems.Add (
-                           new MenuItem
-                           {
-                               CommandView = _diagnosticFlagsSelector,
-                               HelpText = "View Diagnostics"
-                           });
+            MenuItem diagFlagMenuItem = new MenuItem ()
+            {
+                CommandView = _diagnosticFlagsSelector,
+                HelpText = "View Diagnostics"
+            };
+            diagFlagMenuItem.Accepting += (sender, args) =>
+                                         {
+                                             //_diagnosticFlags = (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value;
+                                             //Diagnostics = _diagnosticFlags;
+                                             //args.Handled = true;
+                                         };
+
+            menuItems.Add (diagFlagMenuItem);
 
             menuItems.Add (new Line ());
 
@@ -319,8 +329,13 @@ public class UICatalogRunnable : Runnable
                 HighlightStates = MouseState.None
             };
 
-            _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; };
+            //_disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; };
+            _disableMouseCb.Selecting += (sender, args) =>
+                                         {
+                                             Application.IsMouseDisabled = !Application.IsMouseDisabled;
+                                             _disableMouseCb.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.None;
 
+                                         };
             menuItems.Add (
                            new MenuItem
                            {
@@ -639,39 +654,30 @@ public class UICatalogRunnable : Runnable
         _force16ColorsShortcutCb = new ()
         {
             Title = "16 color mode",
-            CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
-            CanFocus = false
+            CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked,
+            CanFocus = true
         };
 
-        _force16ColorsShortcutCb.CheckedStateChanging += (sender, args) =>
-                                                         {
-                                                             if (Application.Force16Colors
-                                                                 && args.Result == CheckState.UnChecked
-                                                                 && !Application.Driver!.SupportsTrueColor)
-                                                             {
-                                                                 // If the driver does not support TrueColor, we cannot disable 16 colors
-                                                                 args.Handled = true;
-                                                             }
-                                                         };
-
-        _force16ColorsShortcutCb.CheckedStateChanged += (sender, args) =>
-                                                         {
-                                                             Application.Force16Colors = args.Value == CheckState.Checked;
-                                                             _force16ColorsMenuItemCb!.CheckedState = args.Value;
-                                                             Application.LayoutAndDraw ();
-                                                         };
+        Shortcut force16ColorsShortcut = new ()
+        {
+            CanFocus = false,
+            CommandView = _force16ColorsShortcutCb,
+            HelpText = "",
+            BindKeyToApplication = true,
+            Key = Key.F7
+        };
 
+        force16ColorsShortcut.Accepting += (sender, args) =>
+                                           {
+                                               Application.Driver.Force16Colors = !Application.Driver.Force16Colors;
+                                               _force16ColorsMenuItemCb!.CheckedState = Application.Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
+                                               SetNeedsDraw ();
+                                               args.Handled = true;
+                                           };
         statusBar.Add (
                        _shQuit,
                        statusBarShortcut,
-                       new Shortcut
-                       {
-                           CanFocus = false,
-                           CommandView = _force16ColorsShortcutCb,
-                           HelpText = "",
-                           BindKeyToApplication = true,
-                           Key = Key.F7
-                       },
+                       force16ColorsShortcut,
                        _shVersion
                       );
 
@@ -707,7 +713,7 @@ public class UICatalogRunnable : Runnable
         }
 
         _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked;
-        _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
+        _force16ColorsShortcutCb!.CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked;
 
         Application.TopRunnableView?.SetNeedsDraw ();
     }

+ 9 - 25
Terminal.Gui/App/Application.Driver.cs

@@ -1,4 +1,6 @@
 
+
+using System.Collections.Concurrent;
 using System.Diagnostics.CodeAnalysis;
 
 namespace Terminal.Gui.App;
@@ -13,30 +15,13 @@ public static partial class Application // Driver abstractions
         internal set => ApplicationImpl.Instance.Driver = value;
     }
 
-    private static bool _force16Colors = false; // Resources/config.json overrides
-
-    /// <inheritdoc cref="IApplication.Force16Colors"/>
-    [ConfigurationProperty (Scope = typeof (SettingsScope))]
-    [Obsolete ("The legacy static Application object is going away.")]
-    public static bool Force16Colors
-    {
-        get => _force16Colors;
-        set
-        {
-            bool oldValue = _force16Colors;
-            _force16Colors = value;
-            Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs<bool> (oldValue, _force16Colors));
-        }
-    }
-
-    /// <summary>Raised when <see cref="Force16Colors"/> changes.</summary>
-    public static event EventHandler<ValueChangedEventArgs<bool>>? Force16ColorsChanged;
-
+    // NOTE: ForceDriver is a configuration property (Application.ForceDriver).
+    // NOTE: IApplication also has a ForceDriver property, which is an instance property
+    // NOTE: set whenever this static property is set.
     private static string _forceDriver = string.Empty; // Resources/config.json overrides
 
     /// <inheritdoc cref="IApplication.ForceDriver"/>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
-    [Obsolete ("The legacy static Application object is going away.")]
     public static string ForceDriver
     {
         get => _forceDriver;
@@ -44,16 +29,15 @@ public static partial class Application // Driver abstractions
         {
             string oldValue = _forceDriver;
             _forceDriver = value;
-            ForceDriverChanged?.Invoke (null, new ValueChangedEventArgs<string> (oldValue, _forceDriver));
+            ForceDriverChanged?.Invoke (null, new (oldValue, _forceDriver));
         }
     }
 
     /// <summary>Raised when <see cref="ForceDriver"/> changes.</summary>
     public static event EventHandler<ValueChangedEventArgs<string>>? ForceDriverChanged;
 
-    /// <inheritdoc cref="IApplication.Sixel"/>
-    [Obsolete ("The legacy static Application object is going away.")] 
-    public static List<SixelToRender> Sixel => ApplicationImpl.Instance.Sixel;
+    /// <inheritdoc cref="IDriver.GetSixels"/>
+    public static ConcurrentQueue<SixelToRender> GetSixels () => ApplicationImpl.Instance.Driver?.GetSixels ()!;
 
     /// <summary>Gets a list of <see cref="IDriver"/> types and type names that are available.</summary>
     /// <returns></returns>
@@ -67,7 +51,7 @@ public static partial class Application // Driver abstractions
         // Only inspect the IDriver assembly
         var asm = typeof (IDriver).Assembly;
 
-        foreach (Type? type in asm.GetTypes ())
+        foreach (Type type in asm.GetTypes ())
         {
             if (typeof (IDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true })
             {

+ 12 - 8
Terminal.Gui/App/ApplicationImpl.Driver.cs

@@ -7,15 +7,9 @@ internal partial class ApplicationImpl
     /// <inheritdoc/>
     public IDriver? Driver { get; set; }
 
-    /// <inheritdoc/>
-    public bool Force16Colors { get; set; }
-
     /// <inheritdoc/>
     public string ForceDriver { get; set; } = string.Empty;
 
-    /// <inheritdoc/>
-    public List<SixelToRender> Sixel { get; } = new ();
-
     /// <summary>
     ///     Creates the appropriate <see cref="IDriver"/> based on platform and driverName.
     /// </summary>
@@ -85,6 +79,8 @@ internal partial class ApplicationImpl
         {
             throw new ("Driver was null even after booting MainLoopCoordinator");
         }
+
+        Driver.Force16Colors = Terminal.Gui.Drivers.Driver.Force16Colors;
     }
 
     private readonly IComponentFactory? _componentFactory;
@@ -149,7 +145,11 @@ internal partial class ApplicationImpl
 
     internal void SubscribeDriverEvents ()
     {
-        ArgumentNullException.ThrowIfNull (Driver);
+        if (Driver is null)
+        {
+            Logging.Error($"Driver is null");
+            return;
+        }
 
         Driver.SizeChanged += Driver_SizeChanged;
         Driver.KeyDown += Driver_KeyDown;
@@ -159,7 +159,11 @@ internal partial class ApplicationImpl
 
     internal void UnsubscribeDriverEvents ()
     {
-        ArgumentNullException.ThrowIfNull (Driver);
+        if (Driver is null)
+        {
+            Logging.Error ($"Driver is null");
+            return;
+        }
 
         Driver.SizeChanged -= Driver_SizeChanged;
         Driver.KeyDown -= Driver_KeyDown;

+ 4 - 24
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs

@@ -87,7 +87,7 @@ internal partial class ApplicationImpl
         _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
 
         CreateDriver (_driverName);
-        Screen = Driver!.Screen;
+
         Initialized = true;
 
         RaiseInitializedChanged (this, new (true));
@@ -269,14 +269,10 @@ internal partial class ApplicationImpl
         if (Driver is { })
         {
             UnsubscribeDriverEvents ();
-            Driver?.End ();
+            Driver.Dispose ();
             Driver = null;
         }
 
-        // Reset screen
-        ResetScreen ();
-        _screen = null;
-
         // === 5. Clear run state ===
         Iteration = null;
         SessionBegun = null;
@@ -304,23 +300,11 @@ internal partial class ApplicationImpl
         // === 7. Clear navigation and screen state ===
         ScreenChanged = null;
 
-        //Navigation = null;
-
         // === 8. Reset initialization state ===
         Initialized = false;
         MainThreadId = null;
 
-        // === 9. Clear graphics ===
-        Sixel.Clear ();
-
-        // === 10. Reset ForceDriver ===
-        // Note: ForceDriver and Force16Colors are reset
-        // If they need to persist across Init/Shutdown cycles
-        // then the user of the library should manage that state
-        Force16Colors = false;
-        ForceDriver = string.Empty;
-
-        // === 11. Reset synchronization context ===
+        // === 9. Reset synchronization context ===
         // IMPORTANT: Always reset sync context, even if not initialized
         // This ensures cleanup works correctly even if Shutdown is called without Init
         // Reset synchronization context to allow the user to run async/await,
@@ -329,7 +313,7 @@ internal partial class ApplicationImpl
         // (https://github.com/gui-cs/Terminal.Gui/issues/1084).
         SynchronizationContext.SetSynchronizationContext (null);
 
-        // === 12. Unsubscribe from Application static property change events ===
+        // === 10. Unsubscribe from Application static property change events ===
         UnsubscribeApplicationEvents ();
     }
 
@@ -368,9 +352,6 @@ internal partial class ApplicationImpl
     }
 #endif
 
-    // Event handlers for Application static property changes
-    private void OnForce16ColorsChanged (object? sender, ValueChangedEventArgs<bool> e) { Force16Colors = e.NewValue; }
-
     private void OnForceDriverChanged (object? sender, ValueChangedEventArgs<string> e) { ForceDriver = e.NewValue; }
 
     /// <summary>
@@ -378,7 +359,6 @@ internal partial class ApplicationImpl
     /// </summary>
     private void UnsubscribeApplicationEvents ()
     {
-        Application.Force16ColorsChanged -= OnForce16ColorsChanged;
         Application.ForceDriverChanged -= OnForceDriverChanged;
     }
 }

+ 27 - 44
Terminal.Gui/App/ApplicationImpl.Screen.cs

@@ -1,4 +1,3 @@
-
 namespace Terminal.Gui.App;
 
 internal partial class ApplicationImpl
@@ -6,24 +5,10 @@ internal partial class ApplicationImpl
     /// <inheritdoc/>
     public event EventHandler<EventArgs<Rectangle>>? ScreenChanged;
 
-    private readonly object _lockScreen = new ();
-    private Rectangle? _screen;
-
     /// <inheritdoc/>
     public Rectangle Screen
     {
-        get
-        {
-            lock (_lockScreen)
-            {
-                if (_screen == null)
-                {
-                    _screen = Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
-                }
-
-                return _screen.Value;
-            }
-        }
+        get => Driver?.Screen ?? new (new (0, 0), new (2048, 2048));
         set
         {
             if (value is { } && (value.X != 0 || value.Y != 0))
@@ -31,10 +16,7 @@ internal partial class ApplicationImpl
                 throw new NotImplementedException ("Screen locations other than 0, 0 are not yet supported");
             }
 
-            lock (_lockScreen)
-            {
-                _screen = value;
-            }
+            Driver?.SetScreenSize (value.Width, value.Height);
         }
     }
 
@@ -114,16 +96,6 @@ internal partial class ApplicationImpl
         return false;
     }
 
-    /// <summary>
-    ///     INTERNAL: Resets the Screen field to null so it will be recalculated on next access.
-    /// </summary>
-    private void ResetScreen ()
-    {
-        lock (_lockScreen)
-        {
-            _screen = null;
-        }
-    }
 
     /// <summary>
     ///     INTERNAL: Called when the application's screen has changed.
@@ -132,7 +104,7 @@ internal partial class ApplicationImpl
     /// <param name="screen">The new screen size and position.</param>
     private void RaiseScreenChangedEvent (Rectangle screen)
     {
-        Screen = new (Point.Empty, screen.Size);
+        //Screen = new (Point.Empty, screen.Size);
 
         ScreenChanged?.Invoke (this, new (screen));
 
@@ -150,17 +122,6 @@ internal partial class ApplicationImpl
     /// <inheritdoc/>
     public void LayoutAndDraw (bool forceRedraw = false)
     {
-        List<View?> tops = [.. SessionStack!.Select(r => r.Runnable! as View)!];
-
-        if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
-        {
-            visiblePopover.SetNeedsDraw ();
-            visiblePopover.SetNeedsLayout ();
-            tops.Insert (0, visiblePopover);
-        }
-
-        bool neededLayout = View.Layout (tops.ToArray ().Reverse ()!, Screen.Size);
-
         if (ClearScreenNextIteration)
         {
             forceRedraw = true;
@@ -172,12 +133,34 @@ internal partial class ApplicationImpl
             Driver?.ClearContents ();
         }
 
-        if (Driver is { })
+        List<View?> views = [.. SessionStack!.Select (r => r.Runnable! as View)!];
+
+        if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
+        {
+            visiblePopover.SetNeedsDraw ();
+            visiblePopover.SetNeedsLayout ();
+            views.Insert (0, visiblePopover);
+        }
+
+        // Layout
+        bool neededLayout = View.Layout (views.ToArray ().Reverse ()!, Screen.Size);
+
+        // Draw
+        bool needsDraw = forceRedraw || views.Any (v => v is { NeedsDraw: true } or { SubViewNeedsDraw: true });
+
+        if (Driver is { } && (neededLayout || needsDraw))
         {
+            Logging.Redraws.Add (1);
+
             Driver.Clip = new (Screen);
 
-            View.Draw (views: tops!, neededLayout || forceRedraw);
+            // Only force a complete redraw if needed (needsLayout or forceRedraw).
+            // Otherwise, just redraw views that need it.
+            View.Draw (views: views.ToArray ().Cast<View> (), neededLayout || forceRedraw);
+
             Driver.Clip = new (Screen);
+
+            // Cause the driver to flush any pending updates to the terminal
             Driver?.Refresh ();
         }
     }

+ 0 - 13
Terminal.Gui/App/ApplicationImpl.cs

@@ -15,7 +15,6 @@ internal partial class ApplicationImpl : IApplication
     internal ApplicationImpl ()
     {
         // Subscribe to Application static property change events
-        Application.Force16ColorsChanged += OnForce16ColorsChanged;
         Application.ForceDriverChanged += OnForceDriverChanged;
     }
 
@@ -143,18 +142,6 @@ internal partial class ApplicationImpl : IApplication
         // If an instance exists, reset it
         _instance?.ResetState (ignoreDisposed);
 
-        // Reset Application static properties to their defaults
-        // This ensures tests start with clean state
-        Application.ForceDriver = string.Empty;
-        Application.Force16Colors = false;
-        Application.IsMouseDisabled = false;
-        Application.QuitKey = Key.Esc;
-        Application.ArrangeKey = Key.F5.WithCtrl;
-        Application.NextTabGroupKey = Key.F6;
-        Application.NextTabKey = Key.Tab;
-        Application.PrevTabGroupKey = Key.F6.WithShift;
-        Application.PrevTabKey = Key.Tab.WithShift;
-
         // Always reset the model tracking to allow tests to use either model after reset
         ResetModelUsageTracking ();
     }

+ 0 - 13
Terminal.Gui/App/IApplication.cs

@@ -449,13 +449,6 @@ public interface IApplication : IDisposable
     /// </remarks>
     IClipboard? Clipboard { get; }
 
-    /// <summary>
-    ///     Gets or sets whether <see cref="Driver"/> will be forced to output only the 16 colors defined in
-    ///     <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be
-    ///     output as long as the selected <see cref="IDriver"/> supports TrueColor.
-    /// </summary>
-    bool Force16Colors { get; set; }
-
     /// <summary>
     ///     Forces the use of the specified driver (one of "fake", "dotnet", "windows", or "unix"). If not
     ///     specified, the driver is selected based on the platform.
@@ -497,12 +490,6 @@ public interface IApplication : IDisposable
     /// </remarks>
     bool ClearScreenNextIteration { get; set; }
 
-    /// <summary>
-    ///     Collection of sixel images to write out to screen when updating.
-    ///     Only add to this collection if you are sure terminal supports sixel format.
-    /// </summary>
-    List<SixelToRender> Sixel { get; }
-
     #endregion Screen and Driver
 
     #region Keyboard

+ 12 - 48
Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs

@@ -137,30 +137,18 @@ public class ApplicationMainLoop<TInputRecord> : IApplicationMainLoop<TInputReco
         // Pull any input events from the input queue and process them
         InputProcessor.ProcessQueue ();
 
-        if (App?.TopRunnableView != null)
-        {
-            bool needsDrawOrLayout = AnySubViewsNeedDrawn (App?.Popover?.GetActivePopover () as View)
-                                     || AnySubViewsNeedDrawn (App?.TopRunnableView)
-                                     || (App?.Mouse.MouseGrabView != null && AnySubViewsNeedDrawn (App?.Mouse.MouseGrabView));
-
-            bool sizeChanged = SizeMonitor.Poll ();
-
-            if (needsDrawOrLayout || sizeChanged)
-            {
-                Logging.Redraws.Add (1);
-
-                App?.LayoutAndDraw (true);
-
-                Output.Write (OutputBuffer);
+        // Check for any size changes; this will cause SizeChanged events
+        SizeMonitor.Poll ();
 
-                Output.SetCursorVisibility (CursorVisibility.Default);
-            }
+        // Layout and draw any views that need it
+        App?.LayoutAndDraw (forceRedraw: false);
 
-            SetCursor ();
-        }
+        // Update the cursor
+        SetCursor ();
 
-        var swCallbacks = Stopwatch.StartNew ();
+        Stopwatch swCallbacks = Stopwatch.StartNew ();
 
+        // Run any timeout callbacks that are due
         TimedEvents.RunTimers ();
 
         Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds);
@@ -168,10 +156,11 @@ public class ApplicationMainLoop<TInputRecord> : IApplicationMainLoop<TInputReco
 
     private void SetCursor ()
     {
-        View? mostFocused = App?.TopRunnableView!.MostFocused;
+        View? mostFocused = App?.TopRunnableView?.MostFocused;
 
         if (mostFocused == null)
         {
+            Output.SetCursorVisibility (CursorVisibility.Invisible);
             return;
         }
 
@@ -180,9 +169,9 @@ public class ApplicationMainLoop<TInputRecord> : IApplicationMainLoop<TInputReco
         if (to.HasValue)
         {
             // Translate to screen coordinates
-            to = mostFocused.ViewportToScreen (to.Value);
+            Point screenPos = mostFocused.ViewportToScreen (to.Value);
 
-            Output.SetCursorPosition (to.Value.X, to.Value.Y);
+            Output.SetCursorPosition (screenPos.X, screenPos.Y);
             Output.SetCursorVisibility (mostFocused.CursorVisibility);
         }
         else
@@ -191,31 +180,6 @@ public class ApplicationMainLoop<TInputRecord> : IApplicationMainLoop<TInputReco
         }
     }
 
-    private bool AnySubViewsNeedDrawn (View? v)
-    {
-        if (v is null)
-        {
-            return false;
-        }
-
-        if (v.NeedsDraw || v.NeedsLayout)
-        {
-            // Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) ");
-
-            return true;
-        }
-
-        foreach (View subview in v.SubViews)
-        {
-            if (AnySubViewsNeedDrawn (subview))
-            {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     /// <inheritdoc/>
     public void Dispose ()
     {

+ 2 - 8
Terminal.Gui/App/Mouse/MouseImpl.cs

@@ -20,14 +20,8 @@ internal class MouseImpl : IMouse, IDisposable
         Application.IsMouseDisabledChanged += OnIsMouseDisabledChanged;
     }
 
-    private IApplication? _app;
-
     /// <inheritdoc/>
-    public IApplication? App
-    {
-        get => _app;
-        set => _app = value;
-    }
+    public IApplication? App { get; set; }
 
     /// <inheritdoc/>
     public Point? LastMousePosition { get; set; }
@@ -248,7 +242,7 @@ internal class MouseImpl : IMouse, IDisposable
                 continue;
             }
 
-            CancelEventArgs eventArgs = new System.ComponentModel.CancelEventArgs ();
+            CancelEventArgs eventArgs = new CancelEventArgs ();
             bool? cancelled = view.NewMouseEnterEvent (eventArgs);
 
             if (cancelled is true || eventArgs.Cancel)

+ 1 - 1
Terminal.Gui/Drawing/Cell.cs

@@ -27,7 +27,7 @@ public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, st
         readonly get => _grapheme;
         set
         {
-            if (GraphemeHelper.GetGraphemes(value).ToArray().Length > 1)
+            if (GraphemeHelper.GetGraphemeCount (value) > 1)
             {
                 throw new InvalidOperationException ($"Only a single {nameof (Grapheme)} cluster is allowed per Cell.");
             }

+ 23 - 0
Terminal.Gui/Drawing/GraphemeHelper.cs

@@ -46,4 +46,27 @@ public static class GraphemeHelper
             yield return element;
         }
     }
+
+    /// <summary>
+    ///     Counts the number of grapheme clusters in a string without allocating intermediate collections.
+    /// </summary>
+    /// <param name="text">The string to count graphemes in.</param>
+    /// <returns>The number of grapheme clusters, or 0 if the string is null or empty.</returns>
+    public static int GetGraphemeCount (string text)
+    {
+        if (string.IsNullOrEmpty (text))
+        {
+            return 0;
+        }
+
+        TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (text);
+        var count = 0;
+
+        while (enumerator.MoveNext ())
+        {
+            count++;
+        }
+
+        return count;
+    }
 }

+ 12 - 4
Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs

@@ -211,15 +211,23 @@ public class LineCanvas : IDisposable
     {
         Dictionary<Point, Rune> map = new ();
 
+        List<IntersectionDefinition> intersectionsBufferList = [];
+
         // walk through each pixel of the bitmap
         for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++)
         {
             for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
             {
-                IntersectionDefinition [] intersects = _lines
-                    .Select (l => l.Intersects (x, y))
-                    .OfType<IntersectionDefinition> () // automatically filters nulls and casts
-                    .ToArray ();
+                intersectionsBufferList.Clear ();
+                foreach (var line in _lines)
+                {
+                    if (line.Intersects (x, y) is { } intersect)
+                    {
+                        intersectionsBufferList.Add (intersect);
+                    }
+                }
+                // Safe as long as the list is not modified while the span is in use.
+                ReadOnlySpan<IntersectionDefinition> intersects = CollectionsMarshal.AsSpan(intersectionsBufferList);
 
                 Rune? rune = GetRuneForIntersects (intersects);
 

+ 5 - 4
Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs

@@ -32,8 +32,9 @@ public class SixelSupportDetector ()
     /// </returns>
     public void Detect (Action<SixelSupportResult> resultCallback)
     {
-        var result = new SixelSupportResult ();
-        result.SupportsTransparency = IsVirtualTerminal () || IsXtermWithTransparency ();
+        SixelSupportResult result = new SixelSupportResult ();
+        bool isLegacyConsole = IsLegacyConsole ();
+        result.SupportsTransparency = !isLegacyConsole || (!isLegacyConsole && IsXtermWithTransparency ());
         IsSixelSupportedByDar (result, resultCallback);
     }
 
@@ -155,9 +156,9 @@ public class SixelSupportDetector ()
 
     private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); }
 
-    private static bool IsVirtualTerminal ()
+    private bool IsLegacyConsole ()
     {
-        return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION"));
+        return _driver is { IsLegacyConsole: true };
     }
 
     private static bool IsXtermWithTransparency ()

+ 1 - 1
Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs

@@ -81,7 +81,7 @@ public class NetOutput : OutputBase, IOutput
     /// <inheritdoc/>
     protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle)
     {
-        if (Application.Force16Colors)
+        if (Force16Colors)
         {
             output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
             output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));

+ 31 - 0
Terminal.Gui/Drivers/Driver.cs

@@ -0,0 +1,31 @@
+namespace Terminal.Gui.Drivers;
+
+/// <summary>
+///     Holds global driver settings.
+/// </summary>
+public sealed class Driver
+{
+    private static bool _force16Colors = false; // Resources/config.json overrides
+
+    // NOTE: Force16Colors is a configuration property (Driver.Force16Colors).
+    // NOTE: IDriver also has a Force16Colors property, which is an instance property
+    // NOTE: set whenever this static property is set.
+    /// <summary>
+    ///     Determines if driver instances should use 16 colors instead of the default TrueColors.
+    /// </summary>
+    /// <seealso cref="IDriver.Force16Colors"/>
+    [ConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool Force16Colors
+    {
+        get => _force16Colors;
+        set
+        {
+            bool oldValue = _force16Colors;
+            _force16Colors = value;
+            Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs<bool> (oldValue, _force16Colors));
+        }
+    }
+
+    /// <summary>Raised when <see cref="Force16Colors"/> changes.</summary>
+    public static event EventHandler<ValueChangedEventArgs<bool>>? Force16ColorsChanged;
+}

+ 181 - 163
Terminal.Gui/Drivers/DriverImpl.cs

@@ -1,4 +1,5 @@
-using System.Runtime.InteropServices;
+using System.Collections.Concurrent;
+using System.Runtime.InteropServices;
 
 namespace Terminal.Gui.Drivers;
 
@@ -28,10 +29,6 @@ namespace Terminal.Gui.Drivers;
 /// </remarks>
 internal class DriverImpl : IDriver
 {
-    private readonly IOutput _output;
-    private readonly AnsiRequestScheduler _ansiRequestScheduler;
-    private CursorVisibility _lastCursor = CursorVisibility.Default;
-
     /// <summary>
     ///     Initializes a new instance of the <see cref="DriverImpl"/> class.
     /// </summary>
@@ -63,19 +60,88 @@ internal class DriverImpl : IDriver
                                      };
 
         SizeMonitor = sizeMonitor;
+        SizeMonitor.SizeChanged += OnSizeMonitorOnSizeChanged;
 
-        sizeMonitor.SizeChanged += (_, e) =>
-                                   {
-                                       SetScreenSize (e.Size!.Value.Width, e.Size.Value.Height);
+        CreateClipboard ();
 
-                                       //SizeChanged?.Invoke (this, e);
-                                   };
+        Driver.Force16ColorsChanged += OnDriverOnForce16ColorsChanged;
+    }
 
-        CreateClipboard ();
+    #region Driver Lifecycle
+
+    /// <inheritdoc/>
+    public void Init () { throw new NotSupportedException (); }
+
+    /// <inheritdoc/>
+    public void Refresh () { _output.Write (OutputBuffer); }
+
+    /// <inheritdoc/>
+    public string? GetName () => InputProcessor.DriverName?.ToLowerInvariant ();
+
+    /// <inheritdoc/>
+    public virtual string GetVersionInfo ()
+    {
+        string type = InputProcessor.DriverName ?? throw new ArgumentNullException (nameof (InputProcessor.DriverName));
+
+        return type;
     }
 
     /// <inheritdoc/>
-    public event EventHandler<SizeChangedEventArgs>? SizeChanged;
+    public void Suspend ()
+    {
+        // BUGBUG: This is all platform-specific and should not be implemented here.
+        // BUGBUG: This needs to be in each platform's driver implementation.
+        if (Environment.OSVersion.Platform != PlatformID.Unix)
+        {
+            return;
+        }
+
+        Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
+
+        try
+        {
+            Console.ResetColor ();
+            Console.Clear ();
+
+            //Disable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+            //Set cursor key to cursor.
+            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
+
+            Platform.Suspend ();
+
+            //Enable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+        }
+        catch (Exception ex)
+        {
+            Logging.Error ($"Error suspending terminal: {ex.Message}");
+        }
+
+        Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
+    }
+
+    /// <inheritdoc/>
+    public bool IsLegacyConsole
+    {
+        get => _output.IsLegacyConsole;
+        set => _output.IsLegacyConsole = value;
+    }
+
+    /// <inheritdoc/>
+    public void Dispose ()
+    {
+        SizeMonitor.SizeChanged -= OnSizeMonitorOnSizeChanged;
+        Driver.Force16ColorsChanged -= OnDriverOnForce16ColorsChanged;
+        _output.Dispose ();
+    }
+
+    #endregion Driver Lifecycle
+
+    #region Driver Components
+
+    private readonly IOutput _output;
 
     /// <inheritdoc/>
     public IInputProcessor InputProcessor { get; }
@@ -86,6 +152,9 @@ internal class DriverImpl : IDriver
     /// <inheritdoc/>
     public ISizeMonitor SizeMonitor { get; }
 
+    /// <inheritdoc/>
+    public IClipboard? Clipboard { get; private set; } = new FakeClipboard ();
+
     private void CreateClipboard ()
     {
         if (InputProcessor.DriverName is { } && InputProcessor.DriverName.Contains ("fake"))
@@ -116,16 +185,12 @@ internal class DriverImpl : IDriver
         // Clipboard is set to FakeClipboard at initialization
     }
 
-    /// <inheritdoc/>
+    #endregion Driver Components
 
-    public Rectangle Screen =>
+    #region Screen and Display
 
-        //if (Application.RunningUnitTests && _output is WindowsConsoleOutput or NetOutput)
-        //{
-        //    // In unit tests, we don't have a real output, so we return an empty rectangle.
-        //    return Rectangle.Empty;
-        //}
-        new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows);
+    /// <inheritdoc/>
+    public Rectangle Screen => new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows);
 
     /// <inheritdoc/>
     public virtual void SetScreenSize (int width, int height)
@@ -136,23 +201,11 @@ internal class DriverImpl : IDriver
     }
 
     /// <inheritdoc/>
+    public event EventHandler<SizeChangedEventArgs>? SizeChanged;
 
-    public Region? Clip
-    {
-        get => OutputBuffer.Clip;
-        set => OutputBuffer.Clip = value;
-    }
-
-    /// <inheritdoc/>
-
-    public IClipboard? Clipboard { get; private set; } = new FakeClipboard ();
+    private void OnSizeMonitorOnSizeChanged (object? _, SizeChangedEventArgs e) { SetScreenSize (e.Size!.Value.Width, e.Size.Value.Height); }
 
     /// <inheritdoc/>
-
-    public int Col => OutputBuffer.Col;
-
-    /// <inheritdoc/>
-
     public int Cols
     {
         get => OutputBuffer.Cols;
@@ -160,15 +213,13 @@ internal class DriverImpl : IDriver
     }
 
     /// <inheritdoc/>
-
-    public Cell [,]? Contents
+    public int Rows
     {
-        get => OutputBuffer.Contents;
-        set => OutputBuffer.Contents = value;
+        get => OutputBuffer.Rows;
+        set => OutputBuffer.Rows = value;
     }
 
     /// <inheritdoc/>
-
     public int Left
     {
         get => OutputBuffer.Left;
@@ -176,55 +227,45 @@ internal class DriverImpl : IDriver
     }
 
     /// <inheritdoc/>
-
-    public int Row => OutputBuffer.Row;
-
-    /// <inheritdoc/>
-
-    public int Rows
-    {
-        get => OutputBuffer.Rows;
-        set => OutputBuffer.Rows = value;
-    }
-
-    /// <inheritdoc/>
-
     public int Top
     {
         get => OutputBuffer.Top;
         set => OutputBuffer.Top = value;
     }
 
-    // TODO: Probably not everyone right?
-
-    /// <inheritdoc/>
+    #endregion Screen and Display
 
-    public bool SupportsTrueColor => true;
+    #region Color Support
 
     /// <inheritdoc/>
+    public bool SupportsTrueColor => !IsLegacyConsole;
 
+    /// <inheritdoc/>
     public bool Force16Colors
     {
-        get => Application.Force16Colors || !SupportsTrueColor;
-        set => Application.Force16Colors = value || !SupportsTrueColor;
+        get => _output.Force16Colors;
+        set => _output.Force16Colors = value;
     }
 
-    /// <inheritdoc/>
+    private void OnDriverOnForce16ColorsChanged (object? _, ValueChangedEventArgs<bool> e) { Force16Colors = e.NewValue; }
 
-    public Attribute CurrentAttribute
-    {
-        get => OutputBuffer.CurrentAttribute;
-        set => OutputBuffer.CurrentAttribute = value;
-    }
+    #endregion Color Support
 
-    /// <inheritdoc/>
-    public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); }
+    #region Content Buffer
 
     /// <inheritdoc/>
-    public void AddRune (char c) { OutputBuffer.AddRune (c); }
+    public Cell [,]? Contents
+    {
+        get => OutputBuffer.Contents;
+        set => OutputBuffer.Contents = value;
+    }
 
     /// <inheritdoc/>
-    public void AddStr (string str) { OutputBuffer.AddStr (str); }
+    public Region? Clip
+    {
+        get => OutputBuffer.Clip;
+        set => OutputBuffer.Clip = value;
+    }
 
     /// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
     public void ClearContents ()
@@ -236,20 +277,26 @@ internal class DriverImpl : IDriver
     /// <inheritdoc/>
     public event EventHandler<EventArgs>? ClearedContents;
 
+    #endregion Content Buffer
+
+    #region Drawing and Rendering
+
     /// <inheritdoc/>
-    public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
+    public int Col => OutputBuffer.Col;
 
     /// <inheritdoc/>
-    public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); }
+    public int Row => OutputBuffer.Row;
 
     /// <inheritdoc/>
-    public virtual string GetVersionInfo ()
+    public Attribute CurrentAttribute
     {
-        string type = InputProcessor.DriverName ?? throw new ArgumentNullException (nameof (InputProcessor.DriverName));
-
-        return type;
+        get => OutputBuffer.CurrentAttribute;
+        set => OutputBuffer.CurrentAttribute = value;
     }
 
+    /// <inheritdoc/>
+    public void Move (int col, int row) { OutputBuffer.Move (col, row); }
+
     /// <inheritdoc/>
     public bool IsRuneSupported (Rune rune) => Rune.IsValid (rune.Value);
 
@@ -262,77 +309,22 @@ internal class DriverImpl : IDriver
     ///     <see cref="IDriver.Clip"/>.
     ///     <see langword="true"/> otherwise.
     /// </returns>
-    public bool IsValidLocation (string text, int col, int row) { return OutputBuffer.IsValidLocation (text, col, row); }
+    public bool IsValidLocation (string text, int col, int row) => OutputBuffer.IsValidLocation (text, col, row);
 
     /// <inheritdoc/>
-    public void Move (int col, int row) { OutputBuffer.Move (col, row); }
-
-    // TODO: Probably part of output
-
-    /// <inheritdoc/>
-    public bool SetCursorVisibility (CursorVisibility visibility)
-    {
-        _lastCursor = visibility;
-        _output.SetCursorVisibility (visibility);
-
-        return true;
-    }
-
-    /// <inheritdoc/>
-    public bool GetCursorVisibility (out CursorVisibility current)
-    {
-        current = _lastCursor;
-
-        return true;
-    }
+    public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); }
 
     /// <inheritdoc/>
-    public void Suspend ()
-    {
-        // BUGBUG: This is all platform-specific and should not be implemented here.
-        // BUGBUG: This needs to be in each platform's driver implementation.
-        if (Environment.OSVersion.Platform != PlatformID.Unix)
-        {
-            return;
-        }
-
-        Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
-
-        try
-        {
-            Console.ResetColor ();
-            Console.Clear ();
-
-            //Disable alternative screen buffer.
-            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
-
-            //Set cursor key to cursor.
-            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
-
-            Platform.Suspend ();
-
-            //Enable alternative screen buffer.
-            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
-        }
-        catch (Exception ex)
-        {
-            Logging.Error ($"Error suspending terminal: {ex.Message}");
-        }
-
-        Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
-    }
+    public void AddRune (char c) { OutputBuffer.AddRune (c); }
 
     /// <inheritdoc/>
-    public void UpdateCursor () { _output.SetCursorPosition (Col, Row); }
+    public void AddStr (string str) { OutputBuffer.AddStr (str); }
 
     /// <inheritdoc/>
-    public void Init () { throw new NotSupportedException (); }
+    public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
 
     /// <inheritdoc/>
-    public void End ()
-    {
-        // TODO: Nope
-    }
+    public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); }
 
     /// <inheritdoc/>
     public Attribute SetAttribute (Attribute newAttribute)
@@ -346,35 +338,11 @@ internal class DriverImpl : IDriver
     /// <inheritdoc/>
     public Attribute GetAttribute () => OutputBuffer.CurrentAttribute;
 
-    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="IDriver.KeyUp"/>.</summary>
-    public event EventHandler<Key>? KeyDown;
-
-    /// <inheritdoc/>
-    public event EventHandler<Key>? KeyUp;
-
-    /// <summary>Event fired when a mouse event occurs.</summary>
-    public event EventHandler<MouseEventArgs>? MouseEvent;
-
     /// <inheritdoc/>
     public void WriteRaw (string ansi) { _output.Write (ansi); }
 
     /// <inheritdoc/>
-    public void EnqueueKeyEvent (Key key) { InputProcessor.EnqueueKeyDownEvent (key); }
-
-    /// <inheritdoc/>
-    public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (this, request); }
-
-    /// <inheritdoc/>
-    public AnsiRequestScheduler GetRequestScheduler () => _ansiRequestScheduler;
-
-    /// <inheritdoc/>
-    public void Refresh ()
-    {
-        // No need we will always draw when dirty
-    }
-
-    /// <inheritdoc/>
-    public string? GetName () => InputProcessor.DriverName?.ToLowerInvariant ();
+    public ConcurrentQueue<SixelToRender> GetSixels () => _output.GetSixels ();
 
     /// <inheritdoc/>
     public new string ToString ()
@@ -403,9 +371,59 @@ internal class DriverImpl : IDriver
         return sb.ToString ();
     }
 
-    /// <inheritdoc />
-    public string ToAnsi ()
+    /// <inheritdoc/>
+    public string ToAnsi () => _output.ToAnsi (OutputBuffer);
+
+    #endregion Drawing and Rendering
+
+    #region Cursor
+
+    private CursorVisibility _lastCursor = CursorVisibility.Default;
+
+    /// <inheritdoc/>
+    public void UpdateCursor () { _output.SetCursorPosition (Col, Row); }
+
+    /// <inheritdoc/>
+    public bool GetCursorVisibility (out CursorVisibility current)
     {
-        return _output.ToAnsi (OutputBuffer);
+        current = _lastCursor;
+
+        return true;
     }
+
+    /// <inheritdoc/>
+    public bool SetCursorVisibility (CursorVisibility visibility)
+    {
+        _lastCursor = visibility;
+        _output.SetCursorVisibility (visibility);
+
+        return true;
+    }
+
+    #endregion Cursor
+
+    #region Input Events
+
+    /// <summary>Event fired when a mouse event occurs.</summary>
+    public event EventHandler<MouseEventArgs>? MouseEvent;
+
+    /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="IDriver.KeyUp"/>.</summary>
+    public event EventHandler<Key>? KeyDown;
+
+    /// <inheritdoc/>
+    public event EventHandler<Key>? KeyUp;
+
+    /// <inheritdoc/>
+    public void EnqueueKeyEvent (Key key) { InputProcessor.EnqueueKeyDownEvent (key); }
+
+    #endregion Input Events
+
+    #region ANSI Escape Sequences
+
+    private readonly AnsiRequestScheduler _ansiRequestScheduler;
+
+    /// <inheritdoc/>
+    public virtual void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (this, request); }
+
+    #endregion ANSI Escape Sequences
 }

+ 16 - 5
Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs

@@ -86,10 +86,21 @@ public class FakeOutput : OutputBase, IOutput
     /// <inheritdoc/>
     protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle)
     {
-        if (Application.Force16Colors)
+        if (Force16Colors)
         {
-            output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
-            output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
+            if (!IsLegacyConsole)
+            {
+                output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
+                output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
+
+                EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style);
+            }
+            else
+            {
+                Write (output);
+                Console.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor16 ();
+                Console.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor16 ();
+            }
         }
         else
         {
@@ -106,9 +117,9 @@ public class FakeOutput : OutputBase, IOutput
                                                       attr.Background.G,
                                                       attr.Background.B
                                                      );
-        }
 
-        EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style);
+            EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style);
+        }
     }
 
     /// <inheritdoc/>

+ 164 - 123
Terminal.Gui/Drivers/IDriver.cs

@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
 
 namespace Terminal.Gui.Drivers;
 
@@ -5,92 +6,107 @@ namespace Terminal.Gui.Drivers;
 /// <remarks>
 ///     There are currently four implementations: UnixDriver, WindowsDriver, DotNetDriver, and FakeDriver
 /// </remarks>
-public interface IDriver
+public interface IDriver : IDisposable
 {
+    #region Driver Lifecycle
+
+    /// <summary>Initializes the driver</summary>
+    void Init ();
+
     /// <summary>
-    ///     Gets the name of the driver implementation.
+    ///     INTERNAL: Updates the terminal with the current output buffer. Should not be used by applications. Drawing occurs
+    ///     once each Application main loop iteration.
     /// </summary>
-    string? GetName ();
+    void Refresh ();
 
     /// <summary>
-    ///     Class responsible for processing native driver input objects
-    ///     e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
-    ///     and detecting and processing ansi escape sequences.
+    ///     Gets the name of the driver implementation.
     /// </summary>
-    IInputProcessor InputProcessor { get; }
+    string? GetName ();
+
+    /// <summary>Returns the name of the driver and relevant library version information.</summary>
+    /// <returns></returns>
+    string GetVersionInfo ();
+
+    /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
+    /// <remarks>This is only implemented in UnixDriver.</remarks>
+    void Suspend ();
 
     /// <summary>
-    ///     Describes the desired screen state. Data source for <see cref="IOutput"/>.
+    ///     Gets whether the driver has detected the console requires legacy console API (Windows Console API without ANSI/VT
+    ///     support).
+    ///     Returns <see langword="true"/> for legacy consoles that don't support modern ANSI escape sequences (e.g. Windows
+    ///     conhost);
+    ///     <see langword="false"/> for modern terminals with ANSI/VT support.
     /// </summary>
-    IOutputBuffer OutputBuffer { get; }
+    /// <remarks>
+    ///     <para>
+    ///         This property indicates whether the terminal supports modern ANSI escape sequences for input/output.
+    ///         On Windows, this maps to whether Virtual Terminal processing is enabled.
+    ///         On Unix-like systems, this is typically <see langword="false"/> as they support ANSI by default.
+    ///     </para>
+    ///     <para>
+    ///         When <see langword="true"/>, the driver must use legacy Windows Console API functions
+    ///         (e.g., WriteConsoleW, SetConsoleTextAttribute) instead of ANSI escape sequences.
+    ///     </para>
+    /// </remarks>
+    bool IsLegacyConsole { get; internal set; }
+
+    #endregion Driver Lifecycle
+
+    #region Driver Components
 
     /// <summary>
-    ///     Interface for classes responsible for reporting the current
-    ///     size of the terminal window.
+    ///     Class responsible for processing native driver input objects
+    ///     e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events
+    ///     and detecting and processing ansi escape sequences.
     /// </summary>
-    ISizeMonitor SizeMonitor { get; }
+    IInputProcessor InputProcessor { get; }
 
     /// <summary>Get the operating system clipboard.</summary>
-    /// 
     IClipboard? Clipboard { get; }
 
+    #endregion Driver Components
+
+    #region Screen and Display
+
     /// <summary>Gets the location and size of the terminal screen.</summary>
     Rectangle Screen { get; }
 
     /// <summary>
-    /// Sets the screen size for testing purposes. Only supported by FakeDriver.
-    /// <see cref="Screen"/> is the source of truth for screen dimensions.
+    ///     Sets the screen size. <see cref="Screen"/> is the source of truth for screen dimensions.
     /// </summary>
     /// <param name="width">The new width in columns.</param>
     /// <param name="height">The new height in rows.</param>
-    /// <exception cref="NotSupportedException">Thrown when called on non-FakeDriver instances.</exception>
     void SetScreenSize (int width, int height);
 
     /// <summary>
-    ///     Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
-    ///     to.
-    /// </summary>
-    /// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
-    Region? Clip { get; set; }
-
-
-    /// <summary>
-    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
-    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    ///     The event fired when the screen changes (size, position, etc.).
+    ///     <see cref="Screen"/> is the source of truth for screen dimensions.
     /// </summary>
-    int Col { get; }
+    event EventHandler<SizeChangedEventArgs>? SizeChanged;
 
     /// <summary>The number of columns visible in the terminal.</summary>
     int Cols { get; set; }
 
-    // BUGBUG: This should not be publicly settable.
-    /// <summary>
-    ///     Gets or sets the contents of the application output. The driver outputs this buffer to the terminal.
-    ///     <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
-    /// </summary>
-    Cell [,]? Contents { get; set; }
+    /// <summary>The number of rows visible in the terminal.</summary>
+    int Rows { get; set; }
 
     /// <summary>The leftmost column in the terminal.</summary>
     int Left { get; set; }
 
-    /// <summary>
-    ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
-    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
-    /// </summary>
-    int Row { get; }
-
-    /// <summary>The number of rows visible in the terminal.</summary>
-    int Rows { get; set; }
-
     /// <summary>The topmost row in the terminal.</summary>
     int Top { get; set; }
 
+    #endregion Screen and Display
+
+    #region Color Support
+
     /// <summary>Gets whether the <see cref="IDriver"/> supports TrueColor output.</summary>
     bool SupportsTrueColor { get; }
 
     /// <summary>
     ///     Gets or sets whether the <see cref="IDriver"/> should use 16 colors instead of the default TrueColors.
-    ///     See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>.
     /// </summary>
     /// <remarks>
     ///     <para>
@@ -98,42 +114,57 @@ public interface IDriver
     ///         <see langword="false"/>, indicating that the <see cref="IDriver"/> cannot support TrueColor.
     ///     </para>
     /// </remarks>
+    /// <seealso cref="Driver.Force16Colors"/>
     bool Force16Colors { get; set; }
 
+    #endregion Color Support
+
+    #region Content Buffer
+
+    // BUGBUG: This should not be publicly settable.
     /// <summary>
-    ///     The <see cref="System.Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/>
-    ///     call.
+    ///     Gets or sets the contents of the application output. The driver outputs this buffer to the terminal.
+    ///     <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks>
     /// </summary>
-    Attribute CurrentAttribute { get; set; }
+    Cell [,]? Contents { get; set; }
 
-    /// <summary>Returns the name of the driver and relevant library version information.</summary>
-    /// <returns></returns>
-    string GetVersionInfo ();
+    /// <summary>
+    ///     Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
+    ///     to.
+    /// </summary>
+    /// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
+    Region? Clip { get; set; }
+
+    /// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
+    void ClearContents ();
 
     /// <summary>
-    ///     Provide proper writing to send escape sequence recognized by the <see cref="IDriver"/>.
+    ///     Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/>
     /// </summary>
-    /// <param name="ansi"></param>
-    void WriteRaw (string ansi);
+    event EventHandler<EventArgs> ClearedContents;
 
-    /// <summary>Tests if the specified rune is supported by the driver.</summary>
-    /// <param name="rune"></param>
-    /// <returns>
-    ///     <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
-    ///     support displaying this rune.
-    /// </returns>
-    bool IsRuneSupported (Rune rune);
+    #endregion Content Buffer
 
-    /// <summary>Tests whether the specified coordinate are valid for drawing the specified Text.</summary>
-    /// <param name="text">Used to determine if one or two columns are required.</param>
-    /// <param name="col">The column.</param>
-    /// <param name="row">The row.</param>
-    /// <returns>
-    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of
-    ///     <see cref="IDriver.Clip"/>.
-    ///     <see langword="true"/> otherwise.
-    /// </returns>
-    bool IsValidLocation (string text, int col, int row);
+    #region Drawing and Rendering
+
+    /// <summary>
+    ///     Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    int Col { get; }
+
+    /// <summary>
+    ///     Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by
+    ///     <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
+    /// </summary>
+    int Row { get; }
+
+    /// <summary>
+    ///     The <see cref="System.Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or
+    ///     <see cref="AddStr"/>
+    ///     call.
+    /// </summary>
+    Attribute CurrentAttribute { get; set; }
 
     /// <summary>
     ///     Updates <see cref="IDriver.Col"/> and <see cref="IDriver.Row"/> to the specified column and row in
@@ -153,6 +184,25 @@ public interface IDriver
     /// <param name="row">Row to move to.</param>
     void Move (int col, int row);
 
+    /// <summary>Tests if the specified rune is supported by the driver.</summary>
+    /// <param name="rune"></param>
+    /// <returns>
+    ///     <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not
+    ///     support displaying this rune.
+    /// </returns>
+    bool IsRuneSupported (Rune rune);
+
+    /// <summary>Tests whether the specified coordinate are valid for drawing the specified Text.</summary>
+    /// <param name="text">Used to determine if one or two columns are required.</param>
+    /// <param name="col">The column.</param>
+    /// <param name="row">The row.</param>
+    /// <returns>
+    ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of
+    ///     <see cref="IDriver.Clip"/>.
+    ///     <see langword="true"/> otherwise.
+    /// </returns>
+    bool IsValidLocation (string text, int col, int row);
+
     /// <summary>Adds the specified rune to the display at the current cursor position.</summary>
     /// <remarks>
     ///     <para>
@@ -193,14 +243,6 @@ public interface IDriver
     /// <param name="str">String.</param>
     void AddStr (string str);
 
-    /// <summary>Clears the <see cref="IDriver.Contents"/> of the driver.</summary>
-    void ClearContents ();
-
-    /// <summary>
-    ///     Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/>
-    /// </summary>
-    event EventHandler<EventArgs> ClearedContents;
-
     /// <summary>Fills the specified rectangle with the specified rune, using <see cref="IDriver.CurrentAttribute"/></summary>
     /// <remarks>
     ///     The value of <see cref="IDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be
@@ -218,29 +260,43 @@ public interface IDriver
     /// <param name="c"></param>
     void FillRect (Rectangle rect, char c);
 
+    /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
+    /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
+    /// <param name="c">C.</param>
+    Attribute SetAttribute (Attribute c);
 
-    /// <summary>Gets the terminal cursor visibility.</summary>
-    /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
-    /// <returns><see langword="true"/> upon success</returns>
-    bool GetCursorVisibility (out CursorVisibility visibility);
+    /// <summary>Gets the current <see cref="Attribute"/>.</summary>
+    /// <returns>The current attribute.</returns>
+    Attribute GetAttribute ();
 
-    /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary>
-    void Refresh ();
+    /// <summary>
+    ///     Provide proper writing to send escape sequence recognized by the <see cref="IDriver"/>.
+    /// </summary>
+    /// <param name="ansi"></param>
+    void WriteRaw (string ansi);
 
-    /// <summary>Sets the terminal cursor visibility.</summary>
-    /// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
-    /// <returns><see langword="true"/> upon success</returns>
-    bool SetCursorVisibility (CursorVisibility visibility);
+    /// <summary>
+    ///     Gets the queue of sixel images to write out to screen when updating.
+    ///     If the terminal does not support Sixel, adding to this queue has no effect.
+    /// </summary>
+    ConcurrentQueue<SixelToRender> GetSixels ();
 
     /// <summary>
-    /// The event fired when the screen changes (size, position, etc.).
-    /// <see cref="Screen"/> is the source of truth for screen dimensions.
+    ///     Gets a string representation of <see cref="Contents"/>.
     /// </summary>
-    event EventHandler<SizeChangedEventArgs>? SizeChanged;
+    /// <returns></returns>
+    public string ToString ();
 
-    /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary>
-    /// <remarks>This is only implemented in UnixDriver.</remarks>
-    void Suspend ();
+    /// <summary>
+    ///     Gets an ANSI escape sequence representation of <see cref="Contents"/>. This is the
+    ///     same output as would be written to the terminal to recreate the current screen contents.
+    /// </summary>
+    /// <returns></returns>
+    public string ToAnsi ();
+
+    #endregion Drawing and Rendering
+
+    #region Cursor
 
     /// <summary>
     ///     Sets the position of the terminal cursor to <see cref="IDriver.Col"/> and
@@ -248,20 +304,19 @@ public interface IDriver
     /// </summary>
     void UpdateCursor ();
 
-    /// <summary>Initializes the driver</summary>
-    void Init ();
+    /// <summary>Gets the terminal cursor visibility.</summary>
+    /// <param name="visibility">The current <see cref="CursorVisibility"/></param>
+    /// <returns><see langword="true"/> upon success</returns>
+    bool GetCursorVisibility (out CursorVisibility visibility);
 
-    /// <summary>Ends the execution of the console driver.</summary>
-    void End ();
+    /// <summary>Sets the terminal cursor visibility.</summary>
+    /// <param name="visibility">The wished <see cref="CursorVisibility"/></param>
+    /// <returns><see langword="true"/> upon success</returns>
+    bool SetCursorVisibility (CursorVisibility visibility);
 
-    /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
-    /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
-    /// <param name="c">C.</param>
-    Attribute SetAttribute (Attribute c);
+    #endregion Cursor
 
-    /// <summary>Gets the current <see cref="Attribute"/>.</summary>
-    /// <returns>The current attribute.</returns>
-    Attribute GetAttribute ();
+    #region Input Events
 
     /// <summary>Event fired when a mouse event occurs.</summary>
     event EventHandler<MouseEventArgs>? MouseEvent;
@@ -283,29 +338,15 @@ public interface IDriver
     /// <param name="key"></param>
     void EnqueueKeyEvent (Key key);
 
+    #endregion Input Events
+
+    #region ANSI Escape Sequences
+
     /// <summary>
     ///     Queues the given <paramref name="request"/> for execution
     /// </summary>
     /// <param name="request"></param>
     public void QueueAnsiRequest (AnsiEscapeSequenceRequest request);
 
-    /// <summary>
-    ///     Gets the <see cref="AnsiRequestScheduler"/> for the driver
-    /// </summary>
-    /// <returns></returns>
-    public AnsiRequestScheduler GetRequestScheduler ();
-
-
-    /// <summary>
-    ///     Gets a string representation of <see cref="Contents"/>.
-    /// </summary>
-    /// <returns></returns>
-    public string ToString ();
-
-    /// <summary>
-    ///     Gets an ANSI escape sequence representation of <see cref="Contents"/>. This is the
-    ///     same output as would be written to the terminal to recreate the current screen contents.
-    /// </summary>
-    /// <returns></returns>
-    public string ToAnsi ();
+    #endregion ANSI Escape Sequences
 }

+ 13 - 2
Terminal.Gui/Drivers/IOutput.cs

@@ -1,4 +1,6 @@
-namespace Terminal.Gui.Drivers;
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui.Drivers;
 
 /// <summary>
 ///     The low-level interface drivers implement to provide output capabilities; encapsulates platform-specific
@@ -6,6 +8,15 @@
 /// </summary>
 public interface IOutput : IDisposable
 {
+    /// <seealso cref="IDriver.Force16Colors"/>
+    bool Force16Colors { get; set; }
+
+    /// <seealso cref="IDriver.IsLegacyConsole"/>
+    bool IsLegacyConsole { get; set; }
+
+    /// <seealso cref="IDriver.GetSixels"/>
+    ConcurrentQueue<SixelToRender> GetSixels ();
+
     /// <summary>
     ///     Gets the current position of the console cursor.
     /// </summary>
@@ -17,7 +28,7 @@ public interface IOutput : IDisposable
     ///     of characters not pixels).
     /// </summary>
     /// <returns></returns>
-    public Size GetSize ();
+    Size GetSize ();
 
     /// <summary>
     ///     Moves the console cursor to the given location.

+ 1 - 1
Terminal.Gui/Drivers/ISizeMonitor.cs

@@ -14,6 +14,6 @@ public interface ISizeMonitor
     ///     Examines the current size of the terminal and raises <see cref="SizeChanged"/> if it is different
     ///     from last inspection.
     /// </summary>
-    /// <returns></returns>
+    /// <returns><see langword="true"/> if the size has changed; otherwise, <see langword="false"/>.</returns>
     bool Poll ();
 }

+ 84 - 25
Terminal.Gui/Drivers/OutputBase.cs

@@ -1,3 +1,5 @@
+using System.Collections.Concurrent;
+
 namespace Terminal.Gui.Drivers;
 
 /// <summary>
@@ -5,7 +7,44 @@ namespace Terminal.Gui.Drivers;
 /// </summary>
 public abstract class OutputBase
 {
-    private CursorVisibility? _cachedCursorVisibility;
+    private bool _force16Colors;
+
+    /// <inheritdoc cref="IOutput.Force16Colors"/>
+    public bool Force16Colors
+    {
+        get => _force16Colors;
+        set
+        {
+            if (IsLegacyConsole && !value)
+            {
+                return;
+            }
+
+            _force16Colors = value;
+        }
+    }
+
+    private bool _isLegacyConsole;
+
+    /// <inheritdoc cref="IOutput.IsLegacyConsole"/>
+    public bool IsLegacyConsole
+    {
+        get => _isLegacyConsole;
+        set
+        {
+            _isLegacyConsole = value;
+
+            if (value) // If legacy console (true), force 16 colors
+            {
+                Force16Colors = true;
+            }
+        }
+    }
+
+    private readonly ConcurrentQueue<SixelToRender> _sixels = [];
+
+    /// <inheritdoc cref="IOutput.GetSixels"/>>
+    public ConcurrentQueue<SixelToRender> GetSixels () => _sixels;
 
     // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
     private TextStyle _redrawTextStyle = TextStyle.None;
@@ -28,7 +67,6 @@ public abstract class OutputBase
         Attribute? redrawAttr = null;
         int lastCol = -1;
 
-        CursorVisibility? savedVisibility = _cachedCursorVisibility;
         SetCursorVisibility (CursorVisibility.Invisible);
 
         for (int row = top; row < rows; row++)
@@ -63,6 +101,8 @@ public abstract class OutputBase
                             lastCol++;
                         }
 
+                        SetCursorPositionImpl (lastCol, row);
+
                         continue;
                     }
 
@@ -82,27 +122,41 @@ public abstract class OutputBase
 
             if (output.Length > 0)
             {
-                SetCursorPositionImpl (lastCol, row);
+                if (IsLegacyConsole)
+                {
+                    Write (output);
+                }
+                else
+                {
+                    SetCursorPositionImpl (lastCol, row);
+
+                    // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker
+                    StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output);
+                    Write (processed);
+                }
+            }
+        }
+
+        if (IsLegacyConsole)
+        {
+            return;
+        }
 
-                // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker
-                StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output);
-                Write (processed);
+        foreach (SixelToRender s in GetSixels ())
+        {
+            if (string.IsNullOrWhiteSpace (s.SixelData))
+            {
+                continue;
             }
+
+            SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y);
+            Write ((StringBuilder)new (s.SixelData));
         }
 
-        // BUGBUG: The Sixel impl depends on the legacy static Application object
-        // BUGBUG: Disabled for now
-        //foreach (SixelToRender s in  Application.Sixel)
-        //{
-        //    if (!string.IsNullOrWhiteSpace (s.SixelData))
-        //    {
-        //        SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y);
-        //        Console.Out.Write (s.SixelData);
-        //    }
-        //}
-
-        SetCursorVisibility (savedVisibility ?? CursorVisibility.Default);
-        _cachedCursorVisibility = savedVisibility;
+
+        // DO NOT restore cursor visibility here - let ApplicationMainLoop.SetCursor() handle it
+        // The old code was saving/restoring visibility which caused flickering because
+        // it would restore to the old value even if the application wanted it hidden
     }
 
     /// <summary>
@@ -166,7 +220,7 @@ public abstract class OutputBase
                     continue;
                 }
 
-                Cell cell = buffer.Contents![row, col];
+                Cell cell = buffer.Contents! [row, col];
                 AppendCellAnsi (cell, output, ref lastAttr, ref redrawTextStyle, endCol, ref col);
             }
 
@@ -228,11 +282,16 @@ public abstract class OutputBase
 
     private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
     {
-        SetCursorPositionImpl (lastCol, row);
-
-        // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker
-        StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output);
-        Write (processed);
+        if (IsLegacyConsole)
+        {
+            Write (output);
+        }
+        else
+        {
+            // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker
+            StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output);
+            Write (processed);
+        }
 
         output.Clear ();
         lastCol += outputWidth;

+ 1 - 1
Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs

@@ -39,7 +39,7 @@ internal class UnixOutput : OutputBase, IOutput
     /// <inheritdoc />
     protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle)
     {
-        if (Application.Force16Colors)
+        if (Force16Colors)
         {
             output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
             output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));

+ 75 - 99
Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs

@@ -97,13 +97,12 @@ internal partial class WindowsOutput : OutputBase, IOutput
     private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
     private readonly nint _outputHandle;
     private nint _screenBuffer;
-    private readonly bool _isVirtualTerminal;
     private readonly ConsoleColor _foreground;
     private readonly ConsoleColor _background;
 
     public WindowsOutput ()
     {
-        Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}");
+        Logging.Information ($"Creating {nameof (WindowsOutput)}");
 
         if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
         {
@@ -113,22 +112,9 @@ internal partial class WindowsOutput : OutputBase, IOutput
         // Get the standard output handle which is the current screen buffer.
         _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
         GetConsoleMode (_outputHandle, out uint mode);
-        _isVirtualTerminal = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
+        IsLegacyConsole = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0;
 
-        if (_isVirtualTerminal)
-        {
-            if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null)
-            {
-                //Enable alternative screen buffer.
-                Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
-            }
-            else
-            {
-                _foreground = Console.ForegroundColor;
-                _background = Console.BackgroundColor;
-            }
-        }
-        else
+        if (IsLegacyConsole)
         {
             CreateScreenBuffer ();
 
@@ -145,12 +131,19 @@ internal partial class WindowsOutput : OutputBase, IOutput
             {
                 throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}.");
             }
-
-            // Force 16 colors if not in virtual terminal mode.
-            // BUGBUG: This is bad. It does not work if the app was crated without
-            // BUGBUG: Apis.
-            //ApplicationImpl.Instance.Force16Colors = true;
-
+        }
+        else
+        {
+            if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null)
+            {
+                //Enable alternative screen buffer.
+                Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+            }
+            else
+            {
+                _foreground = Console.ForegroundColor;
+                _background = Console.BackgroundColor;
+            }
         }
 
         GetSize ();
@@ -189,7 +182,7 @@ internal partial class WindowsOutput : OutputBase, IOutput
             return;
         }
 
-        if (!WriteConsole (_isVirtualTerminal ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
+        if (!WriteConsole (!IsLegacyConsole ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
         {
             throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer.");
         }
@@ -220,19 +213,19 @@ internal partial class WindowsOutput : OutputBase, IOutput
         var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX ();
         csbi.cbSize = (uint)Marshal.SizeOf (csbi);
 
-        if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
+        if (!GetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi))
         {
             throw new Win32Exception (Marshal.GetLastWin32Error ());
         }
 
-        WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer);
+        WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (!IsLegacyConsole ? _outputHandle : _screenBuffer);
         short newCols = Math.Min (cols, maxWinSize.X);
         short newRows = Math.Min (rows, maxWinSize.Y);
         csbi.dwSize = new (newCols, Math.Max (newRows, (short)1));
         csbi.srWindow = new (0, 0, newCols, newRows);
         csbi.dwMaximumWindowSize = new (newCols, newRows);
 
-        if (!SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
+        if (!SetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi))
         {
             throw new Win32Exception (Marshal.GetLastWin32Error ());
         }
@@ -252,11 +245,11 @@ internal partial class WindowsOutput : OutputBase, IOutput
 
     private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi)
     {
-        if ((_isVirtualTerminal
+        if ((!IsLegacyConsole
                  ? _outputHandle
                  : _screenBuffer)
             != nint.Zero
-            && !SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
+            && !SetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi))
         {
             throw new Win32Exception (Marshal.GetLastWin32Error ());
         }
@@ -264,65 +257,52 @@ internal partial class WindowsOutput : OutputBase, IOutput
 
     public override void Write (IOutputBuffer outputBuffer)
     {
-        // BUGBUG: This is bad. It does not work if the app was crated without
-        // BUGBUG: Apis.
-        //_force16Colors = ApplicationImpl.Instance.Driver!.Force16Colors;
-        _force16Colors = false;
         _everythingStringBuilder.Clear ();
 
-        // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter.
+        // for 16 color mode we will write to a backing buffer, then flip it to the active one at the end to avoid jitter.
         _consoleBuffer = 0;
 
-        if (_force16Colors)
+        if (Force16Colors)
         {
-            if (_isVirtualTerminal)
-            {
-                _consoleBuffer = _outputHandle;
-            }
-            else
-            {
-                _consoleBuffer = _screenBuffer;
-            }
+            _consoleBuffer = !IsLegacyConsole ? _outputHandle : _screenBuffer;
         }
         else
         {
             _consoleBuffer = _outputHandle;
         }
 
-        base.Write (outputBuffer);
-
         try
         {
-            if (_force16Colors && !_isVirtualTerminal)
-            {
-                SetConsoleActiveScreenBuffer (_consoleBuffer);
-            }
-            else
+            base.Write (outputBuffer);
+
+            ReadOnlySpan<char> span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string
+
+            bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero);
+
+            if (!result)
             {
-                ReadOnlySpan<char> span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string
+                int err = Marshal.GetLastWin32Error ();
+
+                if (err == 1)
+                {
+                    Logging.Error ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}");
 
-                bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero);
+                    return;
+                }
 
-                if (!result)
+                if (err != 0)
                 {
-                    int err = Marshal.GetLastWin32Error ();
-
-                    if (err == 1)
-                    {
-                        Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}");
-
-                        return;
-                    }
-                    if (err != 0)
-                    {
-                        throw new Win32Exception (err);
-                    }
+                    throw new Win32Exception (err);
                 }
             }
         }
+        catch (DllNotFoundException)
+        {
+            // Running unit tests or in an environment where writing is not possible.
+        }
         catch (Exception e)
         {
-            Logging.Logger.LogError ($"Error: {e.Message} in {nameof (WindowsOutput)}");
+            Logging.Error ($"Error: {e.Message} in {nameof (WindowsOutput)}");
 
             if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
             {
@@ -341,7 +321,7 @@ internal partial class WindowsOutput : OutputBase, IOutput
 
         var str = output.ToString ();
 
-        if (_force16Colors && !_isVirtualTerminal)
+        if (Force16Colors && IsLegacyConsole)
         {
             char [] a = str.ToCharArray ();
             WriteConsole (_screenBuffer, a, (uint)a.Length, out _, nint.Zero);
@@ -355,23 +335,20 @@ internal partial class WindowsOutput : OutputBase, IOutput
     /// <inheritdoc/>
     protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle)
     {
-        // BUGBUG: This is bad. It does not work if the app was crated without
-        // BUGBUG: Apis.
-        // bool force16Colors = ApplicationImpl.Instance.Force16Colors;
-        bool force16Colors = false;
-
-        if (force16Colors)
+        if (Force16Colors)
         {
-            if (_isVirtualTerminal)
+            if (IsLegacyConsole)
             {
-                output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
-                output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
-                EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style);
+                Write (output);
+                output.Clear ();
+                var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4));
+                SetConsoleTextAttribute (_screenBuffer, as16ColorInt);
             }
             else
             {
-                var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4));
-                SetConsoleTextAttribute (_screenBuffer, as16ColorInt);
+                output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ()));
+                output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ()));
+                EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style);
             }
         }
         else
@@ -438,7 +415,7 @@ internal partial class WindowsOutput : OutputBase, IOutput
             var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX ();
             csbi.cbSize = (uint)Marshal.SizeOf (csbi);
 
-            if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi))
+            if (!GetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi))
             {
                 //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
                 cursorPosition = default (WindowsConsole.Coord);
@@ -468,7 +445,7 @@ internal partial class WindowsOutput : OutputBase, IOutput
 
         try
         {
-            maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer);
+            maxWinSize = GetLargestConsoleWindowSize (!IsLegacyConsole ? _outputHandle : _screenBuffer);
         }
         catch
         {
@@ -481,7 +458,7 @@ internal partial class WindowsOutput : OutputBase, IOutput
     /// <inheritdoc/>
     protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY)
     {
-        if (_force16Colors && !_isVirtualTerminal)
+        if (Force16Colors && IsLegacyConsole)
         {
             SetConsoleCursorPosition (_screenBuffer, new ((short)screenPositionX, (short)screenPositionY));
         }
@@ -505,7 +482,7 @@ internal partial class WindowsOutput : OutputBase, IOutput
             return;
         }
 
-        if (!_isVirtualTerminal)
+        if (IsLegacyConsole)
         {
             var info = new WindowsConsole.ConsoleCursorInfo
             {
@@ -539,15 +516,15 @@ internal partial class WindowsOutput : OutputBase, IOutput
 
         _lastCursorPosition = new (col, row);
 
-        if (_isVirtualTerminal)
+        if (IsLegacyConsole)
         {
-            var sb = new StringBuilder ();
-            EscSeqUtils.CSI_AppendCursorPosition (sb, row + 1, col + 1);
-            Write (sb.ToString ());
+            SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row));
         }
         else
         {
-            SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row));
+            var sb = new StringBuilder ();
+            EscSeqUtils.CSI_AppendCursorPosition (sb, row + 1, col + 1);
+            Write (sb.ToString ());
         }
     }
 
@@ -558,7 +535,6 @@ internal partial class WindowsOutput : OutputBase, IOutput
     }
 
     private bool _isDisposed;
-    private bool _force16Colors;
     private nint _consoleBuffer;
     private readonly StringBuilder _everythingStringBuilder = new ();
 
@@ -570,7 +546,16 @@ internal partial class WindowsOutput : OutputBase, IOutput
             return;
         }
 
-        if (_isVirtualTerminal)
+        if (IsLegacyConsole)
+        {
+            if (_screenBuffer != nint.Zero)
+            {
+                CloseHandle (_screenBuffer);
+            }
+
+            _screenBuffer = nint.Zero;
+        }
+        else
         {
             if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null)
             {
@@ -585,15 +570,6 @@ internal partial class WindowsOutput : OutputBase, IOutput
                 Console.Clear ();
             }
         }
-        else
-        {
-            if (_screenBuffer != nint.Zero)
-            {
-                CloseHandle (_screenBuffer);
-            }
-
-            _screenBuffer = nint.Zero;
-        }
 
         _isDisposed = true;
     }

+ 5 - 5
Terminal.Gui/Resources/config.json

@@ -19,8 +19,8 @@
   // --------------- Application Settings ---------------
   "Key.Separator": "+",
 
+  "Driver.Force16Colors": false,
   "Application.ArrangeKey": "Ctrl+F5",
-  "Application.Force16Colors": false,
   //"Application.ForceDriver": "", // TODO: ForceDriver should be nullable
   "Application.IsMouseDisabled": false,
   "Application.NextTabGroupKey": "F6",
@@ -136,14 +136,14 @@
                 "Foreground": "White",
                 "Background": "DarkBlue"
               }
-            },
+            }
           },
           {
             "Dialog": {
               "Normal": {
                 "Foreground": "BrightBlue",
                 "Background": "LightGray"
-              },
+              }
             }
           },
           {
@@ -152,7 +152,7 @@
                 "Foreground": "White",
                 "Background": "Blue",
                 "Style": "Bold"
-              },
+              }
             }
           },
           {
@@ -161,7 +161,7 @@
                 "Foreground": "Red",
                 "Background": "WhiteSmoke",
                 "Style": "Italic"
-              },
+              }
             }
           }
         ],

+ 76 - 26
Terminal.Gui/Text/TextFormatter.cs

@@ -123,11 +123,31 @@ public class TextFormatter
             }
 
             string strings = linesFormatted [line];
-            string[] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
+            
+            // Use ArrayPool to avoid per-draw allocations
+            int estimatedCount = strings.Length + 10; // Add buffer for grapheme clusters
+            string [] graphemes = ArrayPool<string>.Shared.Rent (estimatedCount);
+            var graphemeCount = 0;
 
-            // When text is justified, we lost left or right, so we use the direction to align.
+            try
+            {
+                foreach (string grapheme in GraphemeHelper.GetGraphemes (strings))
+                {
+                    if (graphemeCount >= graphemes.Length)
+                    {
+                        // Need larger array (rare case for complex text)
+                        string [] larger = ArrayPool<string>.Shared.Rent (graphemes.Length * 2);
+                        Array.Copy (graphemes, larger, graphemeCount);
+                        ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
+                        graphemes = larger;
+                    }
+
+                    graphemes [graphemeCount++] = grapheme;
+                }
+
+                // When text is justified, we lost left or right, so we use the direction to align.
 
-            int x = 0, y = 0;
+                int x = 0, y = 0;
 
             // Horizontal Alignment
             if (Alignment is Alignment.End)
@@ -214,7 +234,7 @@ public class TextFormatter
             {
                 if (isVertical)
                 {
-                    y = screen.Bottom - graphemes.Length;
+                    y = screen.Bottom - graphemeCount;
                 }
                 else
                 {
@@ -250,7 +270,7 @@ public class TextFormatter
             {
                 if (isVertical)
                 {
-                    int s = (screen.Height - graphemes.Length) / 2;
+                    int s = (screen.Height - graphemeCount) / 2;
                     y = screen.Top + s;
                 }
                 else
@@ -292,17 +312,17 @@ public class TextFormatter
                         continue;
                     }
 
-                    if (!FillRemaining && idx > graphemes.Length - 1)
+                    if (!FillRemaining && idx > graphemeCount - 1)
                     {
                         break;
                     }
 
                     if ((!isVertical
                          && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
-                             || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))
+                             || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))
                         || (isVertical
                             && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
-                                || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))))
+                                || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))))
                     {
                         break;
                     }
@@ -317,7 +337,7 @@ public class TextFormatter
 
                 if (isVertical)
                 {
-                    if (idx >= 0 && idx < graphemes.Length)
+                    if (idx >= 0 && idx < graphemeCount)
                     {
                         text = graphemes [idx];
                     }
@@ -368,7 +388,7 @@ public class TextFormatter
                 {
                     driver?.Move (current, y);
 
-                    if (idx >= 0 && idx < graphemes.Length)
+                    if (idx >= 0 && idx < graphemeCount)
                     {
                         text = graphemes [idx];
                     }
@@ -428,15 +448,20 @@ public class TextFormatter
                     current += runeWidth;
                 }
 
-                int nextRuneWidth = idx + 1 > -1 && idx + 1 < graphemes.Length
+                int nextRuneWidth = idx + 1 > -1 && idx + 1 < graphemeCount
                                         ? graphemes [idx + 1].GetColumns ()
                                         : 0;
 
-                if (!isVertical && idx + 1 < graphemes.Length && current + nextRuneWidth > start + size)
+                if (!isVertical && idx + 1 < graphemeCount && current + nextRuneWidth > start + size)
                 {
                     break;
                 }
             }
+            }
+            finally
+            {
+                ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
+            }
         }
     }
 
@@ -931,10 +956,30 @@ public class TextFormatter
             }
 
             string strings = linesFormatted [line];
-            string [] graphemes = GraphemeHelper.GetGraphemes (strings).ToArray ();
+            
+            // Use ArrayPool to avoid per-line allocations
+            int estimatedCount = strings.Length + 10; // Add buffer for grapheme clusters
+            string [] graphemes = ArrayPool<string>.Shared.Rent (estimatedCount);
+            var graphemeCount = 0;
 
-            // When text is justified, we lost left or right, so we use the direction to align.
-            int x = 0, y = 0;
+            try
+            {
+                foreach (string grapheme in GraphemeHelper.GetGraphemes (strings))
+                {
+                    if (graphemeCount >= graphemes.Length)
+                    {
+                        // Need larger array (rare case for complex text)
+                        string [] larger = ArrayPool<string>.Shared.Rent (graphemes.Length * 2);
+                        Array.Copy (graphemes, larger, graphemeCount);
+                        ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
+                        graphemes = larger;
+                    }
+
+                    graphemes [graphemeCount++] = grapheme;
+                }
+
+                // When text is justified, we lost left or right, so we use the direction to align.
+                int x = 0, y = 0;
 
             switch (Alignment)
             {
@@ -1011,7 +1056,7 @@ public class TextFormatter
             {
                 // Vertical Alignment
                 case Alignment.End when isVertical:
-                    y = screen.Bottom - graphemes.Length;
+                    y = screen.Bottom - graphemeCount;
 
                     break;
                 case Alignment.End:
@@ -1041,7 +1086,7 @@ public class TextFormatter
                     }
                 case Alignment.Center when isVertical:
                     {
-                        int s = (screen.Height - graphemes.Length) / 2;
+                        int s = (screen.Height - graphemeCount) / 2;
                         y = screen.Top + s;
 
                         break;
@@ -1081,22 +1126,22 @@ public class TextFormatter
                     continue;
                 }
 
-                if (!FillRemaining && idx > graphemes.Length - 1)
+                if (!FillRemaining && idx > graphemeCount - 1)
                 {
                     break;
                 }
 
                 if ((!isVertical
                      && (current - start > maxScreen.Left + maxScreen.Width - screen.X + colOffset
-                         || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width)))
+                         || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width)))
                     || (isVertical
                         && ((current > start + size + zeroLengthCount && idx > maxScreen.Top + maxScreen.Height - screen.Y)
-                            || (idx < graphemes.Length && graphemes [idx].GetColumns () > screen.Width))))
+                            || (idx < graphemeCount && graphemes [idx].GetColumns () > screen.Width))))
                 {
                     break;
                 }
 
-                string text = idx >= 0 && idx < graphemes.Length ? graphemes [idx] : " ";
+                string text = idx >= 0 && idx < graphemeCount ? graphemes [idx] : " ";
                 int runeWidth = GetStringWidth (text, TabWidth);
 
                 if (isVertical)
@@ -1116,20 +1161,25 @@ public class TextFormatter
 
                 current += isVertical && runeWidth > 0 ? 1 : runeWidth;
 
-                int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemes.Length
+                int nextStringWidth = idx + 1 > -1 && idx + 1 < graphemeCount
                                         ? graphemes [idx + 1].GetColumns ()
                                         : 0;
 
-                if (!isVertical && idx + 1 < graphemes.Length && current + nextStringWidth > start + size)
+                if (!isVertical && idx + 1 < graphemeCount && current + nextStringWidth > start + size)
                 {
                     break;
                 }
             }
 
-            // Add the line's drawn region to the overall region
-            if (lineWidth > 0 && lineHeight > 0)
+                // Add the line's drawn region to the overall region
+                if (lineWidth > 0 && lineHeight > 0)
+                {
+                    drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
+                }
+            }
+            finally
             {
-                drawnRegion.Union (new Rectangle (lineX, lineY, lineWidth, lineHeight));
+                ArrayPool<string>.Shared.Return (graphemes, clearArray: true);
             }
         }
 

+ 2 - 2
Terminal.Gui/ViewBase/Adornment/Adornment.cs

@@ -88,7 +88,7 @@ public class Adornment : View, IDesignable
     protected override IApplication? GetApp () => Parent?.App;
 
     /// <inheritdoc />
-    protected override IDriver? GetDriver () => Parent?.Driver ?? base.GetDriver();
+    protected override IDriver? GetDriver () => Parent?.Driver ?? base.GetDriver ();
 
     // If a scheme is explicitly set, use that. Otherwise, use the scheme of the parent view.
     private Scheme? _scheme;
@@ -187,7 +187,7 @@ public class Adornment : View, IDesignable
             Thickness.Draw (Driver, ViewportToScreen (Viewport), Diagnostics, ToString ());
         }
 
-        NeedsDraw = true;
+        SetNeedsDraw ();
 
         return true;
     }

+ 1 - 3
Terminal.Gui/ViewBase/Adornment/Border.cs

@@ -241,7 +241,7 @@ public partial class Border : Adornment
 
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (Thickness == Thickness.Empty)
         {
@@ -539,8 +539,6 @@ public partial class Border : Adornment
         }
 
         return true;
-
-        ;
     }
 
     /// <summary>

+ 10 - 10
Terminal.Gui/ViewBase/Adornment/Margin.cs

@@ -1,5 +1,6 @@
 
 
+using System.Diagnostics;
 using System.Runtime.InteropServices;
 
 namespace Terminal.Gui.ViewBase;
@@ -62,7 +63,6 @@ public class Margin : Adornment
         }
     }
 
-    // PERFORMANCE: Margins are ALWAYS drawn. This may be an issue for apps that have a large number of views with shadows.
     /// <summary>
     ///     INTERNAL API - Draws the margins for the specified views. This is called by the <see cref="Application"/> on each
     ///     iteration of the main loop after all Views have been drawn.
@@ -75,21 +75,21 @@ public class Margin : Adornment
 
         while (stack.Count > 0)
         {
-            var view = stack.Pop ();
+            View view = stack.Pop ();
 
-            if (view.Margin?.GetCachedClip () != null)
+            if (view.Margin is { } margin && margin.Thickness != Thickness.Empty && margin.GetCachedClip () != null)
             {
-                view.Margin!.NeedsDraw = true;
+                margin.SetNeedsDraw ();
                 Region? saved = view.GetClip ();
-                view.SetClip (view.Margin!.GetCachedClip ());
-                view.Margin!.Draw (); 
+                view.SetClip (margin.GetCachedClip ());
+                margin.Draw ();
                 view.SetClip (saved);
-                view.Margin!.ClearCachedClip ();
+                margin.ClearCachedClip ();
             }
 
-            view.NeedsDraw = false;
+            view.ClearNeedsDraw ();
 
-            foreach (var subview in view.SubViews)
+            foreach (View subview in view.SubViews)
             {
                 stack.Push (subview);
             }
@@ -225,7 +225,7 @@ public class Margin : Adornment
             return;
         }
 
-        bool pressed = args.Value.HasFlag (MouseState.Pressed) && parent.HighlightStates.HasFlag(MouseState.Pressed);
+        bool pressed = args.Value.HasFlag (MouseState.Pressed) && parent.HighlightStates.HasFlag (MouseState.Pressed);
         bool pressedOutside = args.Value.HasFlag (MouseState.PressedOutside) && parent.HighlightStates.HasFlag (MouseState.PressedOutside); ;
 
         if (pressedOutside)

+ 1 - 1
Terminal.Gui/ViewBase/Adornment/ShadowView.cs

@@ -20,7 +20,7 @@ internal class ShadowView : View
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         switch (ShadowStyle)
         {

+ 58 - 6
Terminal.Gui/ViewBase/DrawContext.cs

@@ -1,10 +1,43 @@
-
-namespace Terminal.Gui.ViewBase;
+namespace Terminal.Gui.ViewBase;
 
 /// <summary>
 ///     Tracks the region that has been drawn during <see cref="View.Draw(DrawContext?)"/>. This is primarily
 ///     in support of <see cref="ViewportSettingsFlags.Transparent"/>.
 /// </summary>
+/// <remarks>
+///     <para>
+///         When a <see cref="View"/> has <see cref="ViewportSettingsFlags.Transparent"/> set, the <see cref="DrawContext"/>
+///         is used to track exactly which areas of the screen have been drawn to. After drawing is complete, these drawn
+///         regions are excluded from the clip region, allowing views beneath the transparent view to show through in
+///         the areas that were not drawn.
+///     </para>
+///     <para>
+///         All coordinates tracked by <see cref="DrawContext"/> are in <b>screen-relative coordinates</b>. When reporting
+///         drawn areas from within <see cref="View.OnDrawingContent(DrawContext?)"/>, use <see cref="View.ViewportToScreen(in Rectangle)"/>
+///         or <see cref="View.ContentToScreen(in Point)"/> to convert viewport-relative or content-relative coordinates to
+///         screen-relative coordinates before calling <see cref="AddDrawnRectangle"/> or <see cref="AddDrawnRegion"/>.
+///     </para>
+///     <para>
+///         Example of reporting a non-rectangular drawn region for transparency:
+///     </para>
+///     <code>
+///         protected override bool OnDrawingContent (DrawContext? context)
+///         {
+///             // Draw some content in viewport-relative coordinates
+///             Rectangle rect1 = new Rectangle (5, 5, 10, 3);
+///             Rectangle rect2 = new Rectangle (8, 8, 4, 7);
+///             FillRect (rect1, Glyphs.BlackCircle);
+///             FillRect (rect2, Glyphs.BlackCircle);
+///             
+///             // Report the drawn region in screen-relative coordinates
+///             Region drawnRegion = new Region (ViewportToScreen (rect1));
+///             drawnRegion.Union (ViewportToScreen (rect2));
+///             context?.AddDrawnRegion (drawnRegion);
+///             
+///             return true;
+///         }
+///     </code>
+/// </remarks>
 public class DrawContext
 {
     private readonly Region _drawnRegion = new Region ();
@@ -12,12 +45,20 @@ public class DrawContext
     /// <summary>
     /// Gets a copy of the region drawn so far in this context.
     /// </summary>
+    /// <remarks>
+    ///     The returned region contains all areas that have been reported as drawn via <see cref="AddDrawnRectangle"/>
+    ///     or <see cref="AddDrawnRegion"/>, in screen-relative coordinates.
+    /// </remarks>
     public Region GetDrawnRegion () => _drawnRegion.Clone ();
 
     /// <summary>
     /// Reports that a rectangle has been drawn.
     /// </summary>
-    /// <param name="rect">The rectangle that was drawn.</param>
+    /// <param name="rect">The rectangle that was drawn, in screen-relative coordinates.</param>
+    /// <remarks>
+    ///     When called from within <see cref="View.OnDrawingContent(DrawContext?)"/>, ensure the rectangle is in
+    ///     screen-relative coordinates by using <see cref="View.ViewportToScreen(in Rectangle)"/> or similar methods.
+    /// </remarks>
     public void AddDrawnRectangle (Rectangle rect)
     {
         _drawnRegion.Combine (rect, RegionOp.Union);
@@ -26,7 +67,18 @@ public class DrawContext
     /// <summary>
     /// Reports that a region has been drawn.
     /// </summary>
-    /// <param name="region">The region that was drawn.</param>
+    /// <param name="region">The region that was drawn, in screen-relative coordinates.</param>
+    /// <remarks>
+    ///     <para>
+    ///         This method is useful for reporting non-rectangular drawn areas, which is important for
+    ///         proper transparency support with <see cref="ViewportSettingsFlags.Transparent"/>.
+    ///     </para>
+    ///     <para>
+    ///         When called from within <see cref="View.OnDrawingContent(DrawContext?)"/>, ensure the region is in
+    ///         screen-relative coordinates by using <see cref="View.ViewportToScreen(in Rectangle)"/> to convert each
+    ///         rectangle in the region.
+    ///     </para>
+    /// </remarks>
     public void AddDrawnRegion (Region region)
     {
         _drawnRegion.Combine (region, RegionOp.Union);
@@ -36,7 +88,7 @@ public class DrawContext
     /// Clips (intersects) the drawn region with the specified rectangle.
     /// This modifies the internal drawn region directly.
     /// </summary>
-    /// <param name="clipRect">The clipping rectangle.</param>
+    /// <param name="clipRect">The clipping rectangle, in screen-relative coordinates.</param>
     public void ClipDrawnRegion (Rectangle clipRect)
     {
         _drawnRegion.Intersect (clipRect);
@@ -46,7 +98,7 @@ public class DrawContext
     /// Clips (intersects) the drawn region with the specified region.
     /// This modifies the internal drawn region directly.
     /// </summary>
-    /// <param name="clipRegion">The clipping region.</param>
+    /// <param name="clipRegion">The clipping region, in screen-relative coordinates.</param>
     public void ClipDrawnRegion (Region clipRegion)
     {
         _drawnRegion.Intersect (clipRegion);

+ 1 - 1
Terminal.Gui/ViewBase/View.Content.cs

@@ -335,7 +335,7 @@ public partial class View
     ///     </para>
     ///     <para>
     ///         Altering the Viewport Size will eventually (when the view is next laid out) cause the
-    ///         <see cref="Layout()"/> and <see cref="OnDrawingContent()"/> methods to be called.
+    ///         <see cref="Layout()"/> and <see cref="OnDrawingContent(DrawContext)"/> methods to be called.
     ///     </para>
     /// </remarks>
     public virtual Rectangle Viewport

+ 98 - 212
Terminal.Gui/ViewBase/View.Drawing.cs

@@ -28,8 +28,35 @@ public partial class View // Drawing APIs
             view.Draw (context);
         }
 
-        // Draw the margins (those with Shadows) last to ensure they are drawn on top of the content.
+        // Draw the margins last to ensure they are drawn on top of the content.
         Margin.DrawMargins (viewsArray);
+
+        // DrawMargins may have caused some views have NeedsDraw/NeedsSubViewDraw set; clear them all.
+        foreach (View view in viewsArray)
+        {
+            view.ClearNeedsDraw ();
+        }
+
+        // After all peer views have been drawn and cleared, we can now clear the SuperView's SubViewNeedsDraw flag.
+        // ClearNeedsDraw() does not clear SuperView.SubViewNeedsDraw (by design, to avoid premature clearing
+        // when siblings still need drawing), so we must do it here after ALL peers are processed.
+        // We only clear the flag if ALL the SuperView's subviews no longer need drawing.
+        View? lastSuperView = null;
+        foreach (View view in viewsArray)
+        {
+            if (view is not Adornment && view.SuperView is { } && view.SuperView != lastSuperView)
+            {
+                // Check if ANY subview of this SuperView still needs drawing
+                bool anySubViewNeedsDrawing = view.SuperView.InternalSubViews.Any (v => v.NeedsDraw || v.SubViewNeedsDraw);
+
+                if (!anySubViewNeedsDrawing)
+                {
+                    view.SuperView.SubViewNeedsDraw = false;
+                }
+
+                lastSuperView = view.SuperView;
+            }
+        }
     }
 
     /// <summary>
@@ -73,7 +100,7 @@ public partial class View // Drawing APIs
             originalClip = AddViewportToClip ();
 
             // If no context ...
-            context ??= new DrawContext ();
+            context ??= new ();
 
             SetAttributeForRole (Enabled ? VisualRole.Normal : VisualRole.Disabled);
             DoClearViewport (context);
@@ -136,7 +163,6 @@ public partial class View // Drawing APIs
 
         // ------------------------------------
         // 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 ();
 
         // ------------------------------------
@@ -154,7 +180,7 @@ public partial class View // Drawing APIs
     {
         // NOTE: We do not support subviews of Margin?
 
-        if (Border?.SubViews is { } && Border.Thickness != Thickness.Empty)
+        if (Border?.SubViews is { } && Border.Thickness != Thickness.Empty && Border.NeedsDraw)
         {
             // PERFORMANCE: Get the check for DrawIndicator out of this somehow.
             foreach (View subview in Border.SubViews.Where (v => v.Visible || v.Id == "DrawIndicator"))
@@ -172,7 +198,7 @@ public partial class View // Drawing APIs
             SetClip (saved);
         }
 
-        if (Padding?.SubViews is { } && Padding.Thickness != Thickness.Empty)
+        if (Padding?.SubViews is { } && Padding.Thickness != Thickness.Empty && Padding.NeedsDraw)
         {
             foreach (View subview in Padding.SubViews)
             {
@@ -206,7 +232,7 @@ public partial class View // Drawing APIs
         {
             Margin.NeedsLayout = false;
             Margin?.Thickness.Draw (Driver, FrameToScreen ());
-            Margin?.Parent?.SetSubViewNeedsDraw ();
+            Margin?.Parent?.SetSubViewNeedsDrawDownHierarchy ();
         }
 
         if (SubViewNeedsDraw)
@@ -253,7 +279,7 @@ public partial class View // Drawing APIs
 
         if (Margin is { } && Margin.Thickness != Thickness.Empty/* && Margin.ShadowStyle == ShadowStyle.None*/)
         {
-           //Margin?.Draw ();
+            //Margin?.Draw ();
         }
     }
 
@@ -288,7 +314,7 @@ public partial class View // Drawing APIs
 
     internal void DoClearViewport (DrawContext? context = null)
     {
-        if (ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) || OnClearingViewport ())
+        if (!NeedsDraw || ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) || OnClearingViewport ())
         {
             return;
         }
@@ -407,8 +433,8 @@ public partial class View // Drawing APIs
 
         DrawText (context);
 
-        OnDrewText();
-        DrewText?.Invoke(this, EventArgs.Empty);
+        OnDrewText ();
+        DrewText?.Invoke (this, EventArgs.Empty);
     }
 
     /// <summary>
@@ -447,7 +473,7 @@ public partial class View // Drawing APIs
 
         if (Driver is { })
         {
-            TextFormatter?.Draw (
+            TextFormatter.Draw (
                                  Driver,
                                  drawRect,
                                  HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal),
@@ -456,7 +482,7 @@ public partial class View // Drawing APIs
         }
 
         // We assume that the text has been drawn over the entire area; ensure that the subviews are redrawn.
-        SetSubViewNeedsDraw ();
+        SetSubViewNeedsDrawDownHierarchy ();
     }
 
     /// <summary>
@@ -468,17 +494,12 @@ public partial class View // Drawing APIs
     public event EventHandler? DrewText;
 
     #endregion DrawText
+
     #region DrawContent
 
     private void DoDrawContent (DrawContext? context = null)
     {
-        if (OnDrawingContent (context))
-        {
-            return;
-        }
-
-        // TODO: Upgrade all overrides of OnDrawingContent to use DrawContext and remove this override
-        if (OnDrawingContent ())
+        if (!NeedsDraw || OnDrawingContent (context))
         {
             return;
         }
@@ -499,20 +520,66 @@ public partial class View // Drawing APIs
     /// </summary>
     /// <param name="context">The draw context to report drawn areas to.</param>
     /// <returns><see langword="true"/> to stop further drawing content.</returns>
+    /// <remarks>
+    ///     <para>
+    ///         Override this method to draw custom content for your View.
+    ///     </para>
+    ///     <para>
+    ///         <b>Transparency Support:</b> If your View has <see cref="ViewportSettings"/> with <see cref="ViewportSettingsFlags.Transparent"/>
+    ///         set, you should report the exact regions you draw to via the <paramref name="context"/> parameter. This allows
+    ///         the transparency system to exclude only the drawn areas from the clip region, letting views beneath show through
+    ///         in the areas you didn't draw.
+    ///     </para>
+    ///     <para>
+    ///         Use <see cref="DrawContext.AddDrawnRectangle"/> for simple rectangular areas, or <see cref="DrawContext.AddDrawnRegion"/>
+    ///         for complex, non-rectangular shapes. All coordinates passed to these methods must be in <b>screen-relative coordinates</b>.
+    ///         Use <see cref="View.ViewportToScreen(in Rectangle)"/> or <see cref="View.ContentToScreen(in Point)"/> to convert from
+    ///         viewport-relative or content-relative coordinates.
+    ///     </para>
+    ///     <para>
+    ///         Example of drawing custom content with transparency support:
+    ///     </para>
+    ///     <code>
+    ///         protected override bool OnDrawingContent (DrawContext? context)
+    ///         {
+    ///             base.OnDrawingContent (context);
+    ///             
+    ///             // Draw content in viewport-relative coordinates
+    ///             Rectangle rect1 = new Rectangle (5, 5, 10, 3);
+    ///             Rectangle rect2 = new Rectangle (8, 8, 4, 7);
+    ///             FillRect (rect1, Glyphs.BlackCircle);
+    ///             FillRect (rect2, Glyphs.BlackCircle);
+    ///             
+    ///             // Report drawn region in screen-relative coordinates for transparency
+    ///             if (ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent))
+    ///             {
+    ///                 Region drawnRegion = new Region (ViewportToScreen (rect1));
+    ///                 drawnRegion.Union (ViewportToScreen (rect2));
+    ///                 context?.AddDrawnRegion (drawnRegion);
+    ///             }
+    ///             
+    ///             return true;
+    ///         }
+    ///     </code>
+    /// </remarks>
     protected virtual bool OnDrawingContent (DrawContext? context) { return false; }
 
-    /// <summary>
-    ///     Called when the View's content is to be drawn. The default implementation does nothing.
-    /// </summary>
-    /// <returns><see langword="true"/> to stop further drawing content.</returns>
-    protected virtual bool OnDrawingContent () { return false; }
-
     /// <summary>Raised when the View's content is to be drawn.</summary>
     /// <remarks>
-    ///     <para>Will be invoked before any subviews added with <see cref="Add(View)"/> have been drawn.</para>
     ///     <para>
-    ///         Rect provides the view-relative rectangle describing the currently visible viewport into the
-    ///         <see cref="View"/> .
+    ///         Subscribe to this event to draw custom content for the View. Use the drawing methods available on <see cref="View"/>
+    ///         such as <see cref="View.AddRune(int, int, Rune)"/>, <see cref="View.AddStr(string)"/>, and <see cref="View.FillRect(Rectangle, Rune)"/>.
+    ///     </para>
+    ///     <para>
+    ///         The event is invoked after <see cref="ClearingViewport"/> and <see cref="Text"/> have been drawn, but before any <see cref="SubViews"/> are drawn.
+    ///     </para>
+    ///     <para>
+    ///         <b>Transparency Support:</b> If the View has <see cref="ViewportSettings"/> with <see cref="ViewportSettingsFlags.Transparent"/>
+    ///         set, use the <see cref="DrawEventArgs.DrawContext"/> to report which areas were actually drawn. This enables proper transparency
+    ///         by excluding only the drawn areas from the clip region. See <see cref="DrawContext"/> for details on reporting drawn regions.
+    ///     </para>
+    ///     <para>
+    ///         The <see cref="DrawEventArgs.NewViewport"/> property provides the view-relative rectangle describing the currently visible viewport into the View.
     ///     </para>
     /// </remarks>
     public event EventHandler<DrawEventArgs>? DrawingContent;
@@ -523,7 +590,7 @@ public partial class View // Drawing APIs
 
     private void DoDrawSubViews (DrawContext? context = null)
     {
-        if (OnDrawingSubViews (context))
+        if (!NeedsDraw || OnDrawingSubViews (context))
         {
             return;
         }
@@ -589,7 +656,7 @@ public partial class View // Drawing APIs
             // TODO: HACK - This forcing of SetNeedsDraw with SuperViewRendersLineCanvas enables auto line join to work, but is brute force.
             if (view.SuperViewRendersLineCanvas || view.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent))
             {
-                view.SetNeedsDraw ();
+                //view.SetNeedsDraw ();
             }
             view.Draw (context);
 
@@ -607,7 +674,7 @@ public partial class View // Drawing APIs
 
     private void DoRenderLineCanvas ()
     {
-        if (OnRenderingLineCanvas ())
+        if (!NeedsDraw || OnRenderingLineCanvas ())
         {
             return;
         }
@@ -733,185 +800,4 @@ public partial class View // Drawing APIs
 
     #endregion DrawComplete
 
-    #region NeedsDraw
-
-    // TODO: Change NeedsDraw to use a Region instead of Rectangle
-    // TODO: Make _needsDrawRect nullable instead of relying on Empty
-    //      TODO: If null, it means ?
-    //      TODO: If Empty, it means no need to redraw
-    //      TODO: If not Empty, it means the region that needs to be redrawn
-    // The viewport-relative region that needs to be redrawn. Marked internal for unit tests.
-    internal Rectangle NeedsDrawRect { get; set; } = Rectangle.Empty;
-
-    /// <summary>Gets or sets whether the view needs to be redrawn.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         Will be <see langword="true"/> if the <see cref="NeedsLayout"/> property is <see langword="true"/> or if
-    ///         any part of the view's <see cref="Viewport"/> needs to be redrawn.
-    ///     </para>
-    ///     <para>
-    ///         Setting has no effect on <see cref="NeedsLayout"/>.
-    ///     </para>
-    /// </remarks>
-    public bool NeedsDraw
-    {
-        get => Visible && (NeedsDrawRect != Rectangle.Empty || Margin?.NeedsDraw == true || Border?.NeedsDraw == true || Padding?.NeedsDraw == true);
-        set
-        {
-            if (value)
-            {
-                SetNeedsDraw ();
-            }
-            else
-            {
-                ClearNeedsDraw ();
-            }
-        }
-    }
-
-    /// <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>
-    /// <remarks>
-    ///     If the view has not been initialized (<see cref="IsInitialized"/> is <see langword="false"/>), this method
-    ///     does nothing.
-    /// </remarks>
-    public void SetNeedsDraw ()
-    {
-        Rectangle viewport = Viewport;
-
-        if (!Visible || (NeedsDrawRect != Rectangle.Empty && viewport.IsEmpty))
-        {
-            // This handles the case where the view has not been initialized yet
-            return;
-        }
-
-        SetNeedsDraw (viewport);
-    }
-
-    /// <summary>Expands the area of this view needing to be redrawn to include <paramref name="viewPortRelativeRegion"/>.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         The location of <paramref name="viewPortRelativeRegion"/> is relative to the View's <see cref="Viewport"/>.
-    ///     </para>
-    ///     <para>
-    ///         If the view has not been initialized (<see cref="IsInitialized"/> is <see langword="false"/>), the area to be
-    ///         redrawn will be the <paramref name="viewPortRelativeRegion"/>.
-    ///     </para>
-    /// </remarks>
-    /// <param name="viewPortRelativeRegion">The <see cref="Viewport"/>relative region that needs to be redrawn.</param>
-    public void SetNeedsDraw (Rectangle viewPortRelativeRegion)
-    {
-        if (!Visible)
-        {
-            return;
-        }
-
-        if (NeedsDrawRect.IsEmpty)
-        {
-            NeedsDrawRect = viewPortRelativeRegion;
-        }
-        else
-        {
-            int x = Math.Min (Viewport.X, viewPortRelativeRegion.X);
-            int y = Math.Min (Viewport.Y, viewPortRelativeRegion.Y);
-            int w = Math.Max (Viewport.Width, viewPortRelativeRegion.Width);
-            int h = Math.Max (Viewport.Height, viewPortRelativeRegion.Height);
-            NeedsDrawRect = new (x, y, w, h);
-        }
-
-        // Do not set on Margin - it will be drawn in a separate pass.
-
-        if (Border is { } && Border.Thickness != Thickness.Empty)
-        {
-            Border?.SetNeedsDraw ();
-        }
-
-        if (Padding is { } && Padding.Thickness != Thickness.Empty)
-        {
-            Padding?.SetNeedsDraw ();
-        }
-
-        SuperView?.SetSubViewNeedsDraw ();
-
-        if (this is Adornment adornment)
-        {
-            adornment.Parent?.SetSubViewNeedsDraw ();
-        }
-
-        // There was multiple enumeration error here, so calling new snapshot collection - probably a stop gap
-        foreach (View subview in InternalSubViews.Snapshot ())
-        {
-            if (subview.Frame.IntersectsWith (viewPortRelativeRegion))
-            {
-                Rectangle subviewRegion = Rectangle.Intersect (subview.Frame, viewPortRelativeRegion);
-                subviewRegion.X -= subview.Frame.X;
-                subviewRegion.Y -= subview.Frame.Y;
-                subview.SetNeedsDraw (subviewRegion);
-            }
-        }
-    }
-
-    /// <summary>Sets <see cref="SubViewNeedsDraw"/> to <see langword="true"/> for this View and all Superviews.</summary>
-    public void SetSubViewNeedsDraw ()
-    {
-        if (!Visible)
-        {
-            return;
-        }
-
-        SubViewNeedsDraw = true;
-
-        if (this is Adornment adornment)
-        {
-            adornment.Parent?.SetSubViewNeedsDraw ();
-        }
-
-        if (SuperView is { SubViewNeedsDraw: false })
-        {
-            SuperView.SetSubViewNeedsDraw ();
-        }
-    }
-
-    /// <summary>Clears <see cref="NeedsDraw"/> and <see cref="SubViewNeedsDraw"/>.</summary>
-    protected void ClearNeedsDraw ()
-    {
-        NeedsDrawRect = Rectangle.Empty;
-        SubViewNeedsDraw = false;
-
-        if (Margin is { } && (Margin.Thickness != Thickness.Empty || Margin.SubViewNeedsDraw || Margin.NeedsDraw))
-        {
-            Margin?.ClearNeedsDraw ();
-        }
-
-        if (Border is { } && (Border.Thickness != Thickness.Empty || Border.SubViewNeedsDraw || Border.NeedsDraw))
-        {
-            Border?.ClearNeedsDraw ();
-        }
-
-        if (Padding is { } && (Padding.Thickness != Thickness.Empty || Padding.SubViewNeedsDraw || Padding.NeedsDraw))
-        {
-            Padding?.ClearNeedsDraw ();
-        }
-
-        // There was multiple enumeration error here, so calling new snapshot collection - probably a stop gap
-        foreach (View subview in InternalSubViews.Snapshot ())
-        {
-            subview.ClearNeedsDraw ();
-        }
-
-        if (SuperView is { })
-        {
-            SuperView.SubViewNeedsDraw = false;
-        }
-
-        // This ensures LineCanvas' get redrawn
-        if (!SuperViewRendersLineCanvas)
-        {
-            LineCanvas.Clear ();
-        }
-    }
-
-    #endregion NeedsDraw
 }

+ 168 - 0
Terminal.Gui/ViewBase/View.NeedsDraw.cs

@@ -0,0 +1,168 @@
+namespace Terminal.Gui.ViewBase;
+
+public partial class View
+{
+    // NOTE: NeedsDrawRect is not currently used to clip drawing to only the invalidated region.
+    //       It is only used within SetNeedsDraw to propagate redraw requests to subviews.
+    // NOTE: Consider changing NeedsDrawRect from Rectangle to Region for more precise invalidation
+    //       NeedsDraw is already efficiently cached via NeedsDrawRect. It checks:
+    //       1. NeedsDrawRect (cached by SetNeedsDraw/ClearNeedsDraw)
+    //       2. Adornment NeedsDraw flags (each cached separately)
+    /// <summary>
+    ///     INTERNAL: Gets the viewport-relative region that needs to be redrawn.
+    /// </summary>
+    internal Rectangle NeedsDrawRect { get; private set; } = Rectangle.Empty;
+
+    /// <summary>Gets whether the view needs to be redrawn.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         Will be <see langword="true"/> if the <see cref="NeedsLayout"/> property is <see langword="true"/> or if
+    ///         any part of the view's <see cref="Viewport"/> needs to be redrawn.
+    ///     </para>
+    /// </remarks>
+    public bool NeedsDraw => Visible && (NeedsDrawRect != Rectangle.Empty || Margin?.NeedsDraw == true || Border?.NeedsDraw == true || Padding?.NeedsDraw == true);
+
+    /// <summary>Sets <see cref="NeedsDraw"/> to <see langword="true"/> indicating the <see cref="Viewport"/> of this View needs to be redrawn.</summary>
+    /// <remarks>
+    ///     If the view is not visible (<see cref="Visible"/> is <see langword="false"/>), this method
+    ///     does nothing.
+    /// </remarks>
+    public void SetNeedsDraw ()
+    {
+        Rectangle viewport = Viewport;
+
+        if (!Visible || (NeedsDrawRect != Rectangle.Empty && viewport.IsEmpty))
+        {
+            // This handles the case where the view has not been initialized yet
+            return;
+        }
+
+        SetNeedsDraw (viewport);
+    }
+
+    /// <summary>Expands the area of this view needing to be redrawn to include <paramref name="viewPortRelativeRegion"/>.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         The location of <paramref name="viewPortRelativeRegion"/> is relative to the View's <see cref="Viewport"/>.
+    ///     </para>
+    ///     <para>
+    ///         If the view has not been initialized (<see cref="IsInitialized"/> is <see langword="false"/>), the area to be
+    ///         redrawn will be the <paramref name="viewPortRelativeRegion"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="viewPortRelativeRegion">The <see cref="Viewport"/>relative region that needs to be redrawn.</param>
+    public void SetNeedsDraw (Rectangle viewPortRelativeRegion)
+    {
+        if (!Visible)
+        {
+            return;
+        }
+
+        if (NeedsDrawRect.IsEmpty)
+        {
+            NeedsDrawRect = viewPortRelativeRegion;
+        }
+        else
+        {
+            int x = Math.Min (Viewport.X, viewPortRelativeRegion.X);
+            int y = Math.Min (Viewport.Y, viewPortRelativeRegion.Y);
+            int w = Math.Max (Viewport.Width, viewPortRelativeRegion.Width);
+            int h = Math.Max (Viewport.Height, viewPortRelativeRegion.Height);
+            NeedsDrawRect = new (x, y, w, h);
+        }
+
+        // Do not set on Margin - it will be drawn in a separate pass.
+
+        if (Border is { } && Border.Thickness != Thickness.Empty)
+        {
+            Border?.SetNeedsDraw ();
+        }
+
+        if (Padding is { } && Padding.Thickness != Thickness.Empty)
+        {
+            Padding?.SetNeedsDraw ();
+        }
+
+        SuperView?.SetSubViewNeedsDrawDownHierarchy ();
+
+        if (this is Adornment adornment)
+        {
+            adornment.Parent?.SetSubViewNeedsDrawDownHierarchy ();
+        }
+
+        foreach (View subview in InternalSubViews.Snapshot ())
+        {
+            if (subview.Frame.IntersectsWith (viewPortRelativeRegion))
+            {
+                Rectangle subviewRegion = Rectangle.Intersect (subview.Frame, viewPortRelativeRegion);
+                subviewRegion.X -= subview.Frame.X;
+                subviewRegion.Y -= subview.Frame.Y;
+                subview.SetNeedsDraw (subviewRegion);
+            }
+        }
+    }
+
+    /// <summary>INTERNAL: Clears <see cref="NeedsDraw"/> and <see cref="SubViewNeedsDraw"/> for this view and all SubViews.</summary>
+    /// <remarks>
+    ///     See <see cref="SubViewNeedsDraw"/> is a cached value that is set when any subview or adornment requests a redraw.
+    ///     It may not always be in sync with the actual state of the subviews.
+    /// </remarks>
+    internal void ClearNeedsDraw ()
+    {
+        NeedsDrawRect = Rectangle.Empty;
+
+        Margin?.ClearNeedsDraw ();
+        Border?.ClearNeedsDraw ();
+        Padding?.ClearNeedsDraw ();
+
+        foreach (View subview in InternalSubViews.Snapshot ())
+        {
+            subview.ClearNeedsDraw ();
+        }
+
+        SubViewNeedsDraw = false;
+
+        // This ensures LineCanvas' get redrawn
+        if (!SuperViewRendersLineCanvas)
+        {
+            LineCanvas.Clear ();
+        }
+    }
+
+    // NOTE: SubViewNeedsDraw is decoupled from the actual state of the subviews (and adornments).
+    //       It is a performance optimization to avoid having to traverse all subviews and adornments to check if any need redraw.
+    //       As a result the code is fragile and can get out of sync; care must be taken to ensure it is set and cleared correctly.
+    /// <summary>
+    ///     INTERNAL: Gets whether any SubViews need to be redrawn.
+    /// </summary>
+    /// <remarks>
+    ///     See <see cref="SubViewNeedsDraw"/> is a cached value that is set when any subview or adornment requests a redraw.
+    ///     It may not always be in sync with the actual state of the subviews.
+    /// </remarks>
+    internal bool SubViewNeedsDraw { get; private set; }
+
+    /// <summary>INTERNAL: Sets <see cref="SubViewNeedsDraw"/> to <see langword="true"/> for this View and all Superviews.</summary>
+    /// <remarks>
+    ///     See <see cref="SubViewNeedsDraw"/> is a cached value that is set when any subview or adornment requests a redraw.
+    ///     It may not always be in sync with the actual state of the subviews.
+    /// </remarks>
+    internal void SetSubViewNeedsDrawDownHierarchy ()
+    {
+        if (!Visible)
+        {
+            return;
+        }
+
+        SubViewNeedsDraw = true;
+
+        if (this is Adornment adornment)
+        {
+            adornment.Parent?.SetSubViewNeedsDrawDownHierarchy ();
+        }
+
+        if (SuperView is { SubViewNeedsDraw: false })
+        {
+            SuperView.SetSubViewNeedsDrawDownHierarchy ();
+        }
+    }
+}

+ 1 - 1
Terminal.Gui/Views/Autocomplete/PopupAutocomplete.PopUp.cs

@@ -15,7 +15,7 @@ public abstract partial class PopupAutocomplete
 
         private readonly PopupAutocomplete _autoComplete;
 
-        protected override bool OnDrawingContent ()
+        protected override bool OnDrawingContent (DrawContext? context)
         {
             if (!_autoComplete.LastPopupPos.HasValue)
             {

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

@@ -580,7 +580,7 @@ public class CharMap : View, IDesignable
     private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1;
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (Viewport.Height == 0 || Viewport.Width == 0)
         {

+ 1 - 1
Terminal.Gui/Views/Color/ColorBar.cs

@@ -101,7 +101,7 @@ internal abstract class ColorBar : View, IColorBar
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (!string.IsNullOrWhiteSpace (Text))
         {

+ 1 - 1
Terminal.Gui/Views/Color/ColorPicker.16.cs

@@ -132,7 +132,7 @@ public class ColorPicker16 : View
     }
 
     ///<inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         SetAttribute (HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal));
         var colorIndex = 0;

+ 6 - 6
Terminal.Gui/Views/Color/ColorPicker.Prompt.cs

@@ -21,14 +21,14 @@ public partial class ColorPicker
         var d = new Dialog
         {
             Title = title,
-            Width = Application.Force16Colors ? 37 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)),
+            Width = app.Driver!.Force16Colors ? 37 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)),
             Height = 20
         };
 
         var btnOk = new Button
         {
             X = Pos.Center () - 5,
-            Y = Application.Force16Colors ? 6 : 4,
+            Y = app.Driver!.Force16Colors ? 6 : 4,
             Text = "Ok",
             Width = Dim.Auto (),
             IsDefault = true
@@ -63,7 +63,7 @@ public partial class ColorPicker
 
         View cpForeground;
 
-        if (Application.Force16Colors)
+        if (app.Driver!.Force16Colors)
         {
             cpForeground = new ColorPicker16
             {
@@ -88,7 +88,7 @@ public partial class ColorPicker
 
         View cpBackground;
 
-        if (Application.Force16Colors)
+        if (app.Driver!.Force16Colors)
         {
             cpBackground = new ColorPicker16
             {
@@ -117,8 +117,8 @@ public partial class ColorPicker
 
         app.Run (d);
         d.Dispose ();
-        Color newForeColor = Application.Force16Colors ? ((ColorPicker16)cpForeground).SelectedColor : ((ColorPicker)cpForeground).SelectedColor;
-        Color newBackColor = Application.Force16Colors ? ((ColorPicker16)cpBackground).SelectedColor : ((ColorPicker)cpBackground).SelectedColor;
+        Color newForeColor = app.Driver!.Force16Colors ? ((ColorPicker16)cpForeground).SelectedColor : ((ColorPicker)cpForeground).SelectedColor;
+        Color newBackColor = app.Driver!.Force16Colors ? ((ColorPicker16)cpBackground).SelectedColor : ((ColorPicker)cpBackground).SelectedColor;
         newAttribute = new (newForeColor, newBackColor);
         app.Dispose ();
         return accept;

+ 1 - 1
Terminal.Gui/Views/Color/ColorPicker.cs

@@ -99,7 +99,7 @@ public partial class ColorPicker : View, IDesignable
     public event EventHandler<ResultEventArgs<Color>>? ColorChanged;
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         Attribute normal = GetAttributeForRole (VisualRole.Normal);
         SetAttribute (new (SelectedColor, normal.Background, Enabled ? TextStyle.None : TextStyle.Faint));

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

@@ -287,7 +287,7 @@ public class ComboBox : View, IDesignable
     public virtual void OnCollapsed () { Collapsed?.Invoke (this, EventArgs.Empty); }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
 
         if (!_autoHide)
@@ -881,7 +881,7 @@ public class ComboBox : View, IDesignable
             return res;
         }
 
-        protected override bool OnDrawingContent ()
+        protected override bool OnDrawingContent (DrawContext context)
         {
             Attribute current = GetAttributeForRole (VisualRole.Focus);
             SetAttribute (current);

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

@@ -409,7 +409,7 @@ public class FileDialog : Dialog, IDesignable
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (!string.IsNullOrWhiteSpace (_feedback))
         {

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

@@ -198,7 +198,7 @@ public class GraphView : View, IDesignable
     }
 
     ///<inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (CellSize.X == 0 || CellSize.Y == 0)
         {

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

@@ -431,7 +431,7 @@ public class HexView : View, IDesignable
     }
 
     ///<inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (Source is null)
         {

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

@@ -217,7 +217,7 @@ public class Line : View, IOrientation
     ///     This method adds the line to the LineCanvas for rendering.
     ///     The actual rendering is performed by the parent view through <see cref="View.RenderLineCanvas"/>.
     /// </remarks>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         Point pos = ViewportToScreen (Viewport).Location;
         int length = Orientation == Orientation.Horizontal ? Frame.Width : Frame.Height;

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

@@ -721,11 +721,11 @@ public class ListView : View, IDesignable
     protected virtual void OnCollectionChanged (NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke (this, e); }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (Source is null)
         {
-            return base.OnDrawingContent ();
+            return base.OnDrawingContent (context);
         }
 
         var current = Attribute.Default;

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

@@ -132,7 +132,7 @@ public class ProgressBar : View, IDesignable
     }
 
     ///<inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         SetAttribute (GetAttributeForRole (VisualRole.Active));
 

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

@@ -391,7 +391,7 @@ public class Shortcut : View, IOrientation, IDesignable
     /// <example>
     ///     <para>
     ///         This example illustrates how to add a <see cref="Shortcut"/> to a <see cref="StatusBar"/> that toggles the
-    ///         <see cref="IApplication.Force16Colors"/> property.
+    ///         <see cref="IDriver.Force16Colors"/> property.
     ///     </para>
     ///     <code>
     ///     var force16ColorsShortcut = new Shortcut
@@ -406,8 +406,8 @@ public class Shortcut : View, IOrientation, IDesignable
     ///     cb.Toggled += (s, e) =>
     ///     {
     ///         var cb = s as CheckBox;
-    ///         Application.Force16Colors = cb!.Checked == true;
-    ///         Application.Refresh();
+    ///         App.Driver.Force16Colors = cb!.Checked == true;
+    ///         App.river.Refresh();
     ///     };
     ///     StatusBar.Add(force16ColorsShortcut);
     /// </code>

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

@@ -779,7 +779,7 @@ public class Slider<T> : View, IOrientation
     #region Drawing
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
         // TODO: make this more surgical to reduce repaint
 

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

@@ -172,7 +172,7 @@ public class SpinnerView : View, IDesignable
     protected override bool OnClearingViewport () { return true; }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         Render ();
 

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

@@ -931,7 +931,7 @@ public class TableView : View, IDesignable
     }
 
     ///<inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
         Move (0, 0);
 

+ 3 - 2
Terminal.Gui/Views/TextInput/TextField.cs

@@ -922,7 +922,7 @@ public class TextField : View, IDesignable
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
         _isDrawing = true;
 
@@ -1118,7 +1118,8 @@ public class TextField : View, IDesignable
                 break;
             }
 
-            int cols = _text [idx].GetColumns ();
+            int cols = Math.Max (_text [idx].GetColumns (), 1);
+
             TextModel.SetCol (ref col, Viewport.Width - 1, cols);
         }
 

+ 6 - 1
Terminal.Gui/Views/TextInput/TextModel.cs

@@ -616,7 +616,8 @@ internal class TextModel
         for (; i < tCount; i++)
         {
             string text = t [i];
-            size += text.GetColumns (false);
+            int colWidth = text.GetColumns (false);
+            size += colWidth;
             len += text.Length;
 
             if (text == "\t")
@@ -624,6 +625,10 @@ internal class TextModel
                 size += tabWidth + 1;
                 len += tabWidth - 1;
             }
+            else if (colWidth == -1)
+            {
+                size += 2; // -1+2=1
+            }
 
             if (checkNextRune && i == tCount - 1 && t.Count > tCount && IsWideText (t [i + 1], tabWidth, out int s, out int l))
             {

+ 1 - 1
Terminal.Gui/Views/TextInput/TextValidateField.cs

@@ -172,7 +172,7 @@ public class TextValidateField : View, IDesignable
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         if (_provider is null)
         {

+ 2 - 2
Terminal.Gui/Views/TextInput/TextView.cs

@@ -1781,7 +1781,7 @@ public class TextView : View, IDesignable
     }
 
     /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext? context)
     {
         _isDrawing = true;
 
@@ -3654,7 +3654,7 @@ public class TextView : View, IDesignable
     {
         List<Cell> currentLine = GetCurrentLine ();
 
-        if ((ReadOnly ? CurrentColumn + 1 : CurrentColumn) < currentLine.Count)
+        if (CurrentColumn < currentLine.Count)
         {
             CurrentColumn++;
         }

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

@@ -1148,7 +1148,7 @@ public class TreeView<T> : View, ITreeView where T : class
     public event EventHandler<ObjectActivatedEventArgs<T>> ObjectActivated;
 
     ///<inheritdoc/>
-    protected override bool OnDrawingContent ()
+    protected override bool OnDrawingContent (DrawContext context)
     {
         if (roots is null)
         {

+ 3 - 0
Terminal.sln.DotSettings

@@ -414,6 +414,9 @@
 	<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/GrammarAndSpelling/GrammarChecking/Exceptions/=Attribute_0020attribute/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=conhost/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Decscusr/@EntryIndexedValue">True</s:Boolean>
+	
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Gainsboro/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Gonek/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Guppie/@EntryIndexedValue">True</s:Boolean>

+ 1 - 1
Tests/UnitTests/Application/SynchronizatonContextTests.cs

@@ -26,7 +26,7 @@ public class SyncrhonizationContextTests
     [InlineData ("fake")]
     [InlineData ("windows")]
     [InlineData ("dotnet")]
-   // [InlineData ("unix")]
+    [InlineData ("unix")]
     public void SynchronizationContext_Post (string driverName = null)
     {
         lock (_lockPost)

+ 28 - 0
Tests/UnitTests/Configuration/SourcesManagerTests.cs

@@ -44,4 +44,32 @@ public class SourcesManagerTests
             ConfigurationManager.ThrowOnJsonErrors = false;
         }
     }
+
+
+    // NOTE: This test causes the static CM._jsonErrors to be modified; can't use in a parallel test
+    [Fact]
+    public void Load_WithInvalidJson_AddsJsonError ()
+    {
+        // Arrange
+        var sourcesManager = new SourcesManager ();
+
+        var settingsScope = new SettingsScope ();
+        var invalidJson = "{ invalid json }";
+        var stream = new MemoryStream ();
+        var writer = new StreamWriter (stream);
+        writer.Write (invalidJson);
+        writer.Flush ();
+        stream.Position = 0;
+
+        var source = "Load_WithInvalidJson_AddsJsonError";
+        var location = ConfigLocations.AppCurrent;
+
+        // Act
+        bool result = sourcesManager.Load (settingsScope, stream, source, location);
+
+        // Assert
+        Assert.False (result);
+
+        // Assuming AddJsonError logs errors, verify the error was logged (mock or inspect logs if possible).
+    }
 }

+ 1 - 1
Tests/UnitTests/FakeDriverBase.cs

@@ -4,7 +4,7 @@ namespace UnitTests;
 ///     Enables tests to create a FakeDriver for testing purposes.
 /// </summary>
 [Collection ("Global Test Setup")]
-public abstract class FakeDriverBase /*: IDisposable*/
+public abstract class FakeDriverBase/* : IDisposable*/
 {
     /// <summary>
     ///     Creates a new FakeDriver instance with the specified buffer size.

+ 1 - 1
Tests/UnitTests/View/Draw/DrawTests.cs

@@ -906,7 +906,7 @@ At 0,0
         public bool IsKeyUp { get; set; }
         public override string Text { get; set; } = null!;
 
-        protected override bool OnDrawingContent ()
+        protected override bool OnDrawingContent (DrawContext? context)
         {
             var idx = 0;
 

+ 1 - 1
Tests/UnitTests/Views/ComboBoxTests.cs

@@ -493,7 +493,7 @@ public class ComboBoxTests (ITestOutputHelper output)
         top.Dispose ();
     }
 
-    [Fact]
+    [Fact (Skip = "Disabled in #4431 to avoid noise; ComboBox will go away anyway")]
     [AutoInitShutdown]
     public void HideDropdownListOnClick_True_Highlight_Current_Item ()
     {

+ 3 - 4
Tests/UnitTestsParallelizable/Application/IApplicationScreenChangedTests.cs

@@ -383,7 +383,7 @@ public class IApplicationScreenChangedTests (ITestOutputHelper output)
     }
 
     [Fact]
-    public void Screen_Property_Setting_Does_Not_Fire_ScreenChanged_Event ()
+    public void Screen_Property_Setting_Raises_ScreenChanged_Event ()
     {
         // Arrange
         using IApplication app = Application.Create ();
@@ -397,11 +397,10 @@ public class IApplicationScreenChangedTests (ITestOutputHelper output)
 
         try
         {
-            // Act - Manually set Screen property (not via driver resize)
+            // Act - Manually set Screen property 
             app.Screen = new (0, 0, 100, 50);
 
-            // Assert - Event should not fire for manual property setting
-            Assert.False (eventFired);
+            Assert.True (eventFired);
             Assert.Equal (new (0, 0, 100, 50), app.Screen);
         }
         finally

+ 1 - 1
Tests/UnitTestsParallelizable/Application/NestedRunTimeoutTests.cs

@@ -164,7 +164,7 @@ public class NestedRunTimeoutTests (ITestOutputHelper output)
         var requestStopTimeoutFired = false;
 
         app.AddTimeout (
-                        TimeSpan.FromMilliseconds (5000),
+                        TimeSpan.FromMilliseconds (10000),
                         () =>
                         {
                             output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!");

+ 0 - 25
Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs

@@ -55,31 +55,6 @@ public class SourcesManagerTests
         Assert.Contains (source, sourcesManager.Sources.Values);
     }
 
-    [Fact]
-    public void Load_WithInvalidJson_AddsJsonError ()
-    {
-        // Arrange
-        var sourcesManager = new SourcesManager ();
-
-        var settingsScope = new SettingsScope ();
-        var invalidJson = "{ invalid json }";
-        var stream = new MemoryStream ();
-        var writer = new StreamWriter (stream);
-        writer.Write (invalidJson);
-        writer.Flush ();
-        stream.Position = 0;
-
-        var source = "Load_WithInvalidJson_AddsJsonError";
-        var location = ConfigLocations.AppCurrent;
-
-        // Act
-        bool result = sourcesManager.Load (settingsScope, stream, source, location);
-
-        // Assert
-        Assert.False (result);
-
-        // Assuming AddJsonError logs errors, verify the error was logged (mock or inspect logs if possible).
-    }
 
     #endregion
 

+ 2 - 2
Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs

@@ -141,7 +141,7 @@ public class AttributeTests : FakeDriverBase
         Assert.Equal (bg, attr.Foreground);
         Assert.Equal (bg, attr.Background);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]
@@ -273,7 +273,7 @@ public class AttributeTests : FakeDriverBase
         Assert.Equal (fg, attr.Foreground);
         Assert.Equal (bg, attr.Background);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]

+ 2 - 1
Tests/UnitTestsParallelizable/Drawing/CellTests.cs

@@ -23,6 +23,7 @@ public class CellTests
     [InlineData ("æ", new uint [] { 0x00E6 })]
     [InlineData ("a︠", new uint [] { 0x0061, 0xFE20 })]
     [InlineData ("e︡", new uint [] { 0x0065, 0xFE21 })]
+    [InlineData ("🇵🇹", new uint [] { 0x1F1F5, 0x1F1F9 })]
     public void Runes_From_Grapheme (string? grapheme, uint [] expected)
     {
         // Arrange
@@ -88,6 +89,7 @@ public class CellTests
         yield return ["👨‍👩‍👦‍👦", null, "[\"👨‍👩‍👦‍👦\":]"];
         yield return ["A", new Attribute (Color.Red) { Style = TextStyle.Blink }, "[\"A\":[Red,Red,Blink]]"];
         yield return ["\U0001F469\u200D\u2764\uFE0F\u200D\U0001F48B\u200D\U0001F468", null, "[\"👩‍❤️‍💋‍👨\":]"];
+        yield return ["\uD83C\uDDF5\uD83C\uDDF9", null, "[\"🇵🇹\":]"];
     }
 
     [Fact]
@@ -176,5 +178,4 @@ public class CellTests
         // And if your Grapheme setter normalizes, assignment should throw as well
         Assert.Throws<ArgumentException> (() => new Cell () { Grapheme = s });
     }
-
 }

+ 1 - 1
Tests/UnitTestsParallelizable/Drawing/Lines/StraightLineExtensionsTests.cs

@@ -1,7 +1,7 @@
 using UnitTests;
 using Xunit.Abstractions;
 
-namespace UnitTests.Parallelizable.Drawing.Lines;
+namespace DrawingTests.Lines;
 
 public class StraightLineExtensionsTests (ITestOutputHelper output)
 {

+ 1 - 1
Tests/UnitTestsParallelizable/Drawing/Lines/StraightLineTests.cs

@@ -1,6 +1,6 @@
 using Xunit.Abstractions;
 
-namespace UnitTests.Parallelizable.Drawing.Lines;
+namespace DrawingTests.Lines;
 
 public class StraightLineTests (ITestOutputHelper output)
 {

+ 115 - 5
Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs → Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs

@@ -37,7 +37,7 @@ public class SixelEncoderTests
         {
             for (var y = 0; y < 12; y++)
             {
-                pixels [x, y] = new (255, 0, 0);
+                pixels [x, y] = new (255, 0);
             }
         }
 
@@ -48,7 +48,7 @@ public class SixelEncoderTests
         // Since image is only red we should only have 1 color definition
         Color c1 = Assert.Single (encoder.Quantizer.Palette);
 
-        Assert.Equal (new (255, 0, 0), c1);
+        Assert.Equal (new (255, 0), c1);
         Assert.Equal (expected, result);
     }
 
@@ -124,7 +124,7 @@ public class SixelEncoderTests
                 // Create a 3x3 checkerboard by alternating the color based on pixel coordinates
                 if ((x / 3 + y / 3) % 2 == 0)
                 {
-                    pixels [x, y] = new (0, 0, 0); // Black
+                    pixels [x, y] = new (0, 0); // Black
                 }
                 else
                 {
@@ -142,7 +142,7 @@ public class SixelEncoderTests
         Color black = encoder.Quantizer.Palette.ElementAt (0);
         Color white = encoder.Quantizer.Palette.ElementAt (1);
 
-        Assert.Equal (new (0, 0, 0), black);
+        Assert.Equal (new (0, 0), black);
         Assert.Equal (new (255, 255, 255), white);
 
         // Compare the generated SIXEL string with the expected one
@@ -213,7 +213,7 @@ public class SixelEncoderTests
                 // For simplicity, we'll make every other row transparent
                 if (y % 2 == 0)
                 {
-                    pixels [x, y] = new (255, 0, 0); // Red pixel
+                    pixels [x, y] = new (255, 0); // Red pixel
                 }
                 else
                 {
@@ -229,4 +229,114 @@ public class SixelEncoderTests
         // Assert: Expect the result to match the expected sixel output
         Assert.Equal (expected, result);
     }
+
+    [Fact]
+    public void EncodeSixel_OnePixel_ReturnsExpectedSequence ()
+    {
+        // Arrange: 1x1 red pixel
+        Color [,] pixels = new Color [1, 1];
+        pixels [0, 0] = new (255, 0);
+
+        var encoder = new SixelEncoder ();
+
+        // Act
+        string result = encoder.EncodeSixel (pixels);
+
+        // Build expected output
+        string expected = "\u001bP" // start
+                          + "0;0;0"
+                          + "q"
+                          + "\"1;1;1;1" // no-scaling + width;height
+                          + "#0;2;100;0;0" // palette
+                          + "#0@$" // single column, single row -> code 1 -> char(1+63) = '@', then $ terminator
+                          + "\u001b\\";
+
+        Assert.Equal (expected, result);
+    }
+
+    [Fact]
+    public void EncodeSixel_WidthRepeat_UsesSequenceRepeatSyntax ()
+    {
+        // Arrange: width 5, height 1, all same color so sequence repeat > 3
+        int width = 5;
+        Color [,] pixels = new Color [width, 1];
+
+        for (var x = 0; x < width; x++)
+        {
+            pixels [x, 0] = new (255, 0);
+        }
+
+        var encoder = new SixelEncoder ();
+
+        // Act
+        string result = encoder.EncodeSixel (pixels);
+
+        // Assert contains the repeat sequence for 5 identical columns: "!5"
+        Assert.Contains ("!5", result);
+
+        // And final payload for the color should include the palette definition
+        Assert.Contains ("#0;2;100;0;0", result);
+    }
+
+    [Fact]
+    public void EncodeSixel_HeightNotMultipleOfSix_IncludesBandSeparator ()
+    {
+        // Arrange: width 2, height 7 to force two bands (6 rows + 1 row)
+        Color [,] pixels = new Color [2, 7];
+
+        for (var x = 0; x < 2; x++)
+        {
+            for (var y = 0; y < 7; y++)
+            {
+                pixels [x, y] = new (0, 0, 255);
+            }
+        }
+
+        var encoder = new SixelEncoder ();
+
+        // Act
+        string result = encoder.EncodeSixel (pixels);
+
+        // Assert: there must be a band separator '-' between the bands
+        Assert.Contains ("-", result);
+    }
+
+    [Fact]
+    public void EncodeSixel_AnyTransparentPixel_SetsTransparencyFlagInHeader ()
+    {
+        // Arrange: 2x2 with one fully transparent pixel
+        Color [,] pixels = new Color [2, 2];
+        pixels [0, 0] = new (255, 0);
+        pixels [0, 1] = new (0, 0, 0, 0); // fully transparent
+        pixels [1, 0] = new (0, 255);
+        pixels [1, 1] = new (0, 0, 255);
+
+        var encoder = new SixelEncoder ();
+
+        // Act
+        string result = encoder.EncodeSixel (pixels);
+
+        // defaultRatios should be "0;1;0" when any pixel has alpha == 0
+        Assert.Contains ("\u001bP0;1;0q", result);
+    }
+
+    [Fact]
+    public void EncodeSixel_MaxPaletteHonored_WhenReducedMaxColors ()
+    {
+        // Arrange: create three distinct colors but restrict max palette to 2
+        Color [,] pixels = new Color [3, 1];
+        pixels [0, 0] = new (255, 0);
+        pixels [1, 0] = new (0, 255);
+        pixels [2, 0] = new (0, 0, 255);
+
+        var encoder = new SixelEncoder ();
+        encoder.Quantizer.MaxColors = 2;
+
+        // Act
+        string result = encoder.EncodeSixel (pixels);
+
+        // Assert: palette count must respect MaxColors (<= 2) and encoding must not throw
+        Assert.True (encoder.Quantizer.Palette.Count <= 2);
+        Assert.False (string.IsNullOrEmpty (result));
+    }
 }

+ 228 - 0
Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs

@@ -0,0 +1,228 @@
+#nullable enable
+using Moq;
+
+namespace DrawingTests;
+
+public class SixelSupportDetectorTests
+{
+    [Fact]
+    public void Detect_SetsSupportedAndResolution_WhenDeviceAttributesContain4_AndResolutionResponds()
+    {
+        // Arrange
+        Mock<IDriver> driverMock = new (MockBehavior.Strict);
+
+        // Setup IsLegacyConsole - false means modern terminal with ANSI support
+        driverMock.Setup (d => d.IsLegacyConsole).Returns (false);
+
+        // Expect QueueAnsiRequest to be called at least twice:
+        // 1) CSI_SendDeviceAttributes (terminator "c")
+        // 2) CSI_RequestSixelResolution (terminator "t")
+        driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                  .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                      {
+                                                          // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4")
+                                                          if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request)
+                                                          {
+                                                              req.ResponseReceived.Invoke ("1;4;7c");
+                                                          }
+                                                          else if (req.Request == EscSeqUtils.CSI_RequestSixelResolution.Request)
+                                                          {
+                                                              // Reply with a resolution response matching regex "\[\d+;(\d+);(\d+)t$"
+                                                              // Group 1 -> ry, Group 2 -> rx. The detector constructs resolution as new(rx, ry)
+                                                              req.ResponseReceived.Invoke ("[6;20;10t");
+                                                          }
+                                                          else
+                                                          {
+                                                              // Any other request - call abandoned to avoid hanging
+                                                              req.Abandoned?.Invoke ();
+                                                          }
+                                                      })
+                  .Verifiable ();
+
+        var detector = new SixelSupportDetector (driverMock.Object);
+
+        SixelSupportResult? final = null;
+
+        // Act
+        detector.Detect (r => final = r);
+
+        // Assert
+        Assert.NotNull (final);
+        Assert.True (final.IsSupported); // Response contained "4"
+        // Resolution should be constructed as new(rx, ry) where rx=10, ry=20 from our reply "[6;20;10t"
+        Assert.Equal (10, final.Resolution.Width);
+        Assert.Equal (20, final.Resolution.Height);
+
+        driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()), Times.AtLeast (2));
+    }
+
+    [Fact]
+    public void Detect_DoesNotSetSupported_WhenDeviceAttributesDoNotContain4()
+    {
+        // Arrange
+        var driverMock = new Mock<IDriver>(MockBehavior.Strict);
+
+        // Setup IsLegacyConsole - false means modern terminal with ANSI support
+        driverMock.Setup (d => d.IsLegacyConsole).Returns (false);
+
+        driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                  .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                      {
+                                                          // SendDeviceAttributes -> reply without "4"
+                                                          if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request)
+                                                          {
+                                                              req.ResponseReceived.Invoke ("1;0;7c");
+                                                          }
+                                                          else
+                                                          {
+                                                              // Any other requests should be abandoned
+                                                              req.Abandoned?.Invoke ();
+                                                          }
+                                                      })
+                  .Verifiable ();
+
+        var detector = new SixelSupportDetector (driverMock.Object);
+
+        SixelSupportResult? final = null;
+
+        // Act
+        detector.Detect (r => final = r);
+
+        // Assert
+        Assert.NotNull (final);
+        Assert.False (final.IsSupported);
+        // On no support, the direct resolution request path isn't followed so resolution remains the default
+        Assert.Equal (10, final.Resolution.Width);
+        Assert.Equal (20, final.Resolution.Height);
+
+        driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()), Times.AtLeast (1));
+    }
+
+    [Theory]
+    [InlineData (true)]
+    [InlineData (false)]
+    public void Detect_SetsSupported_WhenIsLegacyConsoleIsFalseAndResponseContain4OrFalse (bool isLegacyConsole)
+    {
+        // Arrange
+        var responseReceived = false;
+        var output = new FakeOutput ();
+        output.IsLegacyConsole = isLegacyConsole;
+
+        Mock<DriverImpl> driverMock = new (
+                                           MockBehavior.Strict,
+                                           new FakeInputProcessor (null!),
+                                           new OutputBufferImpl (),
+                                           output,
+                                           new AnsiRequestScheduler (new AnsiResponseParser ()),
+                                           new SizeMonitorImpl (output)
+                                          );
+        driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                  .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                        {
+                                                            if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request)
+                                                            {
+                                                                responseReceived = true;
+
+                                                                if (!isLegacyConsole)
+                                                                {
+                                                                    // Response does contain "4" (so DAR indicates has sixel)
+                                                                    req.ResponseReceived.Invoke ("?1;4;0;7c");
+                                                                }
+                                                                else
+                                                                {
+                                                                    // Response does NOT contain "4" (so DAR indicates no sixel)
+                                                                    req.ResponseReceived.Invoke ("");
+                                                                }
+                                                            }
+                                                            else
+                                                            {
+                                                                // Abandon all requests
+                                                                req.Abandoned?.Invoke ();
+                                                            }
+                                                        })
+                  .Verifiable ();
+
+        var detector = new SixelSupportDetector (driverMock.Object);
+        SixelSupportResult? final = null;
+
+        // Act
+        detector.Detect (r => final = r);
+
+        // Assert
+        Assert.Equal (isLegacyConsole, driverMock.Object.IsLegacyConsole);
+        Assert.NotNull (final);
+
+        if (!isLegacyConsole)
+        {
+            Assert.True (final.IsSupported);
+        }
+        else
+        {
+            // Not a real VT, so should be supported
+            Assert.False (final.IsSupported);
+        }
+        Assert.True (responseReceived);
+        driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()), Times.AtLeast (1));
+    }
+
+    [Theory]
+    [InlineData (true)]
+    [InlineData (false)]
+    public void Detect_SetsSupported_WhenIsLegacyConsoleIsTrueOrFalse_With_Response (bool isLegacyConsole)
+    {
+        // Arrange
+        var responseReceived = false;
+        var output = new FakeOutput ();
+        output.IsLegacyConsole = isLegacyConsole;
+
+        Mock<DriverImpl> driverMock = new (
+                                           MockBehavior.Strict,
+                                           new FakeInputProcessor (null!),
+                                           new OutputBufferImpl (),
+                                           output,
+                                           new AnsiRequestScheduler (new AnsiResponseParser ()),
+                                           new SizeMonitorImpl (output)
+                                          );
+
+        driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                  .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                        {
+                                                            if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request)
+                                                            {
+                                                                responseReceived = true;
+
+                                                                // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4")
+                                                                // Respond to the SendDeviceAttributes request with an empty value that indicates non-support
+                                                                req.ResponseReceived.Invoke (!driverMock.Object.IsLegacyConsole ? "1;4;7c" : "");
+                                                            }
+
+                                                            // Abandon all requests
+                                                            req.Abandoned?.Invoke ();
+                                                        })
+                  .Verifiable ();
+
+        var detector = new SixelSupportDetector (driverMock.Object);
+        SixelSupportResult? final = null;
+
+        // Act
+        detector.Detect (r => final = r);
+
+        // Assert
+        Assert.Equal (isLegacyConsole, driverMock.Object.IsLegacyConsole);
+        Assert.NotNull (final);
+
+        if (!isLegacyConsole)
+        {
+            Assert.True (final.IsSupported);
+            Assert.True (final.SupportsTransparency);
+        }
+        else
+        {
+            // Not a real VT, so shouldn't be supported
+            Assert.False (final.IsSupported);
+            Assert.False (final.SupportsTransparency);
+        }
+
+        Assert.True (responseReceived);
+    }
+}

+ 62 - 0
Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs

@@ -0,0 +1,62 @@
+#nullable enable
+
+namespace DrawingTests;
+
+public class SixelSupportResultTests
+{
+    [Fact]
+    public void Defaults_AreCorrect ()
+    {
+        // Arrange & Act
+        var result = new SixelSupportResult ();
+
+        // Assert
+        Assert.False (result.IsSupported);
+        Assert.Equal (10, result.Resolution.Width);
+        Assert.Equal (20, result.Resolution.Height);
+        Assert.Equal (256, result.MaxPaletteColors);
+        Assert.False (result.SupportsTransparency);
+    }
+
+    [Fact]
+    public void Properties_CanBeModified ()
+    {
+        // Arrange
+        var result = new SixelSupportResult ();
+
+        // Act
+        result.IsSupported = true;
+        result.Resolution = new Size (24, 48);
+        result.MaxPaletteColors = 16;
+        result.SupportsTransparency = true;
+
+        // Assert
+        Assert.True (result.IsSupported);
+        Assert.Equal (24, result.Resolution.Width);
+        Assert.Equal (48, result.Resolution.Height);
+        Assert.Equal (16, result.MaxPaletteColors);
+        Assert.True (result.SupportsTransparency);
+    }
+
+    [Fact]
+    public void Resolution_IsValueType_CopyDoesNotAffectOriginal ()
+    {
+        // Arrange
+        var result = new SixelSupportResult ();
+        Size original = result.Resolution;
+
+        // Act
+        // Mutate a local copy and ensure original remains unchanged
+        Size copy = original;
+        copy.Width = 123;
+        copy.Height = 456;
+
+        // Assert
+        Assert.Equal (10, result.Resolution.Width);
+        Assert.Equal (20, result.Resolution.Height);
+        Assert.Equal (10, original.Width);
+        Assert.Equal (20, original.Height);
+        Assert.Equal (123, copy.Width);
+        Assert.Equal (456, copy.Height);
+    }
+}

+ 252 - 0
Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs

@@ -0,0 +1,252 @@
+#nullable enable
+using Moq;
+
+namespace DrawingTests;
+
+public class SixelToRenderTests
+{
+    [Fact]
+    public void SixelToRender_Properties_AreGettableAndSettable ()
+    {
+        SixelToRender s = new SixelToRender
+        {
+            SixelData = "SIXEL-DATA",
+            ScreenPosition = new (3, 5)
+        };
+
+        Assert.Equal ("SIXEL-DATA", s.SixelData);
+        Assert.Equal (3, s.ScreenPosition.X);
+        Assert.Equal (5, s.ScreenPosition.Y);
+    }
+
+    [Fact]
+    public void SixelSupportResult_DefaultValues_AreExpected ()
+    {
+        var r = new SixelSupportResult ();
+
+        Assert.False (r.IsSupported);
+        Assert.Equal (10, r.Resolution.Width);
+        Assert.Equal (20, r.Resolution.Height);
+        Assert.Equal (256, r.MaxPaletteColors);
+        Assert.False (r.SupportsTransparency);
+    }
+
+    [Fact]
+    public void Detect_WhenDeviceAttributesIndicateSupport_GetsResolutionDirectly ()
+    {
+        // Arrange
+        Mock<IDriver> driverMock = new (MockBehavior.Strict);
+
+        // Setup IsLegacyConsole - false means modern terminal with ANSI support
+        driverMock.Setup (d => d.IsLegacyConsole).Returns (false);
+
+        driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                  .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                      {
+                                                          if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request)
+                                                          {
+                                                              // Response contains "4" -> indicates sixel support
+                                                              req.ResponseReceived.Invoke ("?1;4;7c");
+                                                          }
+                                                          else if (req.Request == EscSeqUtils.CSI_RequestSixelResolution.Request)
+                                                          {
+                                                              // Return resolution: "[6;20;10t" (group1=20 -> ry, group2=10 -> rx)
+                                                              req.ResponseReceived.Invoke ("[6;20;10t");
+                                                          }
+                                                          else
+                                                          {
+                                                              req.Abandoned?.Invoke ();
+                                                          }
+                                                      })
+                  .Verifiable ();
+
+        var detector = new SixelSupportDetector (driverMock.Object);
+
+        SixelSupportResult? final = null;
+
+        // Act
+        detector.Detect (r => final = r);
+
+        // Assert
+        Assert.NotNull (final);
+        Assert.True (final.IsSupported);
+        Assert.Equal (10, final.Resolution.Width);
+        Assert.Equal (20, final.Resolution.Height);
+
+        driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()), Times.AtLeast (2));
+    }
+
+    [Fact]
+    public void Detect_WhenDirectResolutionFails_ComputesResolutionFromWindowSizes ()
+    {
+        // Arrange
+        Mock<IDriver> driverMock = new (MockBehavior.Strict);
+
+        // Setup IsLegacyConsole - false means modern terminal with ANSI support
+        driverMock.Setup (d => d.IsLegacyConsole).Returns (false);
+
+        driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                  .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                      {
+                                                          switch (req.Request)
+                                                          {
+                                                              case var r when r == EscSeqUtils.CSI_SendDeviceAttributes.Request:
+                                                                  // Indicate sixel support so flow continues to try resolution
+                                                                  req.ResponseReceived.Invoke ("?1;4;7c");
+                                                                  break;
+
+                                                              case var r when r == EscSeqUtils.CSI_RequestSixelResolution.Request:
+                                                                  // Simulate failure to return resolution directly
+                                                                  req.Abandoned?.Invoke ();
+                                                                  break;
+
+                                                              case var r when r == EscSeqUtils.CSI_RequestWindowSizeInPixels.Request:
+                                                                  // Pixel dimensions reply: [4;600;1200t  -> pixelHeight=600; pixelWidth=1200
+                                                                  req.ResponseReceived.Invoke ("[4;600;1200t");
+                                                                  break;
+
+                                                              case var r when r == EscSeqUtils.CSI_ReportWindowSizeInChars.Request:
+                                                                  // Character dimensions reply: [8;30;120t -> charHeight=30; charWidth=120
+                                                                  req.ResponseReceived.Invoke ("[8;30;120t");
+                                                                  break;
+
+                                                              default:
+                                                                  req.Abandoned?.Invoke ();
+                                                                  break;
+                                                          }
+                                                      })
+                  .Verifiable ();
+
+        var detector = new SixelSupportDetector (driverMock.Object);
+
+        SixelSupportResult? final = null;
+
+        // Act
+        detector.Detect (r => final = r);
+
+        // Assert
+        Assert.NotNull (final);
+        Assert.True (final.IsSupported);
+        // Expect cell width = round(1200 / 120) = 10, cell height = round(600 / 30) = 20
+        Assert.Equal (10, final.Resolution.Width);
+        Assert.Equal (20, final.Resolution.Height);
+
+        driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()), Times.AtLeast (3));
+    }
+
+    [Fact]
+    public void Detect_WhenDeviceAttributesDoNotIndicateSupport_ReturnsNotSupported ()
+    {
+        // Arrange
+        Mock<IDriver> driverMock = new (MockBehavior.Strict);
+
+        // Setup IsLegacyConsole - false means modern terminal with ANSI support
+        driverMock.Setup (d => d.IsLegacyConsole).Returns (false);
+
+        driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                  .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                      {
+                                                          if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request)
+                                                          {
+                                                              // Response does NOT contain "4"
+                                                              req.ResponseReceived.Invoke ("?1;0;7c");
+                                                          }
+                                                          else
+                                                          {
+                                                              req.Abandoned?.Invoke ();
+                                                          }
+                                                      })
+                  .Verifiable ();
+
+        var detector = new SixelSupportDetector (driverMock.Object);
+
+        SixelSupportResult? final = null;
+
+        // Act
+        detector.Detect (r => final = r);
+
+        // Assert
+        Assert.NotNull (final);
+        Assert.False (final.IsSupported);
+
+        driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()), Times.AtLeastOnce ());
+    }
+
+    [Theory]
+    [InlineData ("", true, false, false, false)]
+    [InlineData ("", true, true, false, false)]
+    [InlineData ("?1;0;7c", false, false, false, true)]
+    [InlineData ("?1;0;7c", false, true, false, true)]
+    [InlineData ("?1;4;0;7c", false, false, true, true)]
+    [InlineData ("?1;4;0;7c", false, true, true, true)]
+    public void Detect_WhenXtermEnvironmentIndicatesTransparency_SupportsTransparencyEvenIfDAReturnsNo4 (
+        string darResponse,
+        bool isLegacyConsole,
+        bool isXtermWithTransparency,
+        bool expectedIsSupported,
+        bool expectedSupportsTransparency
+    )
+    {
+        // Arrange - set XTERM_VERSION env var to indicate real xterm with transparency
+        string? prev = Environment.GetEnvironmentVariable ("XTERM_VERSION");
+
+        try
+        {
+            var output = new FakeOutput ();
+            output.IsLegacyConsole = isLegacyConsole;
+
+            Mock<DriverImpl> driverMock = new (
+                                               MockBehavior.Strict,
+                                               new FakeInputProcessor (null!),
+                                               new OutputBufferImpl (),
+                                               output,
+                                               new AnsiRequestScheduler (new AnsiResponseParser ()),
+                                               new SizeMonitorImpl (output)
+                                              );
+
+            driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()))
+                      .Callback<AnsiEscapeSequenceRequest> (req =>
+                                                          {
+                                                              if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request)
+                                                              {
+                                                                  // Response does NOT contain "4" (so DAR indicates no sixel)
+                                                                  req.ResponseReceived.Invoke (darResponse);
+                                                              }
+                                                              else
+                                                              {
+                                                                  req.Abandoned?.Invoke ();
+                                                              }
+                                                          })
+                      .Verifiable ();
+
+            var detector = new SixelSupportDetector (driverMock.Object);
+
+            SixelSupportResult? final = null;
+
+            if (isXtermWithTransparency)
+            {
+                Environment.SetEnvironmentVariable ("XTERM_VERSION", "370");
+            }
+
+            // Act
+            detector.Detect (r => final = r);
+
+            // Assert
+            Assert.NotNull (final);
+            Assert.Equal (isLegacyConsole, driverMock.Object.IsLegacyConsole);
+
+            // DAR did not indicate sixel support
+            Assert.Equal (expectedIsSupported, final.IsSupported);
+
+            // But because XTERM_VERSION >= 370 we expect SupportsTransparency to have been initially true and remain true
+            Assert.Equal (expectedSupportsTransparency, final.SupportsTransparency);
+
+            driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny<AnsiEscapeSequenceRequest> ()), Times.AtLeastOnce ());
+        }
+        finally
+        {
+            // Restore environment
+            Environment.SetEnvironmentVariable ("XTERM_VERSION", prev);
+        }
+    }
+}

+ 5 - 5
Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs

@@ -21,7 +21,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
         driver.AddRune (new Rune ('a'));
         Assert.Equal ("a", driver.Contents? [0, 0].Grapheme);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]
@@ -73,7 +73,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
         //		Application.Refresh ();
         //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
         //ắ", output);
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]
@@ -92,7 +92,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
             }
         }
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]
@@ -133,7 +133,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
             }
         }
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]
@@ -177,6 +177,6 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
         //	}
         //}
 
-        driver.End ();
+        driver.Dispose ();
     }
 }

+ 3 - 3
Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs

@@ -17,7 +17,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase
         driver.AddStr ("\u0301!"); // acute accent + exclamation mark
         DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]
@@ -66,7 +66,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase
         driver.AddStr (combined);
         DriverAssert.AssertDriverContentsAre (expected, output, driver);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Fact]
@@ -96,7 +96,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase
         driver.Move (500, 500);
         Assert.Equal (500, driver.Col);
         Assert.Equal (500, driver.Row);
-        driver.End ();
+        driver.Dispose ();
     }
 
     // TODO: Add these unit tests

+ 1 - 1
Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs

@@ -14,6 +14,6 @@ public class DriverColorTests : FakeDriverBase
         driver.Force16Colors = true;
         Assert.True (driver.Force16Colors);
 
-        driver.End ();
+        driver.Dispose ();
     }
 }

+ 1 - 1
Tests/UnitTestsParallelizable/Drivers/DriverTests.cs

@@ -47,7 +47,7 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase
         Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows - 1));
         Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows));
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Theory]

+ 54 - 0
Tests/UnitTestsParallelizable/Drivers/LegacyConsoleTests.cs

@@ -0,0 +1,54 @@
+#nullable enable
+using UnitTests;
+
+namespace DriverTests;
+
+public class LegacyConsoleTests : FakeDriverBase
+{
+    [Fact]
+    public void IsLegacyConsole_Returns_Expected_Values ()
+    {
+        IDriver? driver = CreateFakeDriver ();
+        Assert.False (driver.IsLegacyConsole);
+    }
+
+    [Fact]
+    public void IsLegacyConsole_False_Force16Colors_True_False ()
+    {
+        IDriver? driver = CreateFakeDriver ();
+
+        Assert.False (driver.IsLegacyConsole);
+        Assert.False (driver.Force16Colors);
+
+        driver.Force16Colors = true;
+        Assert.False (driver.IsLegacyConsole);
+        Assert.True (driver.Force16Colors);
+    }
+
+    [Fact]
+    public void IsLegacyConsole_True_Force16Colors_Is_Always_True ()
+    {
+        IDriver? driver = CreateFakeDriver ();
+
+        Assert.False (driver.IsLegacyConsole);
+        Assert.False (driver.Force16Colors);
+
+        driver.IsLegacyConsole = true;
+        Assert.True (driver.Force16Colors);
+
+        driver.Force16Colors = false;
+        Assert.True (driver.Force16Colors);
+    }
+
+    [Fact]
+    public void IsLegacyConsole_True_False_SupportsTrueColor_Is_Always_True_False ()
+    {
+        IDriver? driver = CreateFakeDriver ();
+
+        Assert.False (driver.IsLegacyConsole);
+        Assert.True (driver.SupportsTrueColor);
+
+        driver.IsLegacyConsole = true;
+        Assert.False (driver.SupportsTrueColor);
+    }
+}

+ 218 - 0
Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs

@@ -0,0 +1,218 @@
+#nullable enable
+
+namespace DriverTests;
+
+public class OutputBaseTests
+{
+    [Fact]
+    public void ToAnsi_SingleCell_NoAttribute_ReturnsGraphemeAndNewline ()
+    {
+        // Arrange
+        var output = new FakeOutput ();
+        IOutputBuffer buffer = output.LastBuffer!;
+        buffer.SetSize (1, 1);
+
+        // Act
+        buffer.AddStr ("A");
+        string ansi = output.ToAnsi (buffer);
+
+        // Assert: single grapheme plus newline (BuildAnsiForRegion appends a newline per row)
+        Assert.Contains ("A" + Environment.NewLine, ansi);
+    }
+
+    [Theory]
+    [InlineData (true, false)]
+    [InlineData (true, true)]
+    [InlineData (false, false)]
+    [InlineData (false, true)]
+    public void ToAnsi_WithAttribute_AppendsCorrectColorSequence_BasedOnIsLegacyConsole_And_Force16Colors (bool isLegacyConsole, bool force16Colors)
+    {
+        // Arrange
+        var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
+
+        // Create DriverImpl and associate it with the FakeOutput to test Sixel output
+        IDriver driver = new DriverImpl (
+                                 new FakeInputProcessor (null!),
+                                 new OutputBufferImpl (),
+                                 output,
+                                 new (new AnsiResponseParser ()),
+                                 new SizeMonitorImpl (output));
+
+        driver.Force16Colors = force16Colors;
+
+        IOutputBuffer buffer = output.LastBuffer!;
+        buffer.SetSize (1, 1);
+
+        // Use a known RGB color and attribute
+        var fg = new Color (1, 2, 3);
+        var bg = new Color (4, 5, 6);
+        buffer.CurrentAttribute = new Attribute (fg, bg);
+        buffer.AddStr ("X");
+
+        // Act
+        string ansi = output.ToAnsi (buffer);
+
+        // Assert: when true color expected, we should see the RGB CSI; otherwise we should see the 16-color CSI
+        if (!isLegacyConsole && !force16Colors)
+        {
+            Assert.Contains ("\u001b[38;2;1;2;3m", ansi);
+        }
+        else if (!isLegacyConsole && force16Colors)
+        {
+            var expected16 = EscSeqUtils.CSI_SetForegroundColor (fg.GetAnsiColorCode ());
+            Assert.Contains (expected16, ansi);
+        }
+        else
+        {
+            var expected16 = (ConsoleColor)fg.GetClosestNamedColor16 ();
+            Assert.Equal (ConsoleColor.Black, expected16);
+            Assert.DoesNotContain ('\u001b', ansi);
+        }
+
+        // Grapheme and newline should always be present
+        Assert.Contains ("X" + Environment.NewLine, ansi);
+    }
+
+    [Fact]
+    public void Write_WritesDirtyCellsAndClearsDirtyFlags ()
+    {
+        // Arrange
+        var output = new FakeOutput ();
+        IOutputBuffer buffer = output.LastBuffer!;
+        buffer.SetSize (2, 1);
+
+        // Mark two characters as dirty by writing them into the buffer
+        buffer.AddStr ("AB");
+
+        // Sanity: ensure cells are dirty before calling Write
+        Assert.True (buffer.Contents! [0, 0].IsDirty);
+        Assert.True (buffer.Contents! [0, 1].IsDirty);
+
+        // Act
+        output.Write (buffer); // calls OutputBase.Write via FakeOutput
+
+        // Assert: content was written to the fake output and dirty flags cleared
+        Assert.Contains ("AB", output.Output);
+        Assert.False (buffer.Contents! [0, 0].IsDirty);
+        Assert.False (buffer.Contents! [0, 1].IsDirty);
+    }
+
+    [Theory]
+    [InlineData (true)]
+    [InlineData (false)]
+    public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags (bool isLegacyConsole)
+    {
+        // Arrange
+        // FakeOutput exposes this because it's in test scope
+        var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
+        IOutputBuffer buffer = output.LastBuffer!;
+        buffer.SetSize (3, 1);
+
+        // Write 'A' at col 0 and 'C' at col 2; leave col 1 untouched (not dirty)
+        buffer.Move (0, 0);
+        buffer.AddStr ("A");
+        buffer.Move (2, 0);
+        buffer.AddStr ("C");
+
+        // Confirm some dirtiness before to write
+        Assert.True (buffer.Contents! [0, 0].IsDirty);
+        Assert.True (buffer.Contents! [0, 2].IsDirty);
+
+        // Act
+        output.Write (buffer);
+
+        // Assert: both characters were written (use Contains to avoid CI side effects)
+        Assert.Contains ("A", output.Output);
+        Assert.Contains ("C", output.Output);
+
+        // Dirty flags cleared for the written cells
+        Assert.False (buffer.Contents! [0, 0].IsDirty);
+        Assert.False (buffer.Contents! [0, 2].IsDirty);
+
+        // Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
+        Assert.Equal (new Point (0, 0), output.GetCursorPosition ());
+
+        // Now write 'X' at col 0 to verify subsequent writes also work
+        buffer.Move (0, 0);
+        buffer.AddStr ("X");
+
+        // Confirm dirtiness state before to write
+        Assert.True (buffer.Contents! [0, 0].IsDirty);
+        Assert.False (buffer.Contents! [0, 2].IsDirty);
+
+        output.Write (buffer);
+
+        // Assert: both characters were written (use Contains to avoid CI side effects)
+        Assert.Contains ("A", output.Output);
+        Assert.Contains ("C", output.Output);
+
+        // Dirty flags cleared for the written cells
+        Assert.False (buffer.Contents! [0, 0].IsDirty);
+        Assert.False (buffer.Contents! [0, 2].IsDirty);
+
+        // Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
+        Assert.Equal (new Point (2, 0), output.GetCursorPosition ());
+    }
+
+    [Theory]
+    [InlineData (true)]
+    [InlineData (false)]
+    public void Write_EmitsSixelDataAndPositionsCursor (bool isLegacyConsole)
+    {
+        // Arrange
+        var output = new FakeOutput ();
+        IOutputBuffer buffer = output.LastBuffer!;
+        buffer.SetSize (1, 1);
+
+        // Ensure the buffer has some content so Write traverses rows
+        buffer.AddStr (".");
+
+        // Create a Sixel to render
+        var s = new SixelToRender
+        {
+            SixelData = "SIXEL-DATA",
+            ScreenPosition = new Point (4, 2)
+        };
+
+        // Create DriverImpl and associate it with the FakeOutput to test Sixel output
+        IDriver driver = new DriverImpl (
+                                 new FakeInputProcessor (null!),
+                                 new OutputBufferImpl (),
+                                 output,
+                                 new (new AnsiResponseParser ()),
+                                 new SizeMonitorImpl (output));
+
+        // Add the Sixel to the driver
+        driver.GetSixels ().Enqueue (s);
+
+        // FakeOutput exposes this because it's in test scope
+        output.IsLegacyConsole = isLegacyConsole;
+
+        // Act
+        output.Write (buffer);
+
+        if (!isLegacyConsole)
+        {
+            // Assert: Sixel data was emitted (use Contains to avoid equality/side-effects)
+            Assert.Contains ("SIXEL-DATA", output.Output);
+
+            // Cursor was moved to Sixel position
+            Assert.Equal (s.ScreenPosition, output.GetCursorPosition ());
+        }
+        else
+        {
+            // Assert: Sixel data was NOT emitted
+            Assert.DoesNotContain ("SIXEL-DATA", output.Output);
+
+            // Cursor was NOT moved to Sixel position
+            Assert.NotEqual (s.ScreenPosition, output.GetCursorPosition ());
+        }
+
+        IApplication app = Application.Create ();
+        app.Driver = driver;
+
+        Assert.Equal (driver.GetSixels (), app.Driver.GetSixels ());
+
+        app.Dispose ();
+    }
+}

+ 20 - 20
Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs

@@ -34,9 +34,9 @@ public class ToAnsiTests : FakeDriverBase
         // Should contain the text
         Assert.Contains ("Hello", ansi);
         Assert.Contains ("World", ansi);
-        
+
         // Should have proper structure with newlines
-        string[] lines = ansi.Split (['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
+        string [] lines = ansi.Split (['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
         Assert.Equal (3, lines.Length);
     }
 
@@ -73,7 +73,7 @@ public class ToAnsiTests : FakeDriverBase
     public void ToAnsi_With_Background_Colors (bool force16Colors, string expected)
     {
         IDriver driver = CreateFakeDriver (10, 2);
-        Application.Force16Colors = force16Colors;
+        driver.Force16Colors = force16Colors;
 
         // Set background color
         driver.CurrentAttribute = new (Color.White, Color.Red);
@@ -204,13 +204,13 @@ public class ToAnsiTests : FakeDriverBase
         Assert.Equal (50, ansi.Count (c => c == '\n'));
     }
 
-    [Fact (Skip = "Use Application.")]
+    [Fact]
     public void ToAnsi_RGB_Colors ()
     {
         IDriver driver = CreateFakeDriver (10, 1);
 
         // Use RGB colors (when not forcing 16 colors)
-        Application.Force16Colors = false;
+        driver.Force16Colors = false;
         try
         {
             driver.CurrentAttribute = new Attribute (new Color (255, 0, 0), new Color (0, 255, 0));
@@ -224,17 +224,17 @@ public class ToAnsiTests : FakeDriverBase
         }
         finally
         {
-            Application.Force16Colors = true; // Reset
+            driver.Force16Colors = true; // Reset
         }
     }
 
-    [Fact (Skip = "Use Application.")]
+    [Fact]
     public void ToAnsi_Force16Colors ()
     {
         IDriver driver = CreateFakeDriver (10, 1);
 
         // Force 16 colors
-        Application.Force16Colors = true;
+        driver.Force16Colors = true;
         driver.CurrentAttribute = new Attribute (Color.Red, Color.Blue);
         driver.AddStr ("16Color");
 
@@ -268,15 +268,15 @@ public class ToAnsiTests : FakeDriverBase
         foreach (string colorName in colors)
         {
             Color fg = colorName switch
-                       {
-                           "Red" => Color.Red,
-                           "Green" => Color.Green,
-                           "Blue" => Color.Blue,
-                           "Yellow" => Color.Yellow,
-                           "Magenta" => Color.Magenta,
-                           "Cyan" => Color.Cyan,
-                           _ => Color.White
-                       };
+            {
+                "Red" => Color.Red,
+                "Green" => Color.Green,
+                "Blue" => Color.Blue,
+                "Yellow" => Color.Yellow,
+                "Magenta" => Color.Magenta,
+                "Cyan" => Color.Cyan,
+                _ => Color.White
+            };
 
             driver.CurrentAttribute = new (fg, Color.Black);
             driver.AddStr (colorName);
@@ -343,10 +343,10 @@ public class ToAnsiTests : FakeDriverBase
 
         string ansi = driver.ToAnsi ();
 
-        string[] lines = ansi.Split ('\n');
+        string [] lines = ansi.Split ('\n');
         Assert.Equal (4, lines.Length); // 3 content lines + 1 empty line at end
-        Assert.Contains ("First", lines[0]);
-        Assert.Contains ("Third", lines[2]);
+        Assert.Contains ("First", lines [0]);
+        Assert.Contains ("Third", lines [2]);
     }
 
     [Fact]

+ 3 - 3
Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs

@@ -269,15 +269,15 @@ public class WindowsKeyConverterTests
     #region ToKey Tests - OEM Keys
 
     [Theory]
-    [InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')]
+    //[InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')] // Keyboard layout dependent and shifted key is needed to produce ';' (Pt)
     [InlineData (':', ConsoleKey.Oem1, true, (KeyCode)':')]
-    [InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')]
+    //[InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')] // Keyboard layout dependent and shifted key is needed to produce '/' (Pt)
     [InlineData ('?', ConsoleKey.Oem2, true, (KeyCode)'?')]
     [InlineData (',', ConsoleKey.OemComma, false, (KeyCode)',')]
     [InlineData ('<', ConsoleKey.OemComma, true, (KeyCode)'<')]
     [InlineData ('.', ConsoleKey.OemPeriod, false, (KeyCode)'.')]
     [InlineData ('>', ConsoleKey.OemPeriod, true, (KeyCode)'>')]
-    [InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Un-shifted OemPlus is '='
+    //[InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Keyboard layout dependent and shifted key is needed to produce '=' (Pt)
     [InlineData ('+', ConsoleKey.OemPlus, true, (KeyCode)'+')] // Shifted OemPlus is '+'
     [InlineData ('-', ConsoleKey.OemMinus, false, (KeyCode)'-')]
     [InlineData ('_', ConsoleKey.OemMinus, true, (KeyCode)'_')] // Shifted OemMinus is '_'

+ 2 - 0
Tests/UnitTestsParallelizable/Text/StringTests.cs

@@ -77,6 +77,7 @@ public class StringTests
     [InlineData ("ힰ", 0, 1, 0)]    // U+D7B0 ힰ Hangul Jungseong O-Yeo
     [InlineData ("ᄀힰ", 2, 1, 2)]  // ᄀ U+1100 HANGUL CHOSEONG KIYEOK (consonant) with U+D7B0 ힰ Hangul Jungseong O-Yeo
     //[InlineData ("षि", 2, 1, 2)] // U+0937 ष DEVANAGARI LETTER SSA with U+093F ि COMBINING DEVANAGARI VOWEL SIGN I
+    [InlineData ("🇵🇹", 2, 1, 2)] // 🇵 U+1F1F5 — REGIONAL INDICATOR SYMBOL LETTER P with 🇹 U+1F1F9 — REGIONAL INDICATOR SYMBOL LETTER T (flag of Portugal)
     public void TestGetColumns_MultiRune_WideBMP_Graphemes (string str, int expectedRunesWidth, int expectedGraphemesCount, int expectedWidth)
     {
         Assert.Equal (expectedRunesWidth, str.EnumerateRunes ().Sum (r => r.GetColumns ()));
@@ -165,6 +166,7 @@ public class StringTests
         yield return [new [] { "👩‍", "🧒" }, "👩‍🧒"]; // Grapheme sequence
         yield return [new [] { "α", "β", "γ" }, "αβγ"]; // Unicode letters
         yield return [new [] { "A", null, "B" }, "AB"]; // Null ignored by string.Concat
+        yield return [new [] { "🇵", "🇹" }, "🇵🇹"]; // Grapheme sequence
     }
 
     [Theory]

+ 4 - 4
Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs

@@ -3121,7 +3121,7 @@ ssb
                  default (Rectangle));
         DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Theory]
@@ -3158,7 +3158,7 @@ ssb
                  default (Rectangle));
         DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Theory]
@@ -3196,7 +3196,7 @@ ssb
                  default (Rectangle));
         DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
 
-        driver.End ();
+        driver.Dispose ();
     }
 
     [Theory]
@@ -3233,6 +3233,6 @@ ssb
                  default (Rectangle));
         DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver);
 
-        driver.End ();
+        driver.Dispose ();
     }
 }

+ 1 - 1
Tests/UnitTestsParallelizable/ViewBase/Adornment/AdornmentTests.cs

@@ -10,7 +10,7 @@ public class AdornmentTests
     //    public bool PaddingDrawn { get; set; }
 
     //    /// <inheritdoc />
-    //    protected override bool OnDrawingContent () 
+    //    protected override bool OnDrawingContent (DrawContext? context) 
     //    {
     //        if (Border is { } && Border.Thickness != Thickness.Empty)
     //        {

+ 51 - 37
Tests/UnitTests/View/Draw/ClearViewportTests.cs → Tests/UnitTestsParallelizable/ViewBase/Draw/ClearViewportTests.cs

@@ -1,15 +1,19 @@
-#nullable enable
-using Moq;
+using Moq;
 using UnitTests;
 using Xunit.Abstractions;
 
-namespace UnitTests.ViewBaseTests;
+namespace ViewBaseTests.Viewport;
 
 [Trait ("Category", "Output")]
 public class ClearViewportTests (ITestOutputHelper output)
 {
     public class TestableView : View
     {
+        public TestableView ()
+        {
+            Frame = new Rectangle (0, 0, 10, 10);
+        }
+
         public bool TestOnClearingViewport () { return OnClearingViewport (); }
 
         public int OnClearingViewportCalled { get; set; }
@@ -76,6 +80,7 @@ public class ClearViewportTests (ITestOutputHelper output)
         Mock<TestableView> view = new () { CallBase = true };
 
         // Act
+        view.Object.SetNeedsDraw ();
         view.Object.DoClearViewport ();
 
         // Assert
@@ -91,6 +96,7 @@ public class ClearViewportTests (ITestOutputHelper output)
         view.Object.ClearingViewport += (sender, e) => eventRaised = true;
 
         // Act
+        view.Object.SetNeedsDraw ();
         view.Object.DoClearViewport ();
 
         // Assert
@@ -98,12 +104,13 @@ public class ClearViewportTests (ITestOutputHelper output)
     }
 
     [Fact]
-    [SetupFakeApplication]
     public void Clear_ClearsEntireViewport ()
     {
-        var superView = new View
+        using IApplication? app = Application.Create ();
+        app.Init ("Fake");
+
+        var superView = new Runnable
         {
-            App = ApplicationImpl.Instance,
             Width = Dim.Fill (), Height = Dim.Fill ()
         };
 
@@ -115,8 +122,7 @@ public class ClearViewportTests (ITestOutputHelper output)
             BorderStyle = LineStyle.Single
         };
         superView.Add (view);
-        superView.BeginInit ();
-        superView.EndInit ();
+        app.Begin (superView);
         superView.LayoutSubViews ();
         superView.Draw ();
 
@@ -125,7 +131,8 @@ public class ClearViewportTests (ITestOutputHelper output)
  ┌─┐
  │X│
  └─┘",
-                                                       output);
+                                                       output,
+                                                       app.Driver);
 
         // On Draw exit the view is excluded from the clip, so this will do nothing.
         view.ClearViewport ();
@@ -135,9 +142,11 @@ public class ClearViewportTests (ITestOutputHelper output)
  ┌─┐
  │X│
  └─┘",
-                                                       output);
+                                                       output,
+                                                       app.Driver);
 
-       view.SetClipToScreen ();
+
+        view.SetClipToScreen ();
 
         view.ClearViewport ();
 
@@ -146,16 +155,18 @@ public class ClearViewportTests (ITestOutputHelper output)
  ┌─┐
  │ │
  └─┘",
-                                                       output);
+                                                       output,
+                                                       app.Driver);
     }
 
     [Fact]
-    [SetupFakeApplication]
     public void Clear_WithClearVisibleContentOnly_ClearsVisibleContentOnly ()
     {
-        var superView = new View
+        using IApplication? app = Application.Create ();
+        app.Init ("Fake");
+
+        var superView = new Runnable
         {
-            App = ApplicationImpl.Instance,
             Width = Dim.Fill (), Height = Dim.Fill ()
         };
 
@@ -168,8 +179,7 @@ public class ClearViewportTests (ITestOutputHelper output)
             ViewportSettings = ViewportSettingsFlags.ClearContentOnly
         };
         superView.Add (view);
-        superView.BeginInit ();
-        superView.EndInit ();
+        app.Begin (superView);
         superView.LayoutSubViews ();
 
         superView.Draw ();
@@ -179,8 +189,9 @@ public class ClearViewportTests (ITestOutputHelper output)
  ┌─┐
  │X│
  └─┘",
-                                                       output);
-       view.SetClipToScreen ();
+                                                       output,
+                                                       app.Driver);
+        view.SetClipToScreen ();
         view.ClearViewport ();
 
         DriverAssert.AssertDriverContentsWithFrameAre (
@@ -188,14 +199,16 @@ public class ClearViewportTests (ITestOutputHelper output)
  ┌─┐
  │ │
  └─┘",
-                                                       output);
+                                                       output,
+                                                       app.Driver);
     }
 
     [Fact]
-    [AutoInitShutdown]
     public void Clear_Viewport_Can_Use_Driver_AddRune_Or_AddStr_Methods ()
     {
-        var view = new FrameView { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single };
+        using IApplication? app = Application.Create ();
+        app.Init ("Fake");
+        var view = new FrameView {  Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single };
 
         view.DrawingContent += (s, e) =>
                                {
@@ -203,11 +216,11 @@ public class ClearViewportTests (ITestOutputHelper output)
 
                                    for (var row = 0; row < view.Viewport.Height; row++)
                                    {
-                                       Application.Driver?.Move (1, row + 1);
+                                       app.Driver?.Move (1, row + 1);
 
                                        for (var col = 0; col < view.Viewport.Width; col++)
                                        {
-                                           Application.Driver?.AddStr ($"{col}");
+                                           app.Driver?.AddStr ($"{col}");
                                        }
                                    }
 
@@ -216,9 +229,9 @@ public class ClearViewportTests (ITestOutputHelper output)
                                };
         var top = new Runnable ();
         top.Add (view);
-        Application.Begin (top);
-        Application.Driver!.SetScreenSize (20, 10);
-        Application.LayoutAndDraw ();
+        app.Begin (top);
+        app.Driver!.SetScreenSize (20, 10);
+        app.LayoutAndDraw ();
 
         var expected = @"
 ┌──────────────────┐
@@ -234,7 +247,7 @@ public class ClearViewportTests (ITestOutputHelper output)
 "
             ;
 
-        Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output);
+        Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output, app.Driver);
         Assert.Equal (new (0, 0, 20, 10), pos);
 
         view.FillRect (view.Viewport);
@@ -253,14 +266,15 @@ public class ClearViewportTests (ITestOutputHelper output)
 "
             ;
 
-        pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output);
+        pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output, app.Driver);
         top.Dispose ();
     }
 
     [Fact]
-    [AutoInitShutdown]
     public void Clear_Can_Use_Driver_AddRune_Or_AddStr_Methods ()
     {
+        using IApplication? app = Application.Create ();
+        app.Init ("Fake");
         var view = new FrameView { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single };
 
         view.DrawingContent += (s, e) =>
@@ -269,11 +283,11 @@ public class ClearViewportTests (ITestOutputHelper output)
 
                                    for (var row = 0; row < view.Viewport.Height; row++)
                                    {
-                                       Application.Driver?.Move (1, row + 1);
+                                       app.Driver?.Move (1, row + 1);
 
                                        for (var col = 0; col < view.Viewport.Width; col++)
                                        {
-                                           Application.Driver?.AddStr ($"{col}");
+                                           app.Driver?.AddStr ($"{col}");
                                        }
                                    }
 
@@ -282,9 +296,9 @@ public class ClearViewportTests (ITestOutputHelper output)
                                };
         var top = new Runnable ();
         top.Add (view);
-        Application.Begin (top);
-        Application.Driver!.SetScreenSize (20, 10);
-        Application.LayoutAndDraw ();
+        app.Begin (top);
+        app.Driver!.SetScreenSize (20, 10);
+        app.LayoutAndDraw ();
 
         var expected = @"
 ┌──────────────────┐
@@ -300,7 +314,7 @@ public class ClearViewportTests (ITestOutputHelper output)
 "
             ;
 
-        Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output);
+        Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output, app.Driver);
         Assert.Equal (new (0, 0, 20, 10), pos);
 
         view.FillRect (view.Viewport);
@@ -318,7 +332,7 @@ public class ClearViewportTests (ITestOutputHelper output)
 └──────────────────┘
 ";
 
-        pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output);
+        pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output, app.Driver);
 
         top.Dispose ();
     }

+ 378 - 4
Tests/UnitTestsParallelizable/ViewBase/Draw/NeedsDrawTests.cs

@@ -69,7 +69,7 @@ public class NeedsDrawTests : FakeDriverBase
         view.BeginInit ();
         Assert.True (view.NeedsDraw);
 
-        view.NeedsDraw = false;
+        view.ClearNeedsDraw ();
 
         view.BeginInit ();
         Assert.False (view.NeedsDraw); // Because layout is still needed
@@ -94,7 +94,7 @@ public class NeedsDrawTests : FakeDriverBase
 
         view = new () { Width = 2, Height = 2, BorderStyle = LineStyle.Single };
         view.BeginInit ();
-        view.NeedsDraw = false;
+        view.ClearNeedsDraw ();
         view.EndInit ();
         Assert.True (view.NeedsDraw);
     }
@@ -145,7 +145,7 @@ public class NeedsDrawTests : FakeDriverBase
         Assert.True (view.NeedsDraw);
         Assert.False (view.NeedsLayout);
 
-        view.NeedsDraw = false;
+        view.ClearNeedsDraw ();
 
         // SRL won't change anything since the view frame wasn't changed. However, Layout has not been called
         view.SetRelativeLayout (new (10, 10));
@@ -199,7 +199,7 @@ public class NeedsDrawTests : FakeDriverBase
         superView.Layout ();
         Assert.True (superView.NeedsDraw);
 
-        superView.NeedsDraw = false;
+        superView.ClearNeedsDraw ();
         superView.SetRelativeLayout (new (10, 10));
         Assert.True (superView.NeedsDraw);
     }
@@ -311,4 +311,378 @@ public class NeedsDrawTests : FakeDriverBase
         Assert.Equal (new (1, 1, 5, 5), view.Viewport);
         Assert.Equal (new (1, 1, 5, 5), view.NeedsDrawRect);
     }
+
+    [Fact]
+    public void ClearNeedsDraw_ClearsOwnFlags ()
+    {
+        // Verify that ClearNeedsDraw properly clears the view's own flags
+        IDriver driver = CreateFakeDriver (80, 25);
+        driver.Clip = new Region (driver.Screen);
+
+        var view = new View
+        {
+            X = 0,
+            Y = 0,
+            Width = 20,
+            Height = 20,
+            Driver = driver
+        };
+        view.BeginInit ();
+        view.EndInit ();
+        view.LayoutSubViews ();
+
+        Assert.True (view.NeedsDraw);
+        Assert.Equal (view.Viewport, view.NeedsDrawRect);
+
+        view.Draw ();
+
+        Assert.False (view.NeedsDraw);
+        Assert.Equal (Rectangle.Empty, view.NeedsDrawRect);
+        Assert.False (view.SubViewNeedsDraw);
+    }
+
+    [Fact]
+    public void ClearNeedsDraw_ClearsAdornments ()
+    {
+        // Verify that ClearNeedsDraw clears adornment flags
+        IDriver driver = CreateFakeDriver (80, 25);
+        driver.Clip = new Region (driver.Screen);
+
+        var view = new View
+        {
+            X = 0,
+            Y = 0,
+            Width = 20,
+            Height = 20,
+            Driver = driver
+        };
+        view.Border!.Thickness = new Thickness (1);
+        view.Padding!.Thickness = new Thickness (1);
+        view.BeginInit ();
+        view.EndInit ();
+        view.LayoutSubViews ();
+
+        Assert.True (view.Border!.NeedsDraw);
+        Assert.True (view.Padding!.NeedsDraw);
+
+        view.Draw ();
+
+        Assert.False (view.Border!.NeedsDraw);
+        Assert.False (view.Padding!.NeedsDraw);
+    }
+
+    [Fact]
+    public void ClearNeedsDraw_PropagatesDownToAllSubViews ()
+    {
+        // Verify that ClearNeedsDraw clears flags on all descendants
+        IDriver driver = CreateFakeDriver (80, 25);
+        driver.Clip = new Region (driver.Screen);
+
+        var topView = new View
+        {
+            X = 0,
+            Y = 0,
+            Width = 100,
+            Height = 100,
+            Driver = driver
+        };
+
+        var middleView = new View { X = 10, Y = 10, Width = 50, Height = 50 };
+        var bottomView = new View { X = 5, Y = 5, Width = 20, Height = 20 };
+
+        topView.Add (middleView);
+        middleView.Add (bottomView);
+        topView.BeginInit ();
+        topView.EndInit ();
+        topView.LayoutSubViews ();
+
+        Assert.True (topView.NeedsDraw);
+        Assert.True (middleView.NeedsDraw);
+        Assert.True (bottomView.NeedsDraw);
+
+        topView.Draw ();
+
+        Assert.False (topView.NeedsDraw);
+        Assert.False (topView.SubViewNeedsDraw);
+        Assert.False (middleView.NeedsDraw);
+        Assert.False (middleView.SubViewNeedsDraw);
+        Assert.False (bottomView.NeedsDraw);
+    }
+
+    #region NeedsDraw Tests
+
+    [Fact]
+    public void NeedsDraw_InitiallyFalse_WhenNotVisible ()
+    {
+        var view = new View { Visible = false };
+        view.BeginInit ();
+        view.EndInit ();
+
+        Assert.False (view.NeedsDraw);
+    }
+
+    [Fact]
+    public void NeedsDraw_TrueAfterSetNeedsDraw ()
+    {
+        var view = new View { X = 0, Y = 0, Width = 10, Height = 10 };
+        view.BeginInit ();
+        view.EndInit ();
+        view.LayoutSubViews ();
+
+        view.SetNeedsDraw ();
+
+        Assert.True (view.NeedsDraw);
+    }
+
+    [Fact]
+    public void NeedsDraw_ClearedAfterDraw ()
+    {
+        IDriver driver = CreateFakeDriver (80, 25);
+        driver.Clip = new Region (driver.Screen);
+
+        var view = new View
+        {
+            X = 0,
+            Y = 0,
+            Width = 10,
+            Height = 10,
+            Driver = driver
+        };
+        view.BeginInit ();
+        view.EndInit ();
+        view.LayoutSubViews ();
+
+        view.SetNeedsDraw ();
+        Assert.True (view.NeedsDraw);
+
+        view.Draw ();
+
+        Assert.False (view.NeedsDraw);
+    }
+
+    [Fact]
+    public void SetNeedsDraw_WithRectangle_UpdatesNeedsDrawRect ()
+    {
+        var view = new View { Driver = CreateFakeDriver (), X = 0, Y = 0, Width = 20, Height = 20 };
+        view.BeginInit ();
+        view.EndInit ();
+        view.LayoutSubViews ();
+
+        // After layout, view will have NeedsDrawRect set to the viewport
+        // We need to clear it first
+        view.Draw ();
+        Assert.False (view.NeedsDraw);
+        Assert.Equal (Rectangle.Empty, view.NeedsDrawRect);
+
+        var rect = new Rectangle (5, 5, 10, 10);
+        view.SetNeedsDraw (rect);
+
+        Assert.True (view.NeedsDraw);
+        Assert.Equal (rect, view.NeedsDrawRect);
+    }
+
+    [Fact]
+    public void SetNeedsDraw_MultipleRectangles_Expands ()
+    {
+        IDriver driver = CreateFakeDriver (80, 25);
+        driver.Clip = new Region (driver.Screen);
+
+        var view = new View { X = 0, Y = 0, Width = 30, Height = 30, Driver = driver };
+        view.BeginInit ();
+        view.EndInit ();
+        view.LayoutSubViews ();
+
+        // After layout, clear NeedsDraw
+        view.Draw ();
+        Assert.False (view.NeedsDraw);
+
+        view.SetNeedsDraw (new Rectangle (5, 5, 10, 10));
+        view.SetNeedsDraw (new Rectangle (15, 15, 10, 10));
+
+        // Should expand to cover the entire viewport when we have overlapping regions
+        // The current implementation expands to viewport size
+        Rectangle expected = new Rectangle (0, 0, 30, 30);
+        Assert.Equal (expected, view.NeedsDrawRect);
+    }
+
+    [Fact]
+    public void SetNeedsDraw_NotVisible_DoesNotSet ()
+    {
+        var view = new View
+        {
+            X = 0,
+            Y = 0,
+            Width = 10,
+            Height = 10,
+            Visible = false
+        };
+        view.BeginInit ();
+        view.EndInit ();
+
+        view.SetNeedsDraw ();
+
+        Assert.False (view.NeedsDraw);
+    }
+
+    [Fact]
+    public void SetNeedsDraw_PropagatesToSuperView ()
+    {
+        var parent = new View { X = 0, Y = 0, Width = 50, Height = 50 };
+        var child = new View { X = 10, Y = 10, Width = 20, Height = 20 };
+        parent.Add (child);
+        parent.BeginInit ();
+        parent.EndInit ();
+        parent.LayoutSubViews ();
+
+        child.SetNeedsDraw ();
+
+        Assert.True (child.NeedsDraw);
+        Assert.True (parent.SubViewNeedsDraw);
+    }
+
+    [Fact]
+    public void SetNeedsDraw_SetsAdornmentsNeedsDraw ()
+    {
+        var view = new View { X = 0, Y = 0, Width = 20, Height = 20 };
+        view.Border!.Thickness = new Thickness (1);
+        view.Padding!.Thickness = new Thickness (1);
+        view.BeginInit ();
+        view.EndInit ();
+        view.LayoutSubViews ();
+
+        view.SetNeedsDraw ();
+
+        Assert.True (view.Border!.NeedsDraw);
+        Assert.True (view.Padding!.NeedsDraw);
+    }
+
+
+    [Fact]
+    public void IndividualViewDraw_DoesNotClearSuperViewSubViewNeedsDraw ()
+    {
+        // This test validates that individual view Draw() calls should NOT clear the superview's
+        // SubViewNeedsDraw flag when sibling subviews still need drawing.
+        //
+        // This is the core behavior that enables the fix in the static Draw method.
+        IDriver driver = CreateFakeDriver ();
+        driver.Clip = new (driver.Screen);
+
+        View superview = new ()
+        {
+            X = 0,
+            Y = 0,
+            Width = 50,
+            Height = 50,
+            Driver = driver,
+            Id = "SuperView"
+        };
+
+        View subview1 = new () { X = 0, Y = 0, Width = 10, Height = 10, Id = "SubView1" };
+        View subview2 = new () { X = 0, Y = 10, Width = 10, Height = 10, Id = "SubView2" };
+
+        superview.Add (subview1, subview2);
+        superview.BeginInit ();
+        superview.EndInit ();
+        superview.LayoutSubViews ();
+
+        Assert.True (superview.SubViewNeedsDraw);
+        Assert.True (subview1.NeedsDraw);
+        Assert.True (subview2.NeedsDraw);
+
+        // Draw only subview1 (NOT using the static Draw method)
+        subview1.Draw ();
+
+        // SubView1 should be cleared
+        Assert.False (subview1.NeedsDraw);
+
+        // SubView2 still needs drawing
+        Assert.True (subview2.NeedsDraw);
+
+        // THE KEY ASSERTION: SuperView's SubViewNeedsDraw should STILL be true
+        // because subview2 still needs drawing
+        //
+        // This behavior is REQUIRED for the static Draw fix to work properly.
+        // ClearNeedsDraw() does NOT clear SuperView.SubViewNeedsDraw anymore.
+        Assert.True (superview.SubViewNeedsDraw,
+            "SuperView's SubViewNeedsDraw must remain true when subview2 still needs drawing");
+
+        // Now draw subview2
+        subview2.Draw ();
+        Assert.False (subview2.NeedsDraw);
+
+        // SuperView's SubViewNeedsDraw should STILL be true because only the superview
+        // itself (or the static Draw method on all subviews) should clear it
+        Assert.True (superview.SubViewNeedsDraw,
+            "SuperView's SubViewNeedsDraw should only be cleared by superview.Draw() or static Draw() on all subviews");
+    }
+
+    #endregion
+
+    #region SubViewNeedsDraw Tests
+
+    [Fact]
+    public void SubViewNeedsDraw_InitiallyFalse ()
+    {
+        IDriver driver = CreateFakeDriver (80, 25);
+        driver.Clip = new Region (driver.Screen);
+
+        var view = new View { Width = 10, Height = 10, Driver = driver };
+        view.BeginInit ();
+        view.EndInit ();
+        view.Draw (); // Draw once to clear initial NeedsDraw
+
+        Assert.False (view.SubViewNeedsDraw);
+    }
+
+    [Fact]
+    public void SetSubViewNeedsDraw_PropagatesUp ()
+    {
+        var grandparent = new View { X = 0, Y = 0, Width = 100, Height = 100 };
+        var parent = new View { X = 10, Y = 10, Width = 50, Height = 50 };
+        var child = new View { X = 5, Y = 5, Width = 20, Height = 20 };
+
+        grandparent.Add (parent);
+        parent.Add (child);
+        grandparent.BeginInit ();
+        grandparent.EndInit ();
+        grandparent.LayoutSubViews ();
+
+        child.SetSubViewNeedsDrawDownHierarchy ();
+
+        Assert.True (child.SubViewNeedsDraw);
+        Assert.True (parent.SubViewNeedsDraw);
+        Assert.True (grandparent.SubViewNeedsDraw);
+    }
+
+    [Fact]
+    public void SubViewNeedsDraw_ClearedAfterDraw ()
+    {
+        IDriver driver = CreateFakeDriver (80, 25);
+        driver.Clip = new Region (driver.Screen);
+
+        var parent = new View
+        {
+            X = 0,
+            Y = 0,
+            Width = 50,
+            Height = 50,
+            Driver = driver
+        };
+        var child = new View { X = 10, Y = 10, Width = 20, Height = 20 };
+        parent.Add (child);
+        parent.BeginInit ();
+        parent.EndInit ();
+        parent.LayoutSubViews ();
+
+        child.SetNeedsDraw ();
+        Assert.True (parent.SubViewNeedsDraw);
+
+        parent.Draw ();
+
+        Assert.False (parent.SubViewNeedsDraw);
+        Assert.False (child.SubViewNeedsDraw);
+    }
+
+    #endregion
+
 }

+ 201 - 0
Tests/UnitTestsParallelizable/ViewBase/Draw/StaticDrawTests.cs

@@ -0,0 +1,201 @@
+#nullable enable
+using UnitTests;
+
+namespace ViewBaseTests.Drawing;
+
+/// <summary>
+/// Tests for the static View.Draw(IEnumerable&lt;View&gt;, bool) method
+/// </summary>
+[Trait ("Category", "Output")]
+public class StaticDrawTests : FakeDriverBase
+{
+    [Fact]
+    public void StaticDraw_ClearsSubViewNeedsDraw_AfterMarginDrawMargins ()
+    {
+        // This test validates the fix where the static Draw method calls ClearNeedsDraw()
+        // on all peer views after drawing them AND after calling Margin.DrawMargins().
+        //
+        // THE BUG (before the fix):
+        // Margin.DrawMargins() can cause SubViewNeedsDraw to be set on views in the hierarchy.
+        // This would leave SubViewNeedsDraw = true even after drawing completed.
+        //
+        // THE FIX (current code):
+        // The static Draw() method explicitly calls ClearNeedsDraw() on all peer views
+        // at the very end, AFTER Margin.DrawMargins(), clearing any SubViewNeedsDraw flags
+        // that were set during margin drawing.
+
+        IDriver driver = CreateFakeDriver ();
+        driver.Clip = new (driver.Screen);
+
+        // Create a view hierarchy where a subview's subview has a margin
+        // This reproduces the scenario where Margin.DrawMargins sets SubViewNeedsDraw
+        View superview = new ()
+        {
+            X = 0,
+            Y = 0,
+            Width = 60,
+            Height = 60,
+            Driver = driver,
+            Id = "SuperView"
+        };
+
+        View subview1 = new () { X = 0, Y = 0, Width = 40, Height = 40, Id = "SubView1" };
+        View subview2 = new () { X = 0, Y = 20, Width = 20, Height = 20, Id = "SubView2" };
+
+        // Add a subview to subview1 that has a margin with shadow
+        // This is key to reproducing the bug
+        View subSubView = new ()
+        {
+            X = 5,
+            Y = 5,
+            Width = 20,
+            Height = 20,
+            Id = "SubSubView"
+        };
+        subSubView.Margin!.Thickness = new (1);
+        subSubView.Margin.ShadowStyle = ShadowStyle.Transparent;
+
+        subview1.Add (subSubView);
+        superview.Add (subview1, subview2);
+
+        superview.BeginInit ();
+        superview.EndInit ();
+        superview.LayoutSubViews ();
+
+        // All views initially need drawing
+        Assert.True (superview.NeedsDraw);
+        Assert.True (superview.SubViewNeedsDraw);
+        Assert.True (subview1.NeedsDraw);
+        Assert.True (subview1.SubViewNeedsDraw);
+        Assert.True (subview2.NeedsDraw);
+        Assert.True (subSubView.NeedsDraw);
+        Assert.True (subSubView.Margin.NeedsDraw);
+
+        // Call the static Draw method on the subviews
+        // This will:
+        // 1. Call view.Draw() on each subview
+        // 2. Call Margin.DrawMargins() which may set SubViewNeedsDraw in the hierarchy
+        // 3. Call ClearNeedsDraw() on each subview to clean up
+        View.Draw (superview.InternalSubViews, force: false);
+
+        // After the static Draw completes:
+        // All subviews should have NeedsDraw = false
+        Assert.False (subview1.NeedsDraw, "SubView1 should not need drawing after Draw()");
+        Assert.False (subview2.NeedsDraw, "SubView2 should not need drawing after Draw()");
+        Assert.False (subSubView.NeedsDraw, "SubSubView should not need drawing after Draw()");
+        Assert.False (subSubView.Margin.NeedsDraw, "SubSubView's Margin should not need drawing after Draw()");
+
+        // SuperView's SubViewNeedsDraw should be false because the static Draw() method
+        // calls ClearNeedsDraw() on all the subviews at the end, AFTER Margin.DrawMargins()
+        // 
+        // BEFORE THE FIX: This would be TRUE because Margin.DrawMargins() would
+        //                 set SubViewNeedsDraw somewhere in the hierarchy and it
+        //                 wouldn't be cleared
+        // AFTER THE FIX: This is FALSE because the static Draw() calls ClearNeedsDraw()
+        //                at the very end, cleaning up any SubViewNeedsDraw flags set
+        //                by Margin.DrawMargins()
+        Assert.False (superview.SubViewNeedsDraw,
+                      "superview's SubViewNeedsDraw should be false after static Draw(). All subviews were drawn in the call to View.Draw");
+        Assert.False (subview1.SubViewNeedsDraw,
+                      "SubView1's SubViewNeedsDraw should be false after its subviews are drawn and cleared");
+    }
+
+    [Fact]
+    public void StaticDraw_WithForceTrue_SetsNeedsDrawOnAllViews ()
+    {
+        // Verify that when force=true, all views get SetNeedsDraw() called before drawing
+        IDriver driver = CreateFakeDriver ();
+        driver.Clip = new (driver.Screen);
+
+        View view1 = new () { X = 0, Y = 0, Width = 10, Height = 10, Driver = driver, Id = "View1" };
+        View view2 = new () { X = 10, Y = 0, Width = 10, Height = 10, Driver = driver, Id = "View2" };
+
+        view1.BeginInit ();
+        view1.EndInit ();
+        view2.BeginInit ();
+        view2.EndInit ();
+
+        // Manually clear their NeedsDraw flags
+        view1.Draw ();
+        view2.Draw ();
+        Assert.False (view1.NeedsDraw);
+        Assert.False (view2.NeedsDraw);
+
+        // Now call static Draw with force=true
+        View.Draw ([view1, view2], force: true);
+
+        // After drawing with force=true, they should be cleared again
+        Assert.False (view1.NeedsDraw);
+        Assert.False (view2.NeedsDraw);
+    }
+
+    [Fact]
+    public void StaticDraw_HandlesEmptyCollection ()
+    {
+        // Verify that calling Draw with an empty collection doesn't crash
+        View.Draw ([], force: false);
+        View.Draw ([], force: true);
+    }
+
+
+    [Fact]
+    public void StaticDraw_ClearsNestedSubViewNeedsDraw ()
+    {
+        // This test verifies that the static Draw method properly clears SubViewNeedsDraw
+        // flags throughout a nested view hierarchy after Margin.DrawMargins
+        IDriver driver = CreateFakeDriver ();
+        driver.Clip = new (driver.Screen);
+
+        View topView = new ()
+        {
+            X = 0,
+            Y = 0,
+            Width = 60,
+            Height = 60,
+            Driver = driver,
+            Id = "TopView"
+        };
+
+        View middleView1 = new () { X = 0, Y = 0, Width = 30, Height = 30, Id = "MiddleView1" };
+        View middleView2 = new () { X = 30, Y = 0, Width = 30, Height = 30, Id = "MiddleView2" };
+
+        View bottomView = new ()
+        {
+            X = 5,
+            Y = 5,
+            Width = 15,
+            Height = 15,
+            Id = "BottomView"
+        };
+
+        // Give the bottom view a margin to trigger the Margin.DrawMargins behavior
+        bottomView.Margin!.Thickness = new (1);
+        bottomView.Margin.ShadowStyle = ShadowStyle.Transparent;
+
+        middleView1.Add (bottomView);
+        topView.Add (middleView1, middleView2);
+
+        topView.BeginInit ();
+        topView.EndInit ();
+        topView.LayoutSubViews ();
+
+        Assert.True (topView.SubViewNeedsDraw);
+        Assert.True (middleView1.SubViewNeedsDraw);
+        Assert.True (bottomView.NeedsDraw);
+
+        // Draw the middle views using static Draw
+        View.Draw (topView.InternalSubViews, force: false);
+
+        // All SubViewNeedsDraw flags should be cleared after the static Draw
+        Assert.False (topView.SubViewNeedsDraw,
+            "TopView's SubViewNeedsDraw should be false after static Draw(). All subviews were drawn in the call to View.Draw");
+        Assert.False (middleView1.SubViewNeedsDraw,
+            "MiddleView1's SubViewNeedsDraw should be false after its subviews are drawn");
+        Assert.False (middleView2.SubViewNeedsDraw,
+            "MiddleView2's SubViewNeedsDraw should be false");
+        Assert.False (bottomView.NeedsDraw,
+            "BottomView should not need drawing after Draw()");
+        Assert.False (bottomView.Margin.NeedsDraw,
+            "BottomView's Margin should not need drawing after Draw()");
+    }
+}

+ 0 - 28
Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawTextAndLineCanvasTests.cs

@@ -205,34 +205,6 @@ public class ViewDrawTextAndLineCanvasTests () : FakeDriverBase
         Assert.True (eventRaised);
     }
 
-    [Fact]
-    public void DrewText_Event_Raised ()
-    {
-        IDriver driver = CreateFakeDriver (80, 25);
-        driver.Clip = new Region (driver.Screen);
-
-        bool eventRaised = false;
-
-        var view = new View
-        {
-            X = 10,
-            Y = 10,
-            Width = 20,
-            Height = 20,
-            Driver = driver,
-            Text = "Test"
-        };
-        view.BeginInit ();
-        view.EndInit ();
-        view.LayoutSubViews ();
-
-        view.DrewText += (s, e) => eventRaised = true;
-
-        view.Draw ();
-
-        Assert.True (eventRaised);
-    }
-
     #endregion
 
     #region LineCanvas Tests

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