浏览代码

Resolving merge conflicts.

BDisp 8 月之前
父节点
当前提交
7ce20f2677
共有 87 个文件被更改,包括 8246 次插入9457 次删除
  1. 3 87
      README.md
  2. 2 2
      Terminal.Gui/Drawing/Glyphs.cs
  3. 1 1
      Terminal.Gui/Drawing/Thickness.cs
  4. 0 89
      Terminal.Gui/Input/Responder.cs
  5. 0 3
      Terminal.Gui/Terminal.Gui.csproj
  6. 29 2
      Terminal.Gui/View/View.Content.cs
  7. 1 1
      Terminal.Gui/View/View.Drawing.Clipping.cs
  8. 0 8
      Terminal.Gui/View/View.Drawing.Primitives.cs
  9. 2 2
      Terminal.Gui/View/View.Hierarchy.cs
  10. 18 1
      Terminal.Gui/View/View.Layout.cs
  11. 197 0
      Terminal.Gui/View/View.ScrollBars.cs
  12. 68 9
      Terminal.Gui/View/View.cs
  13. 44 8
      Terminal.Gui/View/ViewportSettings.cs
  14. 167 251
      Terminal.Gui/Views/HexView.cs
  15. 63 0
      Terminal.Gui/Views/IListDataSource.cs
  16. 28 82
      Terminal.Gui/Views/ListView.cs
  17. 16 14
      Terminal.Gui/Views/NumericUpDown.cs
  18. 628 0
      Terminal.Gui/Views/ScrollBar/ScrollBar.cs
  19. 435 0
      Terminal.Gui/Views/ScrollBar/ScrollSlider.cs
  20. 0 1086
      Terminal.Gui/Views/ScrollBarView.cs
  21. 0 774
      Terminal.Gui/Views/ScrollView.cs
  22. 1 0
      Terminal.Gui/Views/Shortcut.cs
  23. 1 1
      Terminal.Gui/Views/Slider.cs
  24. 0 1404
      Terminal.Gui/Views/TabView.cs
  25. 1 1
      Terminal.Gui/Views/TabView/Tab.cs
  26. 0 0
      Terminal.Gui/Views/TabView/TabChangedEventArgs.cs
  27. 0 0
      Terminal.Gui/Views/TabView/TabMouseEventArgs.cs
  28. 793 0
      Terminal.Gui/Views/TabView/TabRowView.cs
  29. 0 0
      Terminal.Gui/Views/TabView/TabStyle.cs
  30. 585 0
      Terminal.Gui/Views/TabView/TabView.cs
  31. 0 400
      UICatalog/Scenarios/ASCIICustomButton.cs
  32. 0 166
      UICatalog/Scenarios/AdvancedClipping.cs
  33. 3 2
      UICatalog/Scenarios/AllViewsTester.cs
  34. 0 1252
      UICatalog/Scenarios/CharacterMap.cs
  35. 785 0
      UICatalog/Scenarios/CharacterMap/CharMap.cs
  36. 352 0
      UICatalog/Scenarios/CharacterMap/CharacterMap.cs
  37. 11 0
      UICatalog/Scenarios/CharacterMap/README.md
  38. 48 0
      UICatalog/Scenarios/CharacterMap/UcdApiClient.cs
  39. 101 0
      UICatalog/Scenarios/CharacterMap/UnicodeRange.cs
  40. 129 73
      UICatalog/Scenarios/Clipping.cs
  41. 35 35
      UICatalog/Scenarios/CsvEditor.cs
  42. 33 33
      UICatalog/Scenarios/Editor.cs
  43. 10 0
      UICatalog/Scenarios/Editors/DimEditor.cs
  44. 34 18
      UICatalog/Scenarios/Editors/EventLog.cs
  45. 6 1
      UICatalog/Scenarios/HexEditor.cs
  46. 36 36
      UICatalog/Scenarios/ListColumns.cs
  47. 55 72
      UICatalog/Scenarios/ListViewWithSelection.cs
  48. 54 54
      UICatalog/Scenarios/ListsAndCombos.cs
  49. 398 0
      UICatalog/Scenarios/ScrollBarDemo.cs
  50. 184 187
      UICatalog/Scenarios/Scrolling.cs
  51. 41 48
      UICatalog/Scenarios/TableEditor.cs
  52. 43 41
      UICatalog/Scenarios/TreeViewFileSystem.cs
  53. 142 81
      UICatalog/Scenarios/ViewportSettings.cs
  54. 26 26
      UICatalog/Scenarios/Wizards.cs
  55. 10 2
      UICatalog/UICatalog.cs
  56. 3 3
      UnitTests/Application/ApplicationTests.cs
  57. 2 2
      UnitTests/Application/KeyboardTests.cs
  58. 42 42
      UnitTests/Application/Mouse/ApplicationMouseTests.cs
  59. 1 1
      UnitTests/Application/RunStateTests.cs
  60. 1 270
      UnitTests/Input/ResponderTests.cs
  61. 15 10
      UnitTests/TestHelpers.cs
  62. 3 3
      UnitTests/UICatalog/ScenarioTests.cs
  63. 3 5
      UnitTests/View/Draw/DrawTests.cs
  64. 2 2
      UnitTests/View/Layout/Dim.Tests.cs
  65. 51 0
      UnitTests/View/Layout/FrameTests.cs
  66. 1 1
      UnitTests/View/Layout/Pos.ViewTests.cs
  67. 0 2
      UnitTests/View/Layout/SetLayoutTests.cs
  68. 87 0
      UnitTests/View/Layout/ViewportTests.cs
  69. 18 18
      UnitTests/View/Orientation/OrientationHelperTests.cs
  70. 151 2
      UnitTests/View/ViewTests.cs
  71. 12 38
      UnitTests/Views/HexViewTests.cs
  72. 2 2
      UnitTests/Views/ListViewTests.cs
  73. 943 0
      UnitTests/Views/ScrollBarTests.cs
  74. 0 1390
      UnitTests/Views/ScrollBarViewTests.cs
  75. 1010 0
      UnitTests/Views/ScrollSliderTests.cs
  76. 0 1155
      UnitTests/Views/ScrollViewTests.cs
  77. 209 77
      UnitTests/Views/TabViewTests.cs
  78. 0 55
      UnitTests/Views/ToplevelTests.cs
  79. 6 1
      docfx/docs/View.md
  80. 1 1
      docfx/docs/arrangement.md
  81. 1 20
      docfx/docs/index.md
  82. 4 3
      docfx/docs/layout.md
  83. 3 0
      docfx/docs/migratingfromv1.md
  84. 2 1
      docfx/docs/newinv2.md
  85. 52 0
      docfx/docs/scrolling.md
  86. 2 0
      docfx/docs/toc.yml
  87. 二进制
      docfx/images/sample.gif

+ 3 - 87
README.md

@@ -1,4 +1,4 @@
-![Terminal.Gui](https://socialify.git.ci/gui-cs/Terminal.Gui/image?description=1&font=Rokkitt&forks=1&language=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2Fgui-cs%2FTerminal.Gui%2Fdevelop%2Fdocfx%2Fimages%2Flogo.png&name=1&owner=1&pattern=Circuit%20Board&stargazers=1&theme=Auto)
+![Terminal.Gui](https://socialify.git.ci/gui-cs/Terminal.Gui/image?description=1&descriptionEditable=Cross%20Platform%20Terminal%20UI%20Toolkit&font=KoHo&forks=1&logo=https%3A%2F%2Fgithub.com%2Fgui-cs%2FTerminal.Gui%2Fblob%2Fv2_develop%2Fdocfx%2Fimages%2Flogo.png%3Fraw%3Dtrue&pattern=Circuit%20Board&stargazers=1&theme=Dark)
 ![.NET Core](https://github.com/gui-cs/Terminal.Gui/workflows/.NET%20Core/badge.svg?branch=develop)
 [![Version](https://img.shields.io/nuget/v/Terminal.Gui.svg)](https://www.nuget.org/packages/Terminal.Gui)
 ![Code Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/migueldeicaza/90ef67a684cb71db1817921a970f8d27/raw/code-coverage.json)
@@ -11,7 +11,7 @@
 * Developers starting new TUI projects are encouraged to target `v2`. The API is significantly changed, and significantly improved. There will be breaking changes in the API before Beta, but the core API is stable.
 * `v1` is in maintenance mode and we will only accept PRs for issues impacting existing functionality.
  
-**Terminal.Gui**: A toolkit for building rich console apps for .NET, .NET Core, and Mono that works on Windows, the Mac, and Linux/Unix.
+**Terminal.Gui**: A toolkit for building rich console apps for Windows, the Mac, and Linux/Unix.
 
 ![Sample app](docfx/images/sample.gif)
 
@@ -49,91 +49,7 @@ The team is looking forward to seeing new amazing projects made by the community
 
 The following example shows a basic Terminal.Gui application in C#:
 
-```csharp
-// This is a simple example application.  For the full range of functionality
-// see the UICatalog project
-
-// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
-
-using System;
-using Terminal.Gui;
-
-Application.Run<ExampleWindow> ().Dispose ();
-
-// Before the application exits, reset Terminal.Gui for clean shutdown
-Application.Shutdown ();
-
-// To see this output on the screen it must be done after shutdown,
-// which restores the previous screen.
-Console.WriteLine ($@"Username: {ExampleWindow.UserName}");
-
-// Defines a top-level window with border and title
-public class ExampleWindow : Window
-{
-    public static string UserName;
-
-    public ExampleWindow ()
-    {
-        Title = $"Example App ({Application.QuitKey} to quit)";
-
-        // Create input components and labels
-        var usernameLabel = new Label { Text = "Username:" };
-
-        var userNameText = new TextField
-        {
-            // Position text field adjacent to the label
-            X = Pos.Right (usernameLabel) + 1,
-
-            // Fill remaining horizontal space
-            Width = Dim.Fill ()
-        };
-
-        var passwordLabel = new Label
-        {
-            Text = "Password:", X = Pos.Left (usernameLabel), Y = Pos.Bottom (usernameLabel) + 1
-        };
-
-        var passwordText = new TextField
-        {
-            Secret = true,
-
-            // align with the text box above
-            X = Pos.Left (userNameText),
-            Y = Pos.Top (passwordLabel),
-            Width = Dim.Fill ()
-        };
-
-        // Create login button
-        var btnLogin = new Button
-        {
-            Text = "Login",
-            Y = Pos.Bottom (passwordLabel) + 1,
-
-            // center the login button horizontally
-            X = Pos.Center (),
-            IsDefault = true
-        };
-
-        // When login button is clicked display a message popup
-        btnLogin.Accept += (s, e) =>
-                           {
-                               if (userNameText.Text == "admin" && passwordText.Text == "password")
-                               {
-                                   MessageBox.Query ("Logging In", "Login Successful", "Ok");
-                                   UserName = userNameText.Text;
-                                   Application.RequestStop ();
-                               }
-                               else
-                               {
-                                   MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
-                               }
-                           };
-
-        // Add the views to the Window
-        Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin);
-    }
-}
-```
+[!code-csharp[](./Example/Example.cs)]
 
 When run the application looks as follows:
 

+ 2 - 2
Terminal.Gui/Drawing/Glyphs.cs

@@ -79,10 +79,10 @@ public class GlyphDefinitions
     /// <summary>Continuous block meter segment (e.g. for <see cref="ProgressBar"/>).</summary>
     public Rune ContinuousMeterSegment { get; set; } = (Rune)'█';
 
-    /// <summary>Stipple pattern (e.g. for <see cref="ScrollBarView"/>). Default is Light Shade (U+2591) - ░.</summary>
+    /// <summary>Stipple pattern (e.g. for <see cref="ScrollBar"/>). Default is Light Shade (U+2591) - ░.</summary>
     public Rune Stipple { get; set; } = (Rune)'░';
 
-    /// <summary>Diamond (e.g. for <see cref="ScrollBarView"/>. Default is Lozenge (U+25CA) - ◊.</summary>
+    /// <summary>Diamond. Default is Lozenge (U+25CA) - ◊.</summary>
     public Rune Diamond { get; set; } = (Rune)'◊';
 
     /// <summary>Close. Default is Heavy Ballot X (U+2718) - ✘.</summary>

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

@@ -236,7 +236,7 @@ public record struct Thickness
     }
 
     /// <summary>
-    ///     Gets the total width of the left and right sides of the rectangle. Sets the width of the left and rigth sides
+    ///     Gets the total width of the left and right sides of the rectangle. Sets the width of the left and right sides
     ///     of the rectangle to half the specified value.
     /// </summary>
     public int Horizontal

+ 0 - 89
Terminal.Gui/Input/Responder.cs

@@ -1,89 +0,0 @@
-using System.Reflection;
-
-namespace Terminal.Gui;
-
-/// <summary>Responder base class implemented by objects that want to participate on keyboard and mouse input.</summary>
-public class Responder : IDisposable
-{
-    private bool _disposedValue;
-
-    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource.</summary>
-
-    public void Dispose ()
-    {
-        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
-        Disposing?.Invoke (this, EventArgs.Empty);
-        Dispose (true);
-        GC.SuppressFinalize (this);
-#if DEBUG_IDISPOSABLE
-        WasDisposed = true;
-
-        foreach (Responder instance in Instances.Where (x => x.WasDisposed).ToList ())
-        {
-            Instances.Remove (instance);
-        }
-#endif
-    }
-
-    /// <summary>Event raised when <see cref="Dispose()"/> has been called to signal that this object is being disposed.</summary>
-    [CanBeNull]
-    public event EventHandler Disposing;
-
-    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
-    /// <remarks>
-    ///     If disposing equals true, the method has been called directly or indirectly by a user's code. Managed and
-    ///     unmanaged resources can be disposed. If disposing equals false, the method has been called by the runtime from
-    ///     inside the finalizer and you should not reference other objects. Only unmanaged resources can be disposed.
-    /// </remarks>
-    /// <param name="disposing"></param>
-    protected virtual void Dispose (bool disposing)
-    {
-        if (!_disposedValue)
-        {
-            if (disposing)
-            {
-                // TODO: dispose managed state (managed objects)
-            }
-
-            _disposedValue = true;
-        }
-    }
-
-    // TODO: v2 - nuke this
-    /// <summary>Utilty function to determine <paramref name="method"/> is overridden in the <paramref name="subclass"/>.</summary>
-    /// <param name="subclass">The view.</param>
-    /// <param name="method">The method name.</param>
-    /// <returns><see langword="true"/> if it's overridden, <see langword="false"/> otherwise.</returns>
-    internal static bool IsOverridden (Responder subclass, string method)
-    {
-        MethodInfo m = subclass.GetType ()
-                               .GetMethod (
-                                           method,
-                                           BindingFlags.Instance
-                                           | BindingFlags.Public
-                                           | BindingFlags.NonPublic
-                                           | BindingFlags.DeclaredOnly
-                                          );
-
-        if (m is null)
-        {
-            return false;
-        }
-
-        return m.GetBaseDefinition ().DeclaringType != m.DeclaringType;
-    }
-
-#if DEBUG_IDISPOSABLE
-    /// <summary>For debug purposes to verify objects are being disposed properly</summary>
-    public bool WasDisposed;
-
-    /// <summary>For debug purposes to verify objects are being disposed properly</summary>
-    public int DisposedCount = 0;
-
-    /// <summary>For debug purposes</summary>
-    public static List<Responder> Instances = new ();
-
-    /// <summary>For debug purposes</summary>
-    public Responder () { Instances.Add (this); }
-#endif
-}

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

@@ -109,9 +109,6 @@
       <LastGenOutput>Strings.Designer.cs</LastGenOutput>
     </EmbeddedResource>
   </ItemGroup>
-  <ItemGroup>
-    <Folder Include="Views\Scroll\" />
-  </ItemGroup>
   <!-- =================================================================== -->
   <!-- Nuget  -->
   <!-- =================================================================== -->

+ 29 - 2
Terminal.Gui/View/View.Content.cs

@@ -312,7 +312,7 @@ public partial class View
                 //SetSubViewNeedsDraw();
             }
 
-            OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport));
+            RaiseViewportChangedEvent (oldViewport);
 
             return;
         }
@@ -325,6 +325,10 @@ public partial class View
             Size = newSize
         };
 
+        // Note, setting the Frame will cause ViewportChanged to be raised.
+
+        return;
+
         void ApplySettings (ref Rectangle newViewport)
         {
             if (!ViewportSettings.HasFlag (ViewportSettings.AllowXGreaterThanContentWidth))
@@ -344,6 +348,14 @@ public partial class View
                 }
             }
 
+            if (!ViewportSettings.HasFlag (ViewportSettings.AllowNegativeXWhenWidthGreaterThanContentWidth))
+            {
+                if (Viewport.Width > GetContentSize ().Width)
+                {
+                    newViewport.X = 0;
+                }
+            }
+
             if (!ViewportSettings.HasFlag (ViewportSettings.AllowYGreaterThanContentHeight))
             {
                 if (newViewport.Y >= GetContentSize ().Height)
@@ -352,6 +364,14 @@ public partial class View
                 }
             }
 
+            if (!ViewportSettings.HasFlag (ViewportSettings.AllowNegativeYWhenHeightGreaterThanContentHeight))
+            {
+                if (Viewport.Height > GetContentSize ().Height)
+                {
+                    newViewport.Y = 0;
+                }
+            }
+
             // IMPORTANT: Check for negative location AFTER checking for location greater than content width
             if (!ViewportSettings.HasFlag (ViewportSettings.AllowNegativeY))
             {
@@ -363,6 +383,13 @@ public partial class View
         }
     }
 
+    private void RaiseViewportChangedEvent (Rectangle oldViewport)
+    {
+        var args = new DrawEventArgs (IsInitialized ? Viewport : Rectangle.Empty, oldViewport);
+        OnViewportChanged (args);
+        ViewportChanged?.Invoke (this, args);
+    }
+
     /// <summary>
     ///     Fired when the <see cref="Viewport"/> changes. This event is fired after the <see cref="Viewport"/> has been
     ///     updated.
@@ -373,7 +400,7 @@ public partial class View
     ///     Called when the <see cref="Viewport"/> changes. Invokes the <see cref="ViewportChanged"/> event.
     /// </summary>
     /// <param name="e"></param>
-    protected virtual void OnViewportChanged (DrawEventArgs e) { ViewportChanged?.Invoke (this, e); }
+    protected virtual void OnViewportChanged (DrawEventArgs e) { }
 
     /// <summary>
     ///     Converts a <see cref="Viewport"/>-relative location and size to a screen-relative location and size.

+ 1 - 1
Terminal.Gui/View/View.Drawing.Clipping.cs

@@ -104,7 +104,7 @@ public partial class View
         if (this is Adornment adornment && adornment.Thickness != Thickness.Empty)
         {
             // Ensure adornments can't draw outside their thickness
-            frameRegion.Exclude (adornment.Thickness.GetInside (Frame));
+            frameRegion.Exclude (adornment.Thickness.GetInside (FrameToScreen()));
         }
 
         SetClip (frameRegion);

+ 0 - 8
Terminal.Gui/View/View.Drawing.Primitives.cs

@@ -7,9 +7,6 @@ public partial class View
     /// <summary>Moves the drawing cursor to the specified <see cref="Viewport"/>-relative location in the view.</summary>
     /// <remarks>
     ///     <para>
-    ///         If the provided coordinates are outside the visible content area, this method does nothing.
-    ///     </para>
-    ///     <para>
     ///         The top-left corner of the visible content area is <c>ViewPort.Location</c>.
     ///     </para>
     /// </remarks>
@@ -22,11 +19,6 @@ public partial class View
             return false;
         }
 
-        if (col < 0 || row < 0 || col >= Viewport.Width || row >= Viewport.Height)
-        {
-            return false;
-        }
-
         Point screen = ViewportToScreen (new Point (col, row));
         Driver?.Move (screen.X, screen.Y);
 

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

@@ -145,7 +145,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     ///     <para>
     ///         Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
     ///         Subview's
-    ///         lifecycle to be transferred to the caller; the caller muse call <see cref="Dispose"/>.
+    ///         lifecycle to be transferred to the caller; the caller must call <see cref="Dispose()"/>.
     ///     </para>
     /// </remarks>
     /// <returns>
@@ -214,7 +214,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     ///     <para>
     ///         Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
     ///         Subview's
-    ///         lifecycle to be transferred to the caller; the caller must call <see cref="Dispose"/> on any Views that were
+    ///         lifecycle to be transferred to the caller; the caller must call <see cref="Dispose()"/> on any Views that were
     ///         added.
     ///     </para>
     /// </remarks>

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

@@ -96,11 +96,28 @@ public partial class View // Layout APIs
         SetNeedsLayout ();
 
         // BUGBUG: When SetFrame is called from Frame_set, this event gets raised BEFORE OnResizeNeeded. Is that OK?
-        OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport));
+        OnFrameChanged (in frame);
+        FrameChanged?.Invoke (this, new (in frame));
 
+        if (oldViewport != Viewport)
+        {
+            RaiseViewportChangedEvent (oldViewport);
+        }
         return true;
     }
 
+    /// <summary>
+    ///     Called when <see cref="Frame"/> changes.
+    /// </summary>
+    /// <param name="frame">The new Frame.</param>
+    protected virtual void OnFrameChanged (in Rectangle frame) { }
+
+    /// <summary>
+    ///     Raised when the <see cref="Frame"/> changes. This event is raised after the <see cref="Frame"/> has been
+    ///     updated.
+    /// </summary>
+    public event EventHandler<EventArgs<Rectangle>>? FrameChanged;
+
     /// <summary>Gets the <see cref="Frame"/> with a screen-relative location.</summary>
     /// <returns>The location and size of the view in screen-relative coordinates.</returns>
     public virtual Rectangle FrameToScreen ()

+ 197 - 0
Terminal.Gui/View/View.ScrollBars.cs

@@ -0,0 +1,197 @@
+#nullable enable
+namespace Terminal.Gui;
+
+public partial class View
+{
+    private Lazy<ScrollBar> _horizontalScrollBar = null!;
+
+    /// <summary>
+    ///     Gets the horizontal <see cref="ScrollBar"/>. This property is lazy-loaded and will not be created until it is accessed.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         See <see cref="ScrollBar"/> for more information on how to use the ScrollBar.
+    ///     </para>
+    /// </remarks>
+    public ScrollBar HorizontalScrollBar => _horizontalScrollBar.Value;
+
+    private Lazy<ScrollBar> _verticalScrollBar = null!;
+
+    /// <summary>
+    ///     Gets the vertical <see cref="ScrollBar"/>. This property is lazy-loaded and will not be created until it is accessed.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         See <see cref="ScrollBar"/> for more information on how to use the ScrollBar.
+    ///     </para>
+    /// </remarks>
+    public ScrollBar VerticalScrollBar => _verticalScrollBar.Value;
+
+    /// <summary>
+    ///     Initializes the ScrollBars of the View. Called by the View constructor.
+    /// </summary>
+    private void SetupScrollBars ()
+    {
+        if (this is Adornment)
+        {
+            return;
+        }
+
+        _verticalScrollBar = new (() => CreateScrollBar (Orientation.Vertical));
+        _horizontalScrollBar = new (() => CreateScrollBar (Orientation.Horizontal));
+    }
+
+    private ScrollBar CreateScrollBar (Orientation orientation)
+    {
+        var scrollBar = new ScrollBar
+        {
+            Orientation = orientation,
+            Visible = false // Initially hidden until needed
+        };
+
+        if (orientation == Orientation.Vertical)
+        {
+            ConfigureVerticalScrollBar (scrollBar);
+        }
+        else
+        {
+            ConfigureHorizontalScrollBar (scrollBar);
+        }
+
+        scrollBar.Initialized += OnScrollBarInitialized;
+
+        // Add after setting Initialized event!
+        Padding?.Add (scrollBar);
+
+        return scrollBar;
+    }
+
+    private void ConfigureVerticalScrollBar (ScrollBar scrollBar)
+    {
+        scrollBar.X = Pos.AnchorEnd ();
+
+        scrollBar.Height = Dim.Fill (
+                                     Dim.Func (
+                                               () =>
+                                               {
+                                                   if (_horizontalScrollBar.IsValueCreated)
+                                                   {
+                                                       return _horizontalScrollBar.Value.Visible ? 1 : 0;
+                                                   }
+
+                                                   return 0;
+                                               }));
+        scrollBar.ScrollableContentSize = GetContentSize ().Height;
+
+        ViewportChanged += (_, _) =>
+                           {
+                               scrollBar.Position = Viewport.Y;
+                           };
+
+        ContentSizeChanged += (_, _) => { scrollBar.ScrollableContentSize = GetContentSize ().Height; };
+    }
+
+    private void ConfigureHorizontalScrollBar (ScrollBar scrollBar)
+    {
+        scrollBar.Y = Pos.AnchorEnd ();
+
+        scrollBar.Width = Dim.Fill (
+                                    Dim.Func (
+                                              () =>
+                                              {
+                                                  if (_verticalScrollBar.IsValueCreated)
+                                                  {
+                                                      return _verticalScrollBar.Value.Visible ? 1 : 0;
+                                                  }
+
+                                                  return 0;
+                                              }));
+        scrollBar.ScrollableContentSize = GetContentSize ().Width;
+
+        ViewportChanged += (_, _) =>
+                           {
+                               scrollBar.Position = Viewport.X;
+                           };
+
+        ContentSizeChanged += (_, _) => { scrollBar.ScrollableContentSize = GetContentSize ().Width; };
+    }
+
+    private void OnScrollBarInitialized (object? sender, EventArgs e)
+    {
+        var scrollBar = (ScrollBar)sender!;
+
+        if (scrollBar.Orientation == Orientation.Vertical)
+        {
+            ConfigureVerticalScrollBarEvents (scrollBar);
+        }
+        else
+        {
+            ConfigureHorizontalScrollBarEvents (scrollBar);
+        }
+    }
+
+    private void ConfigureVerticalScrollBarEvents (ScrollBar scrollBar)
+    {
+        Padding!.Thickness = Padding.Thickness with { Right = scrollBar.Visible ? Padding.Thickness.Right + 1 : 0 };
+
+        scrollBar.PositionChanged += (_, args) =>
+                                     {
+                                         Viewport = Viewport with
+                                         {
+                                             Y = Math.Min (args.CurrentValue, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize)
+                                         };
+                                     };
+
+        scrollBar.VisibleChanged += (_, _) =>
+                                    {
+                                        Padding.Thickness = Padding.Thickness with
+                                        {
+                                            Right = scrollBar.Visible ? Padding.Thickness.Right + 1 : Padding.Thickness.Right - 1
+                                        };
+                                    };
+    }
+
+    private void ConfigureHorizontalScrollBarEvents (ScrollBar scrollBar)
+    {
+        Padding!.Thickness = Padding.Thickness with { Bottom = scrollBar.Visible ? Padding.Thickness.Bottom + 1 : 0 };
+
+        scrollBar.PositionChanged += (_, args) =>
+                                     {
+                                         Viewport = Viewport with
+                                         {
+                                             X = Math.Min (args.CurrentValue, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize)
+                                         };
+                                     };
+
+        scrollBar.VisibleChanged += (_, _) =>
+                                    {
+                                        Padding.Thickness = Padding.Thickness with
+                                        {
+                                            Bottom = scrollBar.Visible ? Padding.Thickness.Bottom + 1 : Padding.Thickness.Bottom - 1
+                                        };
+                                    };
+    }
+
+    /// <summary>
+    ///     Clean up the ScrollBars of the View. Called by View.Dispose.
+    /// </summary>
+    private void DisposeScrollBars ()
+    {
+        if (this is Adornment)
+        {
+            return;
+        }
+
+        if (_horizontalScrollBar.IsValueCreated)
+        {
+            Padding?.Remove (_horizontalScrollBar.Value);
+            _horizontalScrollBar.Value.Dispose ();
+        }
+
+        if (_verticalScrollBar.IsValueCreated)
+        {
+            Padding?.Remove (_verticalScrollBar.Value);
+            _verticalScrollBar.Value.Dispose ();
+        }
+    }
+}

+ 68 - 9
Terminal.Gui/View/View.cs

@@ -74,8 +74,10 @@ namespace Terminal.Gui;
 ///         To flag the entire view for redraw call <see cref="SetNeedsDraw()"/>.
 ///     </para>
 ///     <para>
-///         The <see cref="SetNeedsLayout"/> method is called when the size or layout of a view has changed. The <see cref="MainLoop"/> will
-///         cause <see cref="Layout()"/> to be called on the next <see cref="Application.Iteration"/> so there is normally no reason to direclty call
+///         The <see cref="SetNeedsLayout"/> method is called when the size or layout of a view has changed. The
+///         <see cref="MainLoop"/> will
+///         cause <see cref="Layout()"/> to be called on the next <see cref="Application.Iteration"/> so there is normally
+///         no reason to direclty call
 ///         see <see cref="Layout()"/>.
 ///     </para>
 ///     <para>
@@ -107,7 +109,7 @@ namespace Terminal.Gui;
 
 #endregion API Docs
 
-public partial class View : Responder, ISupportInitializeNotification
+public partial class View : IDisposable, ISupportInitializeNotification
 {
     #region Constructors and Initialization
 
@@ -135,6 +137,10 @@ public partial class View : Responder, ISupportInitializeNotification
     /// </remarks>
     public View ()
     {
+#if DEBUG_IDISPOSABLE
+        Instances.Add (this);
+#endif
+
         SetupAdornments ();
 
         SetupCommands ();
@@ -144,6 +150,8 @@ public partial class View : Responder, ISupportInitializeNotification
         //SetupMouse ();
 
         SetupText ();
+
+        SetupScrollBars ();
     }
 
     /// <summary>
@@ -255,7 +263,7 @@ public partial class View : Responder, ISupportInitializeNotification
 
     private bool _enabled = true;
 
-    /// <summary>Gets or sets a value indicating whether this <see cref="Responder"/> can respond to user interaction.</summary>
+    /// <summary>Gets or sets a value indicating whether this <see cref="View"/> can respond to user interaction.</summary>
     public bool Enabled
     {
         get => _enabled;
@@ -359,9 +367,9 @@ public partial class View : Responder, ISupportInitializeNotification
             VisibleChanged?.Invoke (this, EventArgs.Empty);
 
             SetNeedsLayout ();
-            SuperView?.SetNeedsLayout();
+            SuperView?.SetNeedsLayout ();
             SetNeedsDraw ();
-            SuperView?.SetNeedsDraw();
+            SuperView?.SetNeedsDraw ();
         }
     }
 
@@ -522,13 +530,22 @@ public partial class View : Responder, ISupportInitializeNotification
     /// <returns></returns>
     public override string ToString () { return $"{GetType ().Name}({Id}){Frame}"; }
 
-    /// <inheritdoc/>
-    protected override void Dispose (bool disposing)
+    private bool _disposedValue;
+
+    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+    /// <remarks>
+    ///     If disposing equals true, the method has been called directly or indirectly by a user's code. Managed and
+    ///     unmanaged resources can be disposed. If disposing equals false, the method has been called by the runtime from
+    ///     inside the finalizer and you should not reference other objects. Only unmanaged resources can be disposed.
+    /// </remarks>
+    /// <param name="disposing"></param>
+    protected virtual void Dispose (bool disposing)
     {
         LineCanvas.Dispose ();
 
         DisposeKeyboard ();
         DisposeAdornments ();
+        DisposeScrollBars ();
 
         for (int i = InternalSubviews.Count - 1; i >= 0; i--)
         {
@@ -537,7 +554,49 @@ public partial class View : Responder, ISupportInitializeNotification
             subview.Dispose ();
         }
 
-        base.Dispose (disposing);
+        if (!_disposedValue)
+        {
+            if (disposing)
+            {
+                // TODO: dispose managed state (managed objects)
+            }
+
+            _disposedValue = true;
+        }
+
         Debug.Assert (InternalSubviews.Count == 0);
     }
+
+    /// <summary>
+    ///     Riased when the <see cref="View"/> is being disposed. 
+    /// </summary>
+    public event EventHandler? Disposing;
+
+    /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource.</summary>
+    public void Dispose ()
+    {
+        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+        Disposing?.Invoke (this, EventArgs.Empty);
+        Dispose (true);
+        GC.SuppressFinalize (this);
+#if DEBUG_IDISPOSABLE
+        WasDisposed = true;
+
+        foreach (View instance in Instances.Where (x => x.WasDisposed).ToList ())
+        {
+            Instances.Remove (instance);
+        }
+#endif
+    }
+
+#if DEBUG_IDISPOSABLE
+    /// <summary>For debug purposes to verify objects are being disposed properly</summary>
+    public bool WasDisposed { get; set; }
+
+    /// <summary>For debug purposes to verify objects are being disposed properly</summary>
+    public int DisposedCount { get; set; } = 0;
+
+    /// <summary>For debug purposes</summary>
+    public static List<View> Instances { get; set; } = [];
+#endif
 }

+ 44 - 8
Terminal.Gui/View/ViewportSettings.cs

@@ -4,7 +4,8 @@
 ///     Settings for how the <see cref="View.Viewport"/> behaves relative to the View's Content area.
 /// </summary>
 /// <remarks>
-///     See the Layout Deep Dive for more information: <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
+///     See the Layout Deep Dive for more information:
+///     <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/layout.html"/>
 /// </remarks>
 [Flags]
 public enum ViewportSettings
@@ -76,7 +77,8 @@ public enum ViewportSettings
     AllowYGreaterThanContentHeight = 8,
 
     /// <summary>
-    ///     If set, <see cref="View.Viewport"/><c>.Size</c> can be set values greater than <see cref="View.GetContentSize ()"/>
+    ///     If set, <see cref="View.Viewport"/><c>.Location</c> can be set values greater than
+    ///     <see cref="View.GetContentSize ()"/>
     ///     enabling scrolling beyond the bottom-right
     ///     of the content area.
     ///     <para>
@@ -87,20 +89,54 @@ public enum ViewportSettings
     /// </summary>
     AllowLocationGreaterThanContentSize = AllowXGreaterThanContentWidth | AllowYGreaterThanContentHeight,
 
+    /// <summary>
+    ///     If set and <see cref="View.Viewport"/><c>.Width</c> is greater than <see cref="View.GetContentSize ()"/>
+    ///     <c>.Width</c> <see cref="View.Viewport"/><c>.X</c> can be negative.
+    ///     <para>
+    ///         When not set, <see cref="View.Viewport"/><c>.X</c> will be constrained to non-negative values when
+    ///         <see cref="View.Viewport"/><c>.Width</c> is greater than <see cref="View.GetContentSize ()"/>
+    ///         <c>.Width</c>, preventing
+    ///         scrolling beyond the left of the Viewport.
+    ///     </para>
+    ///     <para>
+    ///         This can be useful in infinite scrolling scenarios.
+    ///     </para>
+    /// </summary>
+    AllowNegativeXWhenWidthGreaterThanContentWidth = 16,
+
+    /// <summary>
+    ///     If set and <see cref="View.Viewport"/><c>.Height</c> is greater than <see cref="View.GetContentSize ()"/>
+    ///     <c>.Height</c> <see cref="View.Viewport"/><c>.Y</c> can be negative.
+    ///     <para>
+    ///         When not set, <see cref="View.Viewport"/><c>.Y</c> will be constrained to non-negative values when
+    ///         <see cref="View.Viewport"/><c>.Height</c> is greater than <see cref="View.GetContentSize ()"/>
+    ///         <c>.Height</c>, preventing
+    ///         scrolling above the top of the Viewport.
+    ///     </para>
+    ///     <para>
+    ///         This can be useful in infinite scrolling scenarios.
+    ///     </para>
+    /// </summary>
+    AllowNegativeYWhenHeightGreaterThanContentHeight = 32,
+
+    /// <summary>
+    ///     The combination of <see cref="AllowNegativeXWhenWidthGreaterThanContentWidth"/> and
+    ///     <see cref="AllowNegativeYWhenHeightGreaterThanContentHeight"/>.
+    /// </summary>
+    AllowNegativeLocationWhenSizeGreaterThanContentSize = AllowNegativeXWhenWidthGreaterThanContentWidth | AllowNegativeYWhenHeightGreaterThanContentHeight,
+
     /// <summary>
     ///     By default, clipping is applied to the <see cref="View.Viewport"/>. Setting this flag will cause clipping to be
     ///     applied to the visible content area.
     /// </summary>
-    ClipContentOnly = 16,
+    ClipContentOnly = 64,
 
     /// <summary>
     ///     If set <see cref="View.ClearViewport"/> will clear only the portion of the content
     ///     area that is visible within the <see cref="View.Viewport"/>. This is useful for views that have a
     ///     content area larger than the Viewport and want the area outside the content to be visually distinct.
-    ///     <para>
-    ///         <see cref="ClipContentOnly"/> must be set for this setting to work (clipping beyond the visible area must be
-    ///         disabled).
-    ///     </para>
+    ///     <see cref="ClipContentOnly"/> must be set for this setting to work (clipping beyond the visible area must be
+    ///     disabled).
     /// </summary>
-    ClearContentOnly = 32
+    ClearContentOnly = 128
 }

+ 167 - 251
Terminal.Gui/Views/HexView.cs

@@ -31,7 +31,6 @@ namespace Terminal.Gui;
 ///         Control the byte at the caret for editing by setting the <see cref="Address"/> property to an offset in the
 ///         stream.
 ///     </para>
-///     <para>Control the first byte shown by setting the <see cref="DisplayStart"/> property to an offset in the stream.</para>
 /// </remarks>
 public class HexView : View, IDesignable
 {
@@ -72,10 +71,10 @@ public class HexView : View, IDesignable
         AddCommand (Command.End, () => MoveEnd ());
         AddCommand (Command.LeftStart, () => MoveLeftStart ());
         AddCommand (Command.RightEnd, () => MoveEndOfLine ());
-        AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(Address - _displayStart) / BytesPerLine)));
+        AddCommand (Command.StartOfPage, () => MoveUp (BytesPerLine * ((int)(Address - Viewport.Y) / BytesPerLine)));
         AddCommand (
                     Command.EndOfPage,
-                    () => MoveDown (BytesPerLine * (Viewport.Height - 1 - (int)(Address - _displayStart) / BytesPerLine))
+                    () => MoveDown (BytesPerLine * (Viewport.Height - 1 - (int)(Address - Viewport.Y) / BytesPerLine))
                    );
         AddCommand (Command.DeleteCharLeft, () => true);
         AddCommand (Command.DeleteCharRight, () => true);
@@ -104,7 +103,13 @@ public class HexView : View, IDesignable
         KeyBindings.Remove (Key.Space);
         KeyBindings.Remove (Key.Enter);
 
-        SubviewsLaidOut += HexView_LayoutComplete;
+        SubviewsLaidOut += HexViewSubviewsLaidOut;
+    }
+
+    private void HexViewSubviewsLaidOut (object? sender, LayoutEventArgs e)
+    {
+        SetBytesPerLine ();
+        SetContentSize (new (GetLeftSideStartColumn () + BytesPerLine / NUM_BYTES_PER_HEX_COLUMN * HEX_COLUMN_WIDTH + BytesPerLine - 1, (int)((GetEditedSize ()) / BytesPerLine) + 1));
     }
 
     /// <summary>Initializes a <see cref="HexView"/> class.</summary>
@@ -118,44 +123,80 @@ public class HexView : View, IDesignable
     public bool AllowEdits { get; set; } = true;
 
     /// <summary>Gets the current edit position.</summary>
-    public Point Position
+    /// <param name="address"></param>
+    public Point GetPosition (long address)
     {
-        get
+        if (_source is null || BytesPerLine == 0)
         {
-            if (_source is null || BytesPerLine == 0)
-            {
-                return Point.Empty;
-            }
+            return Point.Empty;
+        }
+
+        var line = address / BytesPerLine;
+        var item = address % BytesPerLine;
 
-            var delta = (int)Address;
+        return new ((int)item, (int)line);
+    }
 
-            int line = delta / BytesPerLine;
-            int item = delta % BytesPerLine;
+    /// <summary>Gets cursor location, given an address.</summary>
+    /// <param name="address"></param>
+    public Point GetCursor (long address)
+    {
+        Point position = GetPosition (address);
+        if (_leftSideHasFocus)
+        {
+            int block = position.X / NUM_BYTES_PER_HEX_COLUMN;
+            int column = position.X % NUM_BYTES_PER_HEX_COLUMN;
 
-            return new (item, line);
+            position.X = block * HEX_COLUMN_WIDTH + column * 3 + (_firstNibble ? 0 : 1);
+        }
+        else
+        {
+            position.X += BytesPerLine / NUM_BYTES_PER_HEX_COLUMN * HEX_COLUMN_WIDTH - 1;
         }
+        position.X += GetLeftSideStartColumn ();
+
+        position.Offset (-Viewport.X, -Viewport.Y);
+        return position;
     }
 
-    ///<inheritdoc/>
-    public override Point? PositionCursor ()
+    private void ScrollToMakeCursorVisible (Point offsetToNewCursor)
     {
-        var delta = (int)(Address - _displayStart);
-        int line = delta / BytesPerLine;
-        int item = delta % BytesPerLine;
-        int block = item / NUM_BYTES_PER_HEX_COLUMN;
-        int column = item % NUM_BYTES_PER_HEX_COLUMN * 3;
-
-        int x = GetLeftSideStartColumn () + block * HEX_COLUMN_WIDTH + column + (_firstNibble ? 0 : 1);
-        int y = line;
+        // Adjust vertical scrolling
+        if (offsetToNewCursor.Y < 1)
+        {
+            ScrollVertical (offsetToNewCursor.Y);
+        }
+        else if (offsetToNewCursor.Y >= Viewport.Height)
+        {
+            ScrollVertical (offsetToNewCursor.Y);
+        }
 
-        if (!_leftSideHasFocus)
+        if (offsetToNewCursor.X < 1)
+        {
+            ScrollHorizontal(offsetToNewCursor.X);
+        }
+        else if (offsetToNewCursor.X >= Viewport.Width)
         {
-            x = GetLeftSideStartColumn () + BytesPerLine / NUM_BYTES_PER_HEX_COLUMN * HEX_COLUMN_WIDTH + item - 1;
+            ScrollHorizontal (offsetToNewCursor.X);
         }
+    }
 
-        Move (x, y);
+    ///<inheritdoc/>
+    public override Point? PositionCursor ()
+    {
+        Point position = GetCursor (Address);
 
-        return new (x, y);
+        if (HasFocus
+            && position.X >= 0
+            && position.X < Viewport.Width
+            && position.Y >= 0
+            && position.Y < Viewport.Height)
+        {
+            Move (position.X, position.Y);
+
+            return position;
+        }
+        return null;
     }
 
     private SortedDictionary<long, byte> _edits = [];
@@ -167,6 +208,51 @@ public class HexView : View, IDesignable
     /// <value>The edits.</value>
     public IReadOnlyDictionary<long, byte> Edits => _edits;
 
+    private long GetEditedSize ()
+    {
+        if (_edits.Count == 0)
+        {
+            return _source!.Length;
+        }
+
+        long maxEditAddress = _edits.Keys.Max ();
+
+        return Math.Max (_source!.Length, maxEditAddress + 1);
+    }
+
+
+    /// <summary>
+    ///     Applies and edits made to the <see cref="Stream"/> and resets the contents of the
+    ///     <see cref="Edits"/> property.
+    /// </summary>
+    /// <param name="stream">If provided also applies the changes to the passed <see cref="Stream"/>.</param>
+    /// .
+    public void ApplyEdits (Stream? stream = null)
+    {
+        foreach (KeyValuePair<long, byte> kv in _edits)
+        {
+            _source!.Position = kv.Key;
+            _source.WriteByte (kv.Value);
+            _source.Flush ();
+
+            if (stream is { })
+            {
+                stream.Position = kv.Key;
+                stream.WriteByte (kv.Value);
+                stream.Flush ();
+            }
+        }
+
+        _edits = new ();
+        SetNeedsDraw ();
+    }
+
+    /// <summary>
+    ///     Discards the edits made to the <see cref="Stream"/> by resetting the contents of the
+    ///     <see cref="Edits"/> property.
+    /// </summary>
+    public void DiscardEdits () { _edits = new (); }
+
     private Stream? _source;
 
     /// <summary>
@@ -186,12 +272,9 @@ public class HexView : View, IDesignable
                 throw new ArgumentException (@"The source stream must be seekable (CanSeek property)");
             }
 
+            DiscardEdits ();
             _source = value;
-
-            if (_displayStart > _source.Length)
-            {
-                DisplayStart = 0;
-            }
+            SetBytesPerLine ();
 
             if (Address > _source.Length)
             {
@@ -229,29 +312,15 @@ public class HexView : View, IDesignable
                 return;
             }
 
-            //ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual (value, Source!.Length, $"Position");
-
-            _address = value;
-            RaisePositionChanged ();
-        }
-    }
+            long newAddress = Math.Clamp (value, 0, GetEditedSize ());
 
-    private long _displayStart;
+            Point offsetToNewCursor = GetCursor (newAddress);
 
-    // TODO: Use Viewport content scrolling instead
-    /// <summary>
-    ///     Sets or gets the offset into the <see cref="Stream"/> that will be displayed at the top of the
-    ///     <see cref="HexView"/>.
-    /// </summary>
-    /// <value>The display start.</value>
-    public long DisplayStart
-    {
-        get => _displayStart;
-        set
-        {
-            Address = value;
+            _address = newAddress;
 
-            SetDisplayStart (value);
+            // Ensure the new cursor position is visible
+            ScrollToMakeCursorVisible (offsetToNewCursor);
+            RaisePositionChanged ();
         }
     }
 
@@ -278,56 +347,6 @@ public class HexView : View, IDesignable
 
     private int GetLeftSideStartColumn () { return AddressWidth == 0 ? 0 : AddressWidth + 1; }
 
-    internal void SetDisplayStart (long value)
-    {
-        if (value > 0 && value >= _source?.Length)
-        {
-            _displayStart = _source.Length - 1;
-        }
-        else if (value < 0)
-        {
-            _displayStart = 0;
-        }
-        else
-        {
-            _displayStart = value;
-        }
-
-        SetNeedsDraw ();
-    }
-
-    /// <summary>
-    ///     Applies and edits made to the <see cref="Stream"/> and resets the contents of the
-    ///     <see cref="Edits"/> property.
-    /// </summary>
-    /// <param name="stream">If provided also applies the changes to the passed <see cref="Stream"/>.</param>
-    /// .
-    public void ApplyEdits (Stream? stream = null)
-    {
-        foreach (KeyValuePair<long, byte> kv in _edits)
-        {
-            _source!.Position = kv.Key;
-            _source.WriteByte (kv.Value);
-            _source.Flush ();
-
-            if (stream is { })
-            {
-                stream.Position = kv.Key;
-                stream.WriteByte (kv.Value);
-                stream.Flush ();
-            }
-        }
-
-        _edits = new ();
-        SetNeedsDraw ();
-    }
-
-    /// <summary>
-    ///     Discards the edits made to the <see cref="Stream"/> by resetting the contents of the
-    ///     <see cref="Edits"/> property.
-    /// </summary>
-    public void DiscardEdits () { _edits = new (); }
-
     /// <inheritdoc/>
     protected override bool OnMouseEvent (MouseEventArgs me)
     {
@@ -351,14 +370,14 @@ public class HexView : View, IDesignable
 
         if (me.Flags == MouseFlags.WheeledDown)
         {
-            DisplayStart = Math.Min (DisplayStart + BytesPerLine, GetEditedSize ());
+            ScrollVertical (1);
 
             return true;
         }
 
         if (me.Flags == MouseFlags.WheeledUp)
         {
-            DisplayStart = Math.Max (DisplayStart - BytesPerLine, 0);
+            ScrollVertical (-1);
 
             return true;
         }
@@ -368,8 +387,8 @@ public class HexView : View, IDesignable
             return true;
         }
 
-        int nblocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN;
-        int blocksSize = nblocks * HEX_COLUMN_WIDTH;
+        int blocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN;
+        int blocksSize = blocks * HEX_COLUMN_WIDTH;
         int blocksRightOffset = GetLeftSideStartColumn () + blocksSize - 1;
 
         if (me.Position.X > blocksRightOffset + BytesPerLine - 1)
@@ -378,7 +397,7 @@ public class HexView : View, IDesignable
         }
 
         bool clickIsOnLeftSide = me.Position.X >= blocksRightOffset;
-        long lineStart = me.Position.Y * BytesPerLine + _displayStart;
+        long lineStart = me.Position.Y * BytesPerLine + Viewport.Y * BytesPerLine;
         int x = me.Position.X - GetLeftSideStartColumn () + 1;
         int block = x / HEX_COLUMN_WIDTH;
         x -= block * 2;
@@ -413,10 +432,9 @@ public class HexView : View, IDesignable
             {
                 _firstNibble = true;
             }
+            SetNeedsDraw ();
         }
 
-        SetNeedsDraw ();
-
         return true;
     }
 
@@ -431,37 +449,40 @@ public class HexView : View, IDesignable
         Attribute currentAttribute = Attribute.Default;
         Attribute current = GetFocusColor ();
         SetAttribute (current);
-        Move (0, 0);
+        Move (-Viewport.X, 0);
+
+        long addressOfFirstLine = Viewport.Y * BytesPerLine;
 
         int nBlocks = BytesPerLine / NUM_BYTES_PER_HEX_COLUMN;
         var data = new byte [nBlocks * NUM_BYTES_PER_HEX_COLUMN * Viewport.Height];
-        Source.Position = _displayStart;
-        int n = _source!.Read (data, 0, data.Length);
+        Source.Position = addressOfFirstLine;
+        long bytesRead = Source!.Read (data, 0, data.Length);
 
         Attribute selectedAttribute = GetHotNormalColor ();
         Attribute editedAttribute = new Attribute (GetNormalColor ().Foreground.GetHighlightColor (), GetNormalColor ().Background);
         Attribute editingAttribute = new Attribute (GetFocusColor ().Background, GetFocusColor ().Foreground);
+        Attribute addressAttribute = new Attribute (GetNormalColor ().Foreground.GetHighlightColor (), GetNormalColor ().Background);
         for (var line = 0; line < Viewport.Height; line++)
         {
-            Rectangle lineRect = new (0, line, Viewport.Width, 1);
+            Move (-Viewport.X, line);
+            long addressOfLine = addressOfFirstLine + line * nBlocks * NUM_BYTES_PER_HEX_COLUMN;
 
-            if (!Viewport.Contains (lineRect))
+            if (addressOfLine <= GetEditedSize ())
             {
-                continue;
+                SetAttribute (addressAttribute);
             }
-
-            Move (0, line);
-            currentAttribute = new Attribute (GetNormalColor ().Foreground.GetHighlightColor (), GetNormalColor ().Background);
-            SetAttribute (currentAttribute);
-            var address = $"{_displayStart + line * nBlocks * NUM_BYTES_PER_HEX_COLUMN:x8}";
-            Driver?.AddStr ($"{address.Substring (8 - AddressWidth)}");
-
-            if (AddressWidth > 0)
+            else
             {
-                Driver?.AddStr (" ");
+                SetAttribute (new Attribute (GetNormalColor ().Background.GetHighlightColor (), addressAttribute.Background));
             }
+            var address = $"{addressOfLine:x8}";
+            AddStr ($"{address.Substring (8 - AddressWidth)}");
 
             SetAttribute (GetNormalColor ());
+            if (AddressWidth > 0)
+            {
+                AddStr (" ");
+            }
 
             for (var block = 0; block < nBlocks; block++)
             {
@@ -470,7 +491,7 @@ public class HexView : View, IDesignable
                     int offset = line * nBlocks * NUM_BYTES_PER_HEX_COLUMN + block * NUM_BYTES_PER_HEX_COLUMN + b;
                     byte value = GetData (data, offset, out bool edited);
 
-                    if (offset + _displayStart == Address)
+                    if (offset + addressOfFirstLine == Address)
                     {
                         // Selected
                         SetAttribute (_leftSideHasFocus ? editingAttribute : (edited ? editedAttribute : selectedAttribute));
@@ -480,12 +501,12 @@ public class HexView : View, IDesignable
                         SetAttribute (edited ? editedAttribute : GetNormalColor ());
                     }
 
-                    Driver?.AddStr (offset >= n && !edited ? "  " : $"{value:x2}");
+                    AddStr (offset >= bytesRead && !edited ? "  " : $"{value:x2}");
                     SetAttribute (GetNormalColor ());
-                    Driver?.AddRune (_spaceCharRune);
+                    AddRune (_spaceCharRune);
                 }
 
-                Driver?.AddStr (block + 1 == nBlocks ? " " : $"{_columnSeparatorRune} ");
+                AddStr (block + 1 == nBlocks ? " " : $"{_columnSeparatorRune} ");
             }
 
             for (var byteIndex = 0; byteIndex < nBlocks * NUM_BYTES_PER_HEX_COLUMN; byteIndex++)
@@ -496,7 +517,7 @@ public class HexView : View, IDesignable
 
                 var utf8BytesConsumed = 0;
 
-                if (offset >= n && !edited)
+                if (offset >= bytesRead && !edited)
                 {
                     c = _spaceCharRune;
                 }
@@ -528,7 +549,7 @@ public class HexView : View, IDesignable
                     }
                 }
 
-                if (offset + _displayStart == Address)
+                if (offset + Source.Position == Address)
                 {
                     // Selected
                     SetAttribute (_leftSideHasFocus ? editingAttribute : (edited ? editedAttribute : selectedAttribute));
@@ -538,26 +559,17 @@ public class HexView : View, IDesignable
                     SetAttribute (edited ? editedAttribute : GetNormalColor ());
                 }
 
-                Driver?.AddRune (c);
+                AddRune (c);
 
                 for (var i = 1; i < utf8BytesConsumed; i++)
                 {
                     byteIndex++;
-                    Driver?.AddRune (_periodCharRune);
+                    AddRune (_periodCharRune);
                 }
             }
         }
 
         return true;
-
-        void SetAttribute (Attribute attribute)
-        {
-            if (currentAttribute != attribute)
-            {
-                currentAttribute = attribute;
-                SetAttribute (attribute);
-            }
-        }
     }
 
     /// <summary>Raises the <see cref="Edited"/> event.</summary>
@@ -576,24 +588,22 @@ public class HexView : View, IDesignable
     protected virtual void OnEdited (HexViewEditEventArgs e) { }
 
     /// <summary>
-    ///     Call this when <see cref="Position"/> (and <see cref="Address"/>) has changed. Raises the
+    ///     Call this when the position (see <see cref="GetPosition"/>) and <see cref="Address"/> have changed. Raises the
     ///     <see cref="PositionChanged"/> event.
     /// </summary>
     protected void RaisePositionChanged ()
     {
-        SetNeedsDraw ();
-
-        HexViewEventArgs args = new (Address, Position, BytesPerLine);
+        HexViewEventArgs args = new (Address, GetPosition (Address), BytesPerLine);
         OnPositionChanged (args);
         PositionChanged?.Invoke (this, args);
     }
 
     /// <summary>
-    ///     Called when <see cref="Position"/> (and <see cref="Address"/>) has changed.
+    ///     Called when the position (see <see cref="GetPosition"/>) and <see cref="Address"/> have changed.
     /// </summary>
     protected virtual void OnPositionChanged (HexViewEventArgs e) { }
 
-    /// <summary>Raised when <see cref="Position"/> (and <see cref="Address"/>) has changed.</summary>
+    /// <summary>Raised when the position (see <see cref="GetPosition"/>) and <see cref="Address"/> have changed.</summary>
     public event EventHandler<HexViewEventArgs>? PositionChanged;
 
     /// <inheritdoc/>
@@ -642,9 +652,6 @@ public class HexView : View, IDesignable
                 b = (byte)_source.ReadByte ();
             }
 
-            // BUGBUG: This makes no sense here.
-            RedisplayLine (Address);
-
             if (_firstNibble)
             {
                 _firstNibble = false;
@@ -701,13 +708,13 @@ public class HexView : View, IDesignable
     //
     // This is used to support editing of the buffer on a peer List<>,
     // the offset corresponds to an offset relative to DisplayStart, and
-    // the buffer contains the contents of a screenful of data, so the
+    // the buffer contains the contents of a Viewport of data, so the
     // offset is relative to the buffer.
     //
     // 
     private byte GetData (byte [] buffer, int offset, out bool edited)
     {
-        long pos = DisplayStart + offset;
+        long pos = Viewport.Y * BytesPerLine + offset;
 
         if (_edits.TryGetValue (pos, out byte v))
         {
@@ -726,7 +733,7 @@ public class HexView : View, IDesignable
         var returnBytes = new byte [count];
         edited = false;
 
-        long pos = DisplayStart + offset;
+        long pos = Viewport.Y + offset;
         for (long i = pos; i < pos + count; i++)
         {
             if (_edits.TryGetValue (i, out byte v))
@@ -746,7 +753,7 @@ public class HexView : View, IDesignable
         return returnBytes;
     }
 
-    private void HexView_LayoutComplete (object? sender, LayoutEventArgs e)
+    private void SetBytesPerLine ()
     {
         // Small buffers will just show the position, with the bsize field value (4 bytes)
         BytesPerLine = NUM_BYTES_PER_HEX_COLUMN;
@@ -761,16 +768,14 @@ public class HexView : View, IDesignable
 
     private bool MoveDown (int bytes)
     {
-        RedisplayLine (Address);
-
         if (Address + bytes < GetEditedSize ())
         {
             // We can move down lines cleanly (without extending stream)
             Address += bytes;
         }
-        else if ((bytes == BytesPerLine * Viewport.Height && _source!.Length >= DisplayStart + BytesPerLine * Viewport.Height)
+        else if ((bytes == BytesPerLine * Viewport.Height && _source!.Length >= Viewport.Y * BytesPerLine + BytesPerLine * Viewport.Height)
                  || (bytes <= BytesPerLine * Viewport.Height - BytesPerLine
-                     && _source!.Length <= DisplayStart + BytesPerLine * Viewport.Height))
+                     && _source!.Length <= Viewport.Y * BytesPerLine + BytesPerLine * Viewport.Height))
         {
             long p = Address;
 
@@ -783,16 +788,6 @@ public class HexView : View, IDesignable
             Address = p;
         }
 
-        if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
-        {
-            SetDisplayStart (DisplayStart + bytes);
-            SetNeedsDraw ();
-        }
-        else
-        {
-            RedisplayLine (Address);
-        }
-
         return true;
     }
 
@@ -800,17 +795,6 @@ public class HexView : View, IDesignable
     {
         // This lets address go past the end of the stream one, enabling adding to the stream.
         Address = GetEditedSize ();
-
-        if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
-        {
-            SetDisplayStart (Address);
-            SetNeedsDraw ();
-        }
-        else
-        {
-            RedisplayLine (Address);
-        }
-
         return true;
     }
 
@@ -818,23 +802,17 @@ public class HexView : View, IDesignable
     {
         // This lets address go past the end of the stream one, enabling adding to the stream.
         Address = Math.Min (Address / BytesPerLine * BytesPerLine + BytesPerLine - 1, GetEditedSize ());
-        SetNeedsDraw ();
-
         return true;
     }
 
     private bool MoveHome ()
     {
-        DisplayStart = 0;
-        SetNeedsDraw ();
-
+        Address = 0;
         return true;
     }
 
     private bool MoveLeft ()
     {
-        RedisplayLine (Address);
-
         if (_leftSideHasFocus)
         {
             if (!_firstNibble)
@@ -852,16 +830,6 @@ public class HexView : View, IDesignable
             return true;
         }
 
-        if (Address - 1 < DisplayStart)
-        {
-            SetDisplayStart (_displayStart - BytesPerLine);
-            SetNeedsDraw ();
-        }
-        else
-        {
-            RedisplayLine (Address);
-        }
-
         Address--;
 
         return true;
@@ -869,8 +837,6 @@ public class HexView : View, IDesignable
 
     private bool MoveRight ()
     {
-        RedisplayLine (Address);
-
         if (_leftSideHasFocus)
         {
             if (_firstNibble)
@@ -889,74 +855,24 @@ public class HexView : View, IDesignable
             Address++;
         }
 
-        if (Address >= DisplayStart + BytesPerLine * Viewport.Height)
-        {
-            SetDisplayStart (DisplayStart + BytesPerLine);
-            SetNeedsDraw ();
-        }
-        else
-        {
-            RedisplayLine (Address);
-        }
-
         return true;
     }
 
-    private long GetEditedSize ()
-    {
-        if (_edits.Count == 0)
-        {
-            return _source!.Length;
-        }
-
-        long maxEditAddress = _edits.Keys.Max ();
-
-        return Math.Max (_source!.Length, maxEditAddress + 1);
-    }
 
     private bool MoveLeftStart ()
     {
         Address = Address / BytesPerLine * BytesPerLine;
-        SetNeedsDraw ();
 
         return true;
     }
 
     private bool MoveUp (int bytes)
     {
-        RedisplayLine (Address);
-
-        if (Address - bytes > -1)
-        {
-            Address -= bytes;
-        }
-
-        if (Address < DisplayStart)
-        {
-            SetDisplayStart (DisplayStart - bytes);
-            SetNeedsDraw ();
-        }
-        else
-        {
-            RedisplayLine (Address);
-        }
+        Address -= bytes;
 
         return true;
     }
 
-    private void RedisplayLine (long pos)
-    {
-        if (BytesPerLine == 0)
-        {
-            return;
-        }
-
-        var delta = (int)(pos - DisplayStart);
-        int line = delta / BytesPerLine;
-
-        SetNeedsDraw (new (0, line, Viewport.Width, 1));
-    }
-
     /// <inheritdoc />
     protected override bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior)
     {
@@ -969,8 +885,8 @@ public class HexView : View, IDesignable
             || (direction == NavigationDirection.Backward && !_leftSideHasFocus))
         {
             _leftSideHasFocus = !_leftSideHasFocus;
-            RedisplayLine (Address);
             _firstNibble = true;
+            SetNeedsDraw ();
 
             return true;
         }

+ 63 - 0
Terminal.Gui/Views/IListDataSource.cs

@@ -0,0 +1,63 @@
+#nullable enable
+using System.Collections;
+using System.Collections.Specialized;
+
+namespace Terminal.Gui;
+
+/// <summary>Implement <see cref="IListDataSource"/> to provide custom rendering for a <see cref="ListView"/>.</summary>
+public interface IListDataSource : IDisposable
+{
+    /// <summary>
+    /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed.
+    /// </summary>
+    event NotifyCollectionChangedEventHandler CollectionChanged;
+
+    /// <summary>Returns the number of elements to display</summary>
+    int Count { get; }
+
+    /// <summary>Returns the maximum length of elements to display</summary>
+    int Length { get; }
+
+    /// <summary>
+    /// Allow suspending the <see cref="CollectionChanged"/> event from being invoked,
+    /// if <see langword="true"/>, otherwise is <see langword="false"/>.
+    /// </summary>
+    bool SuspendCollectionChangedEvent { get; set; }
+
+    /// <summary>Should return whether the specified item is currently marked.</summary>
+    /// <returns><see langword="true"/>, if marked, <see langword="false"/> otherwise.</returns>
+    /// <param name="item">Item index.</param>
+    bool IsMarked (int item);
+
+    /// <summary>This method is invoked to render a specified item, the method should cover the entire provided width.</summary>
+    /// <returns>The render.</returns>
+    /// <param name="listView">The list view to render.</param>
+    /// <param name="selected">Describes whether the item being rendered is currently selected by the user.</param>
+    /// <param name="item">The index of the item to render, zero for the first item and so on.</param>
+    /// <param name="col">The column where the rendering will start</param>
+    /// <param name="line">The line where the rendering will be done.</param>
+    /// <param name="width">The width that must be filled out.</param>
+    /// <param name="start">The index of the string to be displayed.</param>
+    /// <remarks>
+    ///     The default color will be set before this method is invoked, and will be based on whether the item is selected
+    ///     or not.
+    /// </remarks>
+    void Render (
+        ListView listView,
+        bool selected,
+        int item,
+        int col,
+        int line,
+        int width,
+        int start = 0
+    );
+
+    /// <summary>Flags the item as marked.</summary>
+    /// <param name="item">Item index.</param>
+    /// <param name="value">If set to <see langword="true"/> value.</param>
+    void SetMark (int item, bool value);
+
+    /// <summary>Return the source as IList.</summary>
+    /// <returns></returns>
+    IList ToList ();
+}

+ 28 - 82
Terminal.Gui/Views/ListView.cs

@@ -1,68 +1,9 @@
 using System.Collections;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
-using static Terminal.Gui.SpinnerStyle;
 
 namespace Terminal.Gui;
 
-/// <summary>Implement <see cref="IListDataSource"/> to provide custom rendering for a <see cref="ListView"/>.</summary>
-public interface IListDataSource : IDisposable
-{
-    /// <summary>
-    /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed.
-    /// </summary>
-    event NotifyCollectionChangedEventHandler CollectionChanged;
-
-    /// <summary>Returns the number of elements to display</summary>
-    int Count { get; }
-
-    /// <summary>Returns the maximum length of elements to display</summary>
-    int Length { get; }
-
-    /// <summary>
-    /// Allow suspending the <see cref="CollectionChanged"/> event from being invoked,
-    /// if <see langword="true"/>, otherwise is <see langword="false"/>.
-    /// </summary>
-    bool SuspendCollectionChangedEvent { get; set; }
-
-    /// <summary>Should return whether the specified item is currently marked.</summary>
-    /// <returns><see langword="true"/>, if marked, <see langword="false"/> otherwise.</returns>
-    /// <param name="item">Item index.</param>
-    bool IsMarked (int item);
-
-    /// <summary>This method is invoked to render a specified item, the method should cover the entire provided width.</summary>
-    /// <returns>The render.</returns>
-    /// <param name="listView">The list view to render.</param>
-    /// <param name="selected">Describes whether the item being rendered is currently selected by the user.</param>
-    /// <param name="item">The index of the item to render, zero for the first item and so on.</param>
-    /// <param name="col">The column where the rendering will start</param>
-    /// <param name="line">The line where the rendering will be done.</param>
-    /// <param name="width">The width that must be filled out.</param>
-    /// <param name="start">The index of the string to be displayed.</param>
-    /// <remarks>
-    ///     The default color will be set before this method is invoked, and will be based on whether the item is selected
-    ///     or not.
-    /// </remarks>
-    void Render (
-        ListView listView,
-        bool selected,
-        int item,
-        int col,
-        int line,
-        int width,
-        int start = 0
-    );
-
-    /// <summary>Flags the item as marked.</summary>
-    /// <param name="item">Item index.</param>
-    /// <param name="value">If set to <see langword="true"/> value.</param>
-    void SetMark (int item, bool value);
-
-    /// <summary>Return the source as IList.</summary>
-    /// <returns></returns>
-    IList ToList ();
-}
-
 /// <summary>
 ///     ListView <see cref="View"/> renders a scrollable list of data where each item can be activated to perform an
 ///     action.
@@ -200,7 +141,7 @@ public class ListView : View, IDesignable
                                         return !SetFocus ();
                                     });
 
-        AddCommand (Command.SelectAll, (ctx) => MarkAll((bool)ctx.KeyBinding?.Context!));
+        AddCommand (Command.SelectAll, (ctx) => MarkAll ((bool)ctx.KeyBinding?.Context!));
 
         // Default keybindings for all ListViews
         KeyBindings.Add (Key.CursorUp, Command.Up);
@@ -224,13 +165,18 @@ public class ListView : View, IDesignable
         // Use the form of Add that lets us pass context to the handler
         KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, true));
         KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, false));
+    }
 
-        SubviewsLaidOut += ListView_LayoutComplete;
+    /// <inheritdoc />
+    protected override void OnViewportChanged (DrawEventArgs e)
+    {
+        SetContentSize (new Size (MaxLength, _source?.Count ?? Viewport.Height));
     }
 
-    private void ListView_LayoutComplete (object sender, LayoutEventArgs e)
+    /// <inheritdoc />
+    protected override void OnFrameChanged (in Rectangle frame)
     {
-        SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
+        EnsureSelectedItemVisible ();
     }
 
     /// <summary>Gets or sets whether this <see cref="ListView"/> allows items to be marked.</summary>
@@ -353,7 +299,7 @@ public class ListView : View, IDesignable
             SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
             if (IsInitialized)
             {
-               // Viewport = Viewport with { Y = 0 };
+                // Viewport = Viewport with { Y = 0 };
             }
 
             KeystrokeNavigator.Collection = _source?.ToList ();
@@ -453,23 +399,17 @@ public class ListView : View, IDesignable
     /// <summary>Ensures the selected item is always visible on the screen.</summary>
     public void EnsureSelectedItemVisible ()
     {
-        if (SuperView?.IsInitialized == true)
+        if (_selected == -1)
         {
-            if (_selected < Viewport.Y)
-            {
-                // TODO: The Max check here is not needed because, by default, Viewport enforces staying w/in ContentArea (View.ScrollSettings).
-                Viewport = Viewport with { Y = _selected };
-            }
-            else if (Viewport.Height > 0 && _selected >= Viewport.Y + Viewport.Height)
-            {
-                Viewport = Viewport with { Y = _selected - Viewport.Height + 1 };
-            }
-
-            SubviewLayout -= ListView_LayoutStarted;
+            return;
         }
-        else
+        if (_selected < Viewport.Y)
+        {
+            Viewport = Viewport with { Y = _selected };
+        }
+        else if (Viewport.Height > 0 && _selected >= Viewport.Y + Viewport.Height)
         {
-            SubviewLayout += ListView_LayoutStarted;
+            Viewport = Viewport with { Y = _selected - Viewport.Height + 1 };
         }
     }
 
@@ -485,7 +425,7 @@ public class ListView : View, IDesignable
             return Source.IsMarked (SelectedItem);
         }
 
-        // BUGBUG: Shouldn't this retrn Source.IsMarked (SelectedItem)
+        // BUGBUG: Shouldn't this return Source.IsMarked (SelectedItem)
 
         return false;
     }
@@ -515,7 +455,10 @@ public class ListView : View, IDesignable
 
         if (me.Flags == MouseFlags.WheeledDown)
         {
-            ScrollVertical (1);
+            if (Viewport.Y + Viewport.Height < GetContentSize ().Height)
+            {
+                ScrollVertical (1);
+            }
 
             return true;
         }
@@ -529,7 +472,10 @@ public class ListView : View, IDesignable
 
         if (me.Flags == MouseFlags.WheeledRight)
         {
-            ScrollHorizontal (1);
+            if (Viewport.X + Viewport.Width < GetContentSize ().Width)
+            {
+                ScrollHorizontal (1);
+            }
 
             return true;
         }
@@ -1209,7 +1155,7 @@ public class ListWrapper<T> : IListDataSource, IDisposable
 
         var maxLength = 0;
 
-        for (var i = 0; i < _source.Count; i++)
+        for (var i = 0; i < _source!.Count; i++)
         {
             object t = _source [i];
             int l;

+ 16 - 14
Terminal.Gui/Views/NumericUpDown.cs

@@ -92,7 +92,7 @@ public class NumericUpDown<T> : View where T : notnull
         Add (_down, _number, _up);
 
         AddCommand (
-                    Command.ScrollUp,
+                    Command.Up,
                     (ctx) =>
                     {
                         if (type == typeof (object))
@@ -100,10 +100,11 @@ public class NumericUpDown<T> : View where T : notnull
                             return false;
                         }
 
-                        if (RaiseSelecting (ctx) is true)
-                        {
-                            return true;
-                        }
+                        // BUGBUG: If this is uncommented, the numericupdown in a shortcut will not work
+                        //if (RaiseSelecting (ctx) is true)
+                        //{
+                        //    return true;
+                        //}
 
                         if (Value is { } && Increment is { })
                         {
@@ -114,7 +115,7 @@ public class NumericUpDown<T> : View where T : notnull
                     });
 
         AddCommand (
-                    Command.ScrollDown,
+                    Command.Down,
                     (ctx) =>
                     {
                         if (type == typeof (object))
@@ -122,10 +123,11 @@ public class NumericUpDown<T> : View where T : notnull
                             return false;
                         }
 
-                        if (RaiseSelecting (ctx) is true)
-                        {
-                            return true;
-                        }
+                        // BUGBUG: If this is uncommented, the numericupdown in a shortcut will not work
+                        //if (RaiseSelecting (ctx) is true)
+                        //{
+                        //    return true;
+                        //}
 
                         if (Value is { } && Increment is { })
                         {
@@ -136,8 +138,8 @@ public class NumericUpDown<T> : View where T : notnull
                         return true;
                     });
 
-        KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
-        KeyBindings.Add (Key.CursorDown, Command.ScrollDown);
+        KeyBindings.Add (Key.CursorUp, Command.Up);
+        KeyBindings.Add (Key.CursorDown, Command.Down);
 
         SetText ();
 
@@ -145,13 +147,13 @@ public class NumericUpDown<T> : View where T : notnull
 
         void OnDownButtonOnAccept (object? s, CommandEventArgs e)
         {
-            InvokeCommand (Command.ScrollDown);
+            InvokeCommand (Command.Down);
             e.Cancel = true;
         }
 
         void OnUpButtonOnAccept (object? s, CommandEventArgs e)
         {
-            InvokeCommand (Command.ScrollUp);
+            InvokeCommand (Command.Up);
             e.Cancel = true;
         }
     }

+ 628 - 0
Terminal.Gui/Views/ScrollBar/ScrollBar.cs

@@ -0,0 +1,628 @@
+#nullable enable
+
+using System.ComponentModel;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Indicates the size of scrollable content and controls the position of the visible content, either vertically or
+///     horizontally.
+///     Two <see cref="Button"/>s are provided, one to scroll up or left and one to scroll down or right. Between the
+///     buttons is a <see cref="ScrollSlider"/> that can be dragged to
+///     control the position of the visible content. The ScrollSlier is sized to show the proportion of the scrollable
+///     content to the size of the <see cref="View.Viewport"/>.
+/// </summary>
+/// <remarks>
+///     <para>
+///         See the <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/scrolling.html">Scrolling Deep Dive</see>.
+///     </para>
+///     <para>
+///         By default, the built-in View scrollbars (<see cref="View.VerticalScrollBar"/>/
+///         <see cref="View.HorizontalScrollBar"/>) have both <see cref="View.Visible"/> and <see cref="AutoShow"/> set to
+///         <see langword="false"/>.
+///         To enable them, either set <see cref="AutoShow"/> set to <see langword="true"/> or explicitly set
+///         <see cref="View.Visible"/>
+///         to <see langword="true"/>.
+///     </para>
+///     <para>
+///         By default, this view cannot be focused and does not support keyboard input.
+///     </para>
+/// </remarks>
+public class ScrollBar : View, IOrientation, IDesignable
+{
+    private readonly Button _decreaseButton;
+    private readonly ScrollSlider _slider;
+    private readonly Button _increaseButton;
+
+    /// <inheritdoc/>
+    public ScrollBar ()
+    {
+        // Set the default width and height based on the orientation - fill Viewport
+        Width = Dim.Auto (
+                          DimAutoStyle.Content,
+                          Dim.Func (() => Orientation == Orientation.Vertical ? 1 : SuperView?.Viewport.Width ?? 0));
+
+        Height = Dim.Auto (
+                           DimAutoStyle.Content,
+                           Dim.Func (() => Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 0 : 1));
+
+        _decreaseButton = new ()
+        {
+            CanFocus = false,
+            NoDecorations = true,
+            NoPadding = true,
+            ShadowStyle = ShadowStyle.None,
+            WantContinuousButtonPressed = true
+        };
+        _decreaseButton.Accepting += OnDecreaseButtonOnAccept;
+
+        _slider = new ()
+        {
+            SliderPadding = 2 // For the buttons
+        };
+        _slider.Scrolled += SliderOnScroll;
+        _slider.PositionChanged += SliderOnPositionChanged;
+
+        _increaseButton = new ()
+        {
+            CanFocus = false,
+            NoDecorations = true,
+            NoPadding = true,
+            ShadowStyle = ShadowStyle.None,
+            WantContinuousButtonPressed = true
+        };
+        _increaseButton.Accepting += OnIncreaseButtonOnAccept;
+        Add (_decreaseButton, _slider, _increaseButton);
+
+        CanFocus = false;
+
+        _orientationHelper = new (this); // Do not use object initializer!
+        _orientationHelper.Orientation = Orientation.Vertical;
+
+        // This sets the width/height etc...
+        OnOrientationChanged (Orientation);
+
+        return;
+
+        void OnDecreaseButtonOnAccept (object? s, CommandEventArgs e)
+        {
+            Position -= Increment;
+            e.Cancel = true;
+        }
+
+        void OnIncreaseButtonOnAccept (object? s, CommandEventArgs e)
+        {
+            Position += Increment;
+            e.Cancel = true;
+        }
+    }
+
+    /// <inheritdoc/>
+    protected override void OnFrameChanged (in Rectangle frame) { ShowHide (); }
+
+    private void ShowHide ()
+    {
+        if (AutoShow)
+        {
+            Visible = VisibleContentSize < ScrollableContentSize;
+        }
+
+        _slider.VisibleContentSize = VisibleContentSize;
+        _slider.Size = CalculateSliderSize ();
+        _sliderPosition = CalculateSliderPositionFromContentPosition (_position);
+        _slider.Position = _sliderPosition.Value;
+    }
+
+    private void PositionSubviews ()
+    {
+        if (Orientation == Orientation.Vertical)
+        {
+            _decreaseButton.Y = 0;
+            _decreaseButton.X = 0;
+            _decreaseButton.Width = Dim.Fill ();
+            _decreaseButton.Height = 1;
+            _decreaseButton.Title = Glyphs.UpArrow.ToString ();
+
+            _slider.X = 0;
+            _slider.Y = 1;
+            _slider.Width = Dim.Fill ();
+
+            _increaseButton.Y = Pos.AnchorEnd ();
+            _increaseButton.X = 0;
+            _increaseButton.Width = Dim.Fill ();
+            _increaseButton.Height = 1;
+            _increaseButton.Title = Glyphs.DownArrow.ToString ();
+        }
+        else
+        {
+            _decreaseButton.Y = 0;
+            _decreaseButton.X = 0;
+            _decreaseButton.Width = 1;
+            _decreaseButton.Height = Dim.Fill ();
+            _decreaseButton.Title = Glyphs.LeftArrow.ToString ();
+
+            _slider.Y = 0;
+            _slider.X = 1;
+            _slider.Height = Dim.Fill ();
+
+            _increaseButton.Y = 0;
+            _increaseButton.X = Pos.AnchorEnd ();
+            _increaseButton.Width = 1;
+            _increaseButton.Height = Dim.Fill ();
+            _increaseButton.Title = Glyphs.RightArrow.ToString ();
+        }
+    }
+
+    #region IOrientation members
+
+    private readonly OrientationHelper _orientationHelper;
+
+    /// <inheritdoc/>
+    public Orientation Orientation
+    {
+        get => _orientationHelper.Orientation;
+        set => _orientationHelper.Orientation = value;
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
+
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
+
+    /// <inheritdoc/>
+    public void OnOrientationChanged (Orientation newOrientation)
+    {
+        TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
+        TextAlignment = Alignment.Center;
+        VerticalTextAlignment = Alignment.Center;
+        _slider.Orientation = newOrientation;
+        PositionSubviews ();
+
+        OrientationChanged?.Invoke (this, new (newOrientation));
+    }
+
+    #endregion
+
+    /// <summary>
+    ///     Gets or sets the amount each mouse wheel event, or click on the increment/decrement buttons, will
+    ///     incremenet/decrement the <see cref="Position"/>.
+    /// </summary>
+    /// <remarks>
+    ///     The default is 1.
+    /// </remarks>
+    public int Increment { get; set; } = 1;
+
+    // AutoShow should be false by default. Views should not be hidden by default.
+    private bool _autoShow;
+
+    /// <summary>
+    ///     Gets or sets whether <see cref="View.Visible"/> will be set to <see langword="true"/> if the dimension of the
+    ///     scroll bar is less than  <see cref="ScrollableContentSize"/> and <see langword="false"/> if greater than or equal
+    ///     to.
+    /// </summary>
+    /// <remarks>
+    ///     The default is <see langword="false"/>.
+    /// </remarks>
+    public bool AutoShow
+    {
+        get => _autoShow;
+        set
+        {
+            if (_autoShow != value)
+            {
+                _autoShow = value;
+
+                if (!AutoShow)
+                {
+                    Visible = true;
+                }
+
+                ShowHide ();
+                SetNeedsLayout ();
+            }
+        }
+    }
+
+    private int? _visibleContentSize;
+
+    /// <summary>
+    ///     Gets or sets the size of the visible viewport into the content being scrolled, bounded by
+    ///     <see cref="ScrollableContentSize"/>.
+    /// </summary>
+    /// <remarks>
+    ///     If not explicitly set, the visible content size will be appropriate dimension of the ScrollBar's Frame.
+    /// </remarks>
+    public int VisibleContentSize
+    {
+        get
+        {
+            if (_visibleContentSize.HasValue)
+            {
+                return _visibleContentSize.Value;
+            }
+
+            return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
+        }
+        set
+        {
+            _visibleContentSize = value;
+            _slider.Size = CalculateSliderSize ();
+            ShowHide ();
+        }
+    }
+
+    private int? _scrollableContentSize;
+
+    /// <summary>
+    ///     Gets or sets the size of the content that can be scrolled. This is typically set to
+    ///     <see cref="View.GetContentSize()"/>.
+    /// </summary>
+    public int ScrollableContentSize
+    {
+        get
+        {
+            if (_scrollableContentSize.HasValue)
+            {
+                return _scrollableContentSize.Value;
+            }
+
+            return Orientation == Orientation.Vertical ? SuperView?.GetContentSize ().Height ?? 0 : SuperView?.GetContentSize ().Width ?? 0;
+        }
+        set
+        {
+            if (value == _scrollableContentSize || value < 0)
+            {
+                return;
+            }
+
+            _scrollableContentSize = value;
+            _slider.Size = CalculateSliderSize ();
+            ShowHide ();
+
+            if (!Visible)
+            {
+                return;
+            }
+
+            OnSizeChanged (value);
+            ScrollableContentSizeChanged?.Invoke (this, new (in value));
+            SetNeedsLayout ();
+        }
+    }
+
+    /// <summary>Called when <see cref="ScrollableContentSize"/> has changed. </summary>
+    protected virtual void OnSizeChanged (int size) { }
+
+    /// <summary>Raised when <see cref="ScrollableContentSize"/> has changed.</summary>
+    public event EventHandler<EventArgs<int>>? ScrollableContentSizeChanged;
+
+    #region Position
+
+    private int _position;
+
+    /// <summary>
+    ///     Gets or sets the position of the slider relative to <see cref="ScrollableContentSize"/>.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         The content position is clamped to 0 and <see cref="ScrollableContentSize"/> minus
+    ///         <see cref="VisibleContentSize"/>.
+    ///     </para>
+    ///     <para>
+    ///         Setting will result in the <see cref="PositionChanging"/> and <see cref="PositionChanged"/>
+    ///         events being raised.
+    ///     </para>
+    /// </remarks>
+    public int Position
+    {
+        get => _position;
+        set
+        {
+            if (value == _position || !Visible)
+            {
+                return;
+            }
+
+            // Clamp the value between 0 and Size - VisibleContentSize
+            int newContentPosition = Math.Clamp (value, 0, Math.Max (0, ScrollableContentSize - VisibleContentSize));
+            NavigationDirection direction = newContentPosition >= _position ? NavigationDirection.Forward : NavigationDirection.Backward;
+
+            if (OnPositionChanging (_position, newContentPosition))
+            {
+                return;
+            }
+
+            CancelEventArgs<int> args = new (ref _position, ref newContentPosition);
+            PositionChanging?.Invoke (this, args);
+
+            if (args.Cancel)
+            {
+                return;
+            }
+
+            int distance = newContentPosition - _position;
+
+            if (_position == newContentPosition)
+            {
+                return;
+            }
+
+            _position = newContentPosition;
+
+            _sliderPosition = CalculateSliderPositionFromContentPosition (_position, direction);
+
+            if (_slider.Position != _sliderPosition)
+            {
+                _slider.Position = _sliderPosition.Value;
+            }
+
+            OnPositionChanged (_position);
+            PositionChanged?.Invoke (this, new (in _position));
+
+            OnScrolled (distance);
+            Scrolled?.Invoke (this, new (in distance));
+            SetNeedsLayout ();
+        }
+    }
+
+    /// <summary>
+    ///     Called when <see cref="Position"/> is changing. Return true to cancel the change.
+    /// </summary>
+    protected virtual bool OnPositionChanging (int currentPos, int newPos) { return false; }
+
+    /// <summary>
+    ///     Raised when the <see cref="Position"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
+    ///     <see langword="true"/> to prevent the position from being changed.
+    /// </summary>
+    public event EventHandler<CancelEventArgs<int>>? PositionChanging;
+
+    /// <summary>Called when <see cref="Position"/> has changed.</summary>
+    protected virtual void OnPositionChanged (int position) { }
+
+    /// <summary>Raised when the <see cref="Position"/> has changed.</summary>
+    public event EventHandler<EventArgs<int>>? PositionChanged;
+
+    /// <summary>Called when <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
+    protected virtual void OnScrolled (int distance) { }
+
+    /// <summary>Raised when the <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
+    public event EventHandler<EventArgs<int>>? Scrolled;
+
+    /// <summary>
+    ///     INTERNAL API (for unit tests) - Calculates the position within the <see cref="ScrollableContentSize"/> based on the
+    ///     slider position.
+    /// </summary>
+    /// <remarks>
+    ///     Clamps the sliderPosition, ensuring the returned content position is always less than
+    ///     <see cref="ScrollableContentSize"/> - <see cref="VisibleContentSize"/>.
+    /// </remarks>
+    /// <param name="sliderPosition"></param>
+    /// <returns></returns>
+    internal int CalculatePositionFromSliderPosition (int sliderPosition)
+    {
+        int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
+
+        return ScrollSlider.CalculateContentPosition (ScrollableContentSize, VisibleContentSize, sliderPosition, scrollBarSize - _slider.SliderPadding);
+    }
+
+    #endregion ContentPosition
+
+    #region Slider Management
+
+    private int? _sliderPosition;
+
+    /// <summary>
+    ///     INTERNAL (for unit tests). Calculates the size of the slider based on the Orientation, VisibleContentSize, the
+    ///     actual Viewport, and Size.
+    /// </summary>
+    /// <returns></returns>
+    internal int CalculateSliderSize ()
+    {
+        int maxSliderSize = (Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width) - 2;
+
+        return ScrollSlider.CalculateSize (ScrollableContentSize, VisibleContentSize, maxSliderSize);
+    }
+
+    private void SliderOnPositionChanged (object? sender, EventArgs<int> e)
+    {
+        if (VisibleContentSize == 0)
+        {
+            return;
+        }
+
+        RaiseSliderPositionChangeEvents (_sliderPosition, e.CurrentValue);
+    }
+
+    private void SliderOnScroll (object? sender, EventArgs<int> e)
+    {
+        if (VisibleContentSize == 0)
+        {
+            return;
+        }
+
+        int calculatedSliderPos = CalculateSliderPositionFromContentPosition (
+                                                                              _position,
+                                                                              e.CurrentValue >= 0 ? NavigationDirection.Forward : NavigationDirection.Backward);
+
+        if (calculatedSliderPos == _sliderPosition)
+        {
+            return;
+        }
+
+        int sliderScrolledAmount = e.CurrentValue;
+        int calculatedPosition = CalculatePositionFromSliderPosition (calculatedSliderPos + sliderScrolledAmount);
+
+        Position = calculatedPosition;
+    }
+
+    /// <summary>
+    ///     Gets or sets the position of the start of the Scroll slider, within the Viewport.
+    /// </summary>
+    public int GetSliderPosition () { return CalculateSliderPositionFromContentPosition (_position); }
+
+    private void RaiseSliderPositionChangeEvents (int? currentSliderPosition, int newSliderPosition)
+    {
+        if (currentSliderPosition == newSliderPosition)
+        {
+            return;
+        }
+
+        _sliderPosition = newSliderPosition;
+
+        OnSliderPositionChanged (newSliderPosition);
+        SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
+    }
+
+    /// <summary>Called when the slider position has changed.</summary>
+    protected virtual void OnSliderPositionChanged (int position) { }
+
+    /// <summary>Raised when the slider position has changed.</summary>
+    public event EventHandler<EventArgs<int>>? SliderPositionChanged;
+
+    /// <summary>
+    ///     INTERNAL API (for unit tests) - Calculates the position of the slider based on the content position.
+    /// </summary>
+    /// <param name="contentPosition"></param>
+    /// <param name="direction"></param>
+    /// <returns></returns>
+    internal int CalculateSliderPositionFromContentPosition (int contentPosition, NavigationDirection direction = NavigationDirection.Forward)
+    {
+        int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
+
+        return ScrollSlider.CalculatePosition (ScrollableContentSize, VisibleContentSize, contentPosition, scrollBarSize - 2, direction);
+    }
+
+    #endregion Slider Management
+
+    /// <inheritdoc/>
+    protected override bool OnClearingViewport ()
+    {
+        if (Orientation == Orientation.Vertical)
+        {
+            FillRect (Viewport with { Y = Viewport.Y + 1, Height = Viewport.Height - 2 }, Glyphs.Stipple);
+        }
+        else
+        {
+            FillRect (Viewport with { X = Viewport.X + 1, Width = Viewport.Width - 2 }, Glyphs.Stipple);
+        }
+
+        SetNeedsDraw ();
+
+        return true;
+    }
+
+    // TODO: Change this to work OnMouseEvent with continuouse press and grab so it's continous.
+    /// <inheritdoc/>
+    protected override bool OnMouseClick (MouseEventArgs args)
+    {
+        // Check if the mouse click is a single click
+        if (!args.IsSingleClicked)
+        {
+            return false;
+        }
+
+        int sliderCenter;
+        int distanceFromCenter;
+
+        if (Orientation == Orientation.Vertical)
+        {
+            sliderCenter = 1 + _slider.Frame.Y + _slider.Frame.Height / 2;
+            distanceFromCenter = args.Position.Y - sliderCenter;
+        }
+        else
+        {
+            sliderCenter = 1 + _slider.Frame.X + _slider.Frame.Width / 2;
+            distanceFromCenter = args.Position.X - sliderCenter;
+        }
+
+#if PROPORTIONAL_SCROLL_JUMP
+        // TODO: This logic mostly works to provide a proportional jump. However, the math
+        // TODO: falls apart in edge cases. Most other scroll bars (e.g. Windows) do not do proportional
+        // TODO: Thus, this is disabled; we just jump a page each click.
+        // Ratio of the distance to the viewport dimension
+        double ratio = (double)Math.Abs (distanceFromCenter) / (VisibleContentSize);
+        // Jump size based on the ratio and the total content size
+        int jump = (int)(ratio * (Size - VisibleContentSize));
+#else
+        int jump = VisibleContentSize;
+#endif
+
+        // Adjust the content position based on the distance
+        if (distanceFromCenter < 0)
+        {
+            Position = Math.Max (0, Position - jump);
+        }
+        else
+        {
+            Position = Math.Min (ScrollableContentSize - _slider.VisibleContentSize, Position + jump);
+        }
+
+        return true;
+    }
+
+    /// <inheritdoc/>
+    protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
+    {
+        if (SuperView is null)
+        {
+            return false;
+        }
+
+        if (!mouseEvent.IsWheel)
+        {
+            return false;
+        }
+
+        if (Orientation == Orientation.Vertical)
+        {
+            if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown))
+            {
+                Position += Increment;
+            }
+
+            if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp))
+            {
+                Position -= Increment;
+            }
+        }
+        else
+        {
+            if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight))
+            {
+                Position += Increment;
+            }
+
+            if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft))
+            {
+                Position -= Increment;
+            }
+        }
+
+        return true;
+    }
+
+    /// <inheritdoc/>
+    public bool EnableForDesign ()
+    {
+        OrientationChanged += (sender, args) =>
+                              {
+                                  if (args.CurrentValue == Orientation.Vertical)
+                                  {
+                                      Width = 1;
+                                      Height = Dim.Fill ();
+                                  }
+                                  else
+                                  {
+                                      Width = Dim.Fill ();
+                                      Height = 1;
+                                  }
+                              };
+
+        Width = 1;
+        Height = Dim.Fill ();
+        ScrollableContentSize = 250;
+
+        return true;
+    }
+}

+ 435 - 0
Terminal.Gui/Views/ScrollBar/ScrollSlider.cs

@@ -0,0 +1,435 @@
+#nullable enable
+
+using System.ComponentModel;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     The ScrollSlider can be dragged with the mouse, constrained by the size of the Viewport of it's superview. The
+///     ScrollSlider can be
+///     oriented either vertically or horizontally.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Used to represent the proportion of the visible content to the Viewport in a <see cref="Scrolled"/>.
+///     </para>
+/// </remarks>
+public class ScrollSlider : View, IOrientation, IDesignable
+{
+    /// <summary>
+    ///     Initializes a new instance.
+    /// </summary>
+    public ScrollSlider ()
+    {
+        Id = "scrollSlider";
+        WantMousePositionReports = true;
+
+        _orientationHelper = new (this); // Do not use object initializer!
+        _orientationHelper.Orientation = Orientation.Vertical;
+        _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
+        _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
+
+        OnOrientationChanged (Orientation);
+
+        HighlightStyle = HighlightStyle.Hover;
+    }
+
+    #region IOrientation members
+
+    private readonly OrientationHelper _orientationHelper;
+
+    /// <inheritdoc/>
+    public Orientation Orientation
+    {
+        get => _orientationHelper.Orientation;
+        set => _orientationHelper.Orientation = value;
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
+
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
+
+    /// <inheritdoc/>
+    public void OnOrientationChanged (Orientation newOrientation)
+    {
+        TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
+        TextAlignment = Alignment.Center;
+        VerticalTextAlignment = Alignment.Center;
+
+        // Reset Position to 0 when changing orientation
+        X = 0;
+        Y = 0;
+        Position = 0;
+
+        // Reset opposite dim to Dim.Fill ()
+        if (Orientation == Orientation.Vertical)
+        {
+            Height = Width;
+            Width = Dim.Fill ();
+        }
+        else
+        {
+            Width = Height;
+            Height = Dim.Fill ();
+        }
+        SetNeedsLayout ();
+    }
+
+    #endregion
+
+    /// <inheritdoc/>
+    protected override bool OnClearingViewport ()
+    {
+        if (Orientation == Orientation.Vertical)
+        {
+            FillRect (Viewport with { Height = Size }, Glyphs.ContinuousMeterSegment);
+        }
+        else
+        {
+            FillRect (Viewport with { Width = Size }, Glyphs.ContinuousMeterSegment);
+        }
+        return true;
+    }
+
+    private int? _size;
+
+    /// <summary>
+    ///     Gets or sets the size of the ScrollSlider. This is a helper that gets or sets Width or Height depending
+    ///     on  <see cref="Orientation"/>. The size will be clamped between 1 and the dimension of
+    ///     the <see cref="View.SuperView"/>'s Viewport.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         The dimension of the ScrollSlider that is perpendicular to the <see cref="Orientation"/> will be set to
+    ///         <see cref="Dim.Fill()"/>
+    ///     </para>
+    /// </remarks>
+    public int Size
+    {
+        get => _size ?? 1;
+        set
+        {
+            if (value == _size)
+            {
+                return;
+            }
+
+            _size = Math.Clamp (value, 1, VisibleContentSize);
+
+
+            if (Orientation == Orientation.Vertical)
+            {
+                Height = _size;
+            }
+            else
+            {
+                Width = _size;
+            }
+            SetNeedsLayout ();
+        }
+    }
+
+    private int? _visibleContentSize;
+
+    /// <summary>
+    ///     Gets or sets the size of the viewport into the content being scrolled. If not explicitly set, will be the
+    ///     greater of 1 and the dimension of the <see cref="View.SuperView"/>.
+    /// </summary>
+    public int VisibleContentSize
+    {
+        get
+        {
+            if (_visibleContentSize.HasValue)
+            {
+                return _visibleContentSize.Value;
+            }
+
+            return Math.Max (1, Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 2048 : SuperView?.Viewport.Width ?? 2048);
+        }
+        set
+        {
+            if (value == _visibleContentSize)
+            {
+                return;
+            }
+            _visibleContentSize = int.Max (1, value);
+
+            if (_position >= _visibleContentSize - _size)
+            {
+                Position = _position;
+            }
+
+            SetNeedsLayout ();
+        }
+    }
+
+    private int _position;
+
+    /// <summary>
+    ///     Gets or sets the position of the ScrollSlider relative to the size of the ScrollSlider's Frame.
+    ///     The position will be constrained such that the ScrollSlider will not go outside the Viewport of
+    ///     the <see cref="View.SuperView"/>.
+    /// </summary>
+    public int Position
+    {
+        get => _position;
+        set
+        {
+            int clampedPosition = ClampPosition (value);
+            if (_position == clampedPosition)
+            {
+                return;
+            }
+
+            RaisePositionChangeEvents (clampedPosition);
+            SetNeedsLayout ();
+        }
+    }
+
+    /// <summary>
+    ///     Moves the scroll slider to the specified position. Does not clamp.
+    /// </summary>
+    /// <param name="position"></param>
+    internal void MoveToPosition (int position)
+    {
+        if (Orientation == Orientation.Vertical)
+        {
+            Y = _position + SliderPadding / 2;
+        }
+        else
+        {
+            X = _position + SliderPadding / 2;
+        }
+    }
+
+    /// <summary>
+    ///     INTERNAL API (for unit tests) - Clamps the position such that the right side of the slider
+    ///     never goes past the edge of the Viewport.
+    /// </summary>
+    /// <param name="newPosition"></param>
+    /// <returns></returns>
+    internal int ClampPosition (int newPosition)
+    {
+        return Math.Clamp (newPosition, 0, Math.Max (SliderPadding / 2, VisibleContentSize - SliderPadding - Size));
+    }
+
+    private void RaisePositionChangeEvents (int newPosition)
+    {
+        if (OnPositionChanging (_position, newPosition))
+        {
+            return;
+        }
+
+        CancelEventArgs<int> args = new (ref _position, ref newPosition);
+        PositionChanging?.Invoke (this, args);
+
+        if (args.Cancel)
+        {
+            return;
+        }
+
+        int distance = newPosition - _position;
+        _position = ClampPosition (newPosition);
+
+        MoveToPosition (_position);
+
+        OnPositionChanged (_position);
+        PositionChanged?.Invoke (this, new (in _position));
+
+        OnScrolled (distance);
+        Scrolled?.Invoke (this, new (in distance));
+
+        RaiseSelecting (new (Command.Select, null, null, distance));
+    }
+
+    /// <summary>
+    ///     Called when <see cref="Position"/> is changing. Return true to cancel the change.
+    /// </summary>
+    protected virtual bool OnPositionChanging (int currentPos, int newPos) { return false; }
+
+    /// <summary>
+    ///     Raised when the <see cref="Position"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
+    ///     <see langword="true"/> to prevent the position from being changed.
+    /// </summary>
+    public event EventHandler<CancelEventArgs<int>>? PositionChanging;
+
+    /// <summary>Called when <see cref="Position"/> has changed.</summary>
+    protected virtual void OnPositionChanged (int position) { }
+
+    /// <summary>Raised when the <see cref="Position"/> has changed.</summary>
+    public event EventHandler<EventArgs<int>>? PositionChanged;
+
+    /// <summary>Called when <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
+    protected virtual void OnScrolled (int distance) { }
+
+    /// <summary>Raised when the <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
+    public event EventHandler<EventArgs<int>>? Scrolled;
+
+    /// <inheritdoc/>
+    public override Attribute GetNormalColor () { return base.GetHotNormalColor (); }
+
+    ///// <inheritdoc/>
+    private int _lastLocation = -1;
+
+    /// <summary>
+    ///     Gets or sets the amount to pad the start and end of the scroll slider. The default is 0.
+    /// </summary>
+    /// <remarks>
+    ///     When the scroll slider is used by <see cref="ScrollBar"/>, which has increment and decrement buttons, the
+    ///     SliderPadding should be set to the size of the buttons (typically 2).
+    /// </remarks>
+    public int SliderPadding { get; set; }
+
+    /// <inheritdoc/>
+    protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
+    {
+        if (SuperView is null)
+        {
+            return false;
+        }
+
+        if (mouseEvent.IsSingleDoubleOrTripleClicked)
+        {
+            return true;
+        }
+
+        int location = (Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X);
+        int offsetFromLastLocation = _lastLocation > -1 ? location - _lastLocation : 0;
+        int superViewDimension = VisibleContentSize;
+
+        if (mouseEvent.IsPressed || mouseEvent.IsReleased)
+        {
+            if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1)
+            {
+                if (Application.MouseGrabView != this)
+                {
+                    Application.GrabMouse (this);
+                    _lastLocation = location;
+                }
+            }
+            else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
+            {
+                int currentLocation;
+                if (Orientation == Orientation.Vertical)
+                {
+                    currentLocation = Frame.Y;
+                }
+                else
+                {
+                    currentLocation = Frame.X;
+                }
+
+                currentLocation -= SliderPadding / 2;
+                int newLocation = currentLocation + offsetFromLastLocation;
+                Position = newLocation;
+            }
+            else if (mouseEvent.Flags == MouseFlags.Button1Released)
+            {
+                _lastLocation = -1;
+
+                if (Application.MouseGrabView == this)
+                {
+                    Application.UngrabMouse ();
+                }
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     Gets the slider size.
+    /// </summary>
+    /// <param name="scrollableContentSize">The size of the content.</param>
+    /// <param name="visibleContentSize">The size of the visible content.</param>
+    /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
+    public static int CalculateSize (
+        int scrollableContentSize,
+        int visibleContentSize,
+        int sliderBounds
+    )
+    {
+        if (scrollableContentSize <= 0 || sliderBounds <= 0)
+        {
+            return 1;   // Slider must be at least 1
+        }
+
+        if (visibleContentSize <= 0 || scrollableContentSize <= visibleContentSize)
+        {
+            return sliderBounds;
+        }
+
+        double sliderSizeD = ((double)visibleContentSize / scrollableContentSize) * sliderBounds;
+
+        int sliderSize = (int)Math.Floor (sliderSizeD);
+
+        return Math.Clamp (sliderSize, 1, sliderBounds);
+    }
+
+    /// <summary>
+    ///     Calculates the slider position.
+    /// </summary>
+    /// <param name="scrollableContentSize">The size of the content.</param>
+    /// <param name="visibleContentSize">The size of the visible content.</param>
+    /// <param name="contentPosition">The position in the content (between 0 and <paramref name="scrollableContentSize"/>).</param>
+    /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
+    /// <param name="direction">The direction the slider is moving.</param>
+    internal static int CalculatePosition (
+        int scrollableContentSize,
+        int visibleContentSize,
+        int contentPosition,
+        int sliderBounds,
+        NavigationDirection direction
+    )
+    {
+        if (scrollableContentSize - visibleContentSize <= 0 || sliderBounds <= 0)
+        {
+            return 0;
+        }
+
+        int calculatedSliderSize = CalculateSize (scrollableContentSize, visibleContentSize, sliderBounds);
+
+        double newSliderPosition = ((double)contentPosition / (scrollableContentSize - visibleContentSize)) * (sliderBounds - calculatedSliderSize);
+
+        return Math.Clamp ((int)Math.Round (newSliderPosition), 0, sliderBounds - calculatedSliderSize);
+    }
+
+    /// <summary>
+    ///     Calculates the content position.
+    /// </summary>
+    /// <param name="scrollableContentSize">The size of the content.</param>
+    /// <param name="visibleContentSize">The size of the visible content.</param>
+    /// <param name="sliderPosition">The position of the slider.</param>
+    /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
+    internal static int CalculateContentPosition (
+        int scrollableContentSize,
+        int visibleContentSize,
+        int sliderPosition,
+        int sliderBounds
+    )
+    {
+        int sliderSize = CalculateSize (scrollableContentSize, visibleContentSize, sliderBounds);
+
+        double pos = ((double)(sliderPosition) / (sliderBounds - sliderSize)) * (scrollableContentSize - visibleContentSize);
+
+        if (pos is double.NaN)
+        {
+            return 0;
+        }
+        double rounded = Math.Ceiling (pos);
+
+        return (int)Math.Clamp (rounded, 0, Math.Max (0, scrollableContentSize - sliderSize));
+    }
+
+    /// <inheritdoc/>
+    public bool EnableForDesign ()
+    {
+        Size = 5;
+
+        return true;
+    }
+}

+ 0 - 1086
Terminal.Gui/Views/ScrollBarView.cs

@@ -1,1086 +0,0 @@
-//
-// ScrollBarView.cs: ScrollBarView view.
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-
-using System.Diagnostics;
-
-namespace Terminal.Gui;
-
-/// <summary>ScrollBarViews are views that display a 1-character scrollbar, either horizontal or vertical</summary>
-/// <remarks>
-///     <para>
-///         The scrollbar is drawn to be a representation of the Size, assuming that the scroll position is set at
-///         Position.
-///     </para>
-///     <para>If the region to display the scrollbar is larger than three characters, arrow indicators are drawn.</para>
-/// </remarks>
-public class ScrollBarView : View
-{
-    private bool _autoHideScrollBars = true;
-    private View _contentBottomRightCorner;
-    private bool _hosted;
-    private bool _keepContentAlwaysInViewport = true;
-    private int _lastLocation = -1;
-    private ScrollBarView _otherScrollBarView;
-    private int _posBarOffset;
-    private int _posBottomTee;
-    private int _posLeftTee;
-    private int _posRightTee;
-    private int _posTopTee;
-    private bool _showScrollIndicator;
-    private int _size, _position;
-    private bool _vertical;
-
-    /// <summary>
-    ///     Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class.
-    /// </summary>
-    public ScrollBarView ()
-    {
-        WantContinuousButtonPressed = true;
-
-        Added += (s, e) => CreateBottomRightCorner (e.SuperView);
-        Initialized += ScrollBarView_Initialized;
-    }
-
-    /// <summary>
-    ///     Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class.
-    /// </summary>
-    /// <param name="host">The view that will host this scrollbar.</param>
-    /// <param name="isVertical">If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal.</param>
-    /// <param name="showBothScrollIndicator">
-    ///     If set to <c>true (default)</c> will have the other scrollbar, otherwise will
-    ///     have only one.
-    /// </param>
-    public ScrollBarView (View host, bool isVertical, bool showBothScrollIndicator = true)
-    {
-        if (host is null)
-        {
-            throw new ArgumentNullException ("The host parameter can't be null.");
-        }
-
-        if (host.SuperView is null)
-        {
-            throw new ArgumentNullException ("The host SuperView parameter can't be null.");
-        }
-
-        _hosted = true;
-        IsVertical = isVertical;
-        ColorScheme = host.ColorScheme;
-        X = isVertical ? Pos.Right (host) - 1 : Pos.Left (host);
-        Y = isVertical ? Pos.Top (host) : Pos.Bottom (host) - 1;
-        Host = host;
-        CanFocus = false;
-        Enabled = host.Enabled;
-        Visible = host.Visible;
-        Initialized += ScrollBarView_Initialized;
-
-        //Host.CanFocusChanged += Host_CanFocusChanged;
-        Host.EnabledChanged += Host_EnabledChanged;
-        Host.VisibleChanged += Host_VisibleChanged;
-        Host.SuperView.Add (this);
-        AutoHideScrollBars = true;
-
-        if (showBothScrollIndicator)
-        {
-            OtherScrollBarView = new ScrollBarView
-            {
-                IsVertical = !isVertical,
-                ColorScheme = host.ColorScheme,
-                Host = host,
-                CanFocus = false,
-                Enabled = host.Enabled,
-                Visible = host.Visible,
-                OtherScrollBarView = this
-            };
-            OtherScrollBarView._hosted = true;
-            OtherScrollBarView.X = OtherScrollBarView.IsVertical ? Pos.Right (host) - 1 : Pos.Left (host);
-            OtherScrollBarView.Y = OtherScrollBarView.IsVertical ? Pos.Top (host) : Pos.Bottom (host) - 1;
-            OtherScrollBarView.Host.SuperView.Add (OtherScrollBarView);
-            OtherScrollBarView.ShowScrollIndicator = true;
-        }
-
-        ShowScrollIndicator = true;
-        CreateBottomRightCorner (Host);
-    }
-
-    /// <summary>If true the vertical/horizontal scroll bars won't be showed if it's not needed.</summary>
-    public bool AutoHideScrollBars
-    {
-        get => _autoHideScrollBars;
-        set
-        {
-            if (_autoHideScrollBars != value)
-            {
-                _autoHideScrollBars = value;
-                SetNeedsDraw ();
-            }
-        }
-    }
-
-    // BUGBUG: v2 - for consistency this should be named "Parent" not "Host"
-    /// <summary>Get or sets the view that host this <see cref="ScrollBarView"/></summary>
-    public View Host { get; internal set; }
-
-    /// <summary>If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal.</summary>
-    public bool IsVertical
-    {
-        get => _vertical;
-        set
-        {
-            _vertical = value;
-
-            if (IsInitialized)
-            {
-                SetWidthHeight ();
-            }
-        }
-    }
-
-    /// <summary>Get or sets if the view-port is kept always visible in the area of this <see cref="ScrollBarView"/></summary>
-    public bool KeepContentAlwaysInViewport
-    {
-        get => _keepContentAlwaysInViewport;
-        set
-        {
-            if (_keepContentAlwaysInViewport != value)
-            {
-                _keepContentAlwaysInViewport = value;
-                var pos = 0;
-
-                if (value && !_vertical && _position + Host.Viewport.Width > _size)
-                {
-                    pos = _size - Host.Viewport.Width + (_showBothScrollIndicator ? 1 : 0);
-                }
-
-                if (value && _vertical && _position + Host.Viewport.Height > _size)
-                {
-                    pos = _size - Host.Viewport.Height + (_showBothScrollIndicator ? 1 : 0);
-                }
-
-                if (pos != 0)
-                {
-                    Position = pos;
-                }
-
-                if (OtherScrollBarView is { } && OtherScrollBarView._keepContentAlwaysInViewport != value)
-                {
-                    OtherScrollBarView.KeepContentAlwaysInViewport = value;
-                }
-
-                if (pos == 0)
-                {
-                    Refresh ();
-                }
-            }
-        }
-    }
-
-    /// <summary>Represent a vertical or horizontal ScrollBarView other than this.</summary>
-    public ScrollBarView OtherScrollBarView
-    {
-        get => _otherScrollBarView;
-        set
-        {
-            if (value is { } && ((value.IsVertical && _vertical) || (!value.IsVertical && !_vertical)))
-            {
-                throw new ArgumentException (
-                                             $"There is already a {(_vertical ? "vertical" : "horizontal")} ScrollBarView."
-                                            );
-            }
-
-            _otherScrollBarView = value;
-        }
-    }
-
-    /// <summary>The position, relative to <see cref="Size"/>, to set the scrollbar at.</summary>
-    /// <value>The position.</value>
-    public int Position
-    {
-        get => _position;
-        set
-        {
-            if (_position == value)
-            {
-                return;
-            }
-
-            SetPosition (value);
-        }
-    }
-
-    // BUGBUG: v2 - Why can't we get rid of this and just use Visible?
-    /// <summary>Gets or sets the visibility for the vertical or horizontal scroll indicator.</summary>
-    /// <value><c>true</c> if show vertical or horizontal scroll indicator; otherwise, <c>false</c>.</value>
-    public bool ShowScrollIndicator
-    {
-        get => _showScrollIndicator && Visible;
-        set
-        {
-            //if (value == showScrollIndicator) {
-            //	return;
-            //}
-
-            _showScrollIndicator = value;
-
-            if (IsInitialized)
-            {
-                SetNeedsLayout ();
-
-                if (value)
-                {
-                    Visible = true;
-                }
-                else
-                {
-                    Visible = false;
-                    Position = 0;
-                }
-
-                SetWidthHeight ();
-            }
-        }
-    }
-
-    /// <summary>The size of content the scrollbar represents.</summary>
-    /// <value>The size.</value>
-    /// <remarks>
-    ///     The <see cref="Size"/> is typically the size of the virtual content. E.g. when a Scrollbar is part of a
-    ///     <see cref="View"/> the Size is set to the appropriate dimension of <see cref="Host"/>.
-    /// </remarks>
-    public int Size
-    {
-        get => _size;
-        set
-        {
-            _size = value;
-
-            if (IsInitialized)
-            {
-                SetRelativeLayout (SuperView?.Frame.Size ?? Host.Frame.Size);
-                ShowHideScrollBars (false);
-                SetNeedsLayout ();
-            }
-        }
-    }
-
-    private bool _showBothScrollIndicator => OtherScrollBarView?.ShowScrollIndicator == true && ShowScrollIndicator;
-
-    /// <summary>This event is raised when the position on the scrollbar has changed.</summary>
-    public event EventHandler ChangedPosition;
-
-    /// <inheritdoc/>
-    protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
-    {
-        if (mouseEvent.Flags != MouseFlags.Button1Pressed
-            && mouseEvent.Flags != MouseFlags.Button1DoubleClicked
-            && !mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)
-            && mouseEvent.Flags != MouseFlags.Button1Released
-            && mouseEvent.Flags != MouseFlags.WheeledDown
-            && mouseEvent.Flags != MouseFlags.WheeledUp
-            && mouseEvent.Flags != MouseFlags.WheeledRight
-            && mouseEvent.Flags != MouseFlags.WheeledLeft
-            && mouseEvent.Flags != MouseFlags.Button1TripleClicked)
-        {
-            return false;
-        }
-
-        if (!Host.CanFocus)
-        {
-            return true;
-        }
-
-        if (Host?.HasFocus == false)
-        {
-            Host.SetFocus ();
-        }
-
-        int location = _vertical ? mouseEvent.Position.Y : mouseEvent.Position.X;
-        int barsize = _vertical ? Viewport.Height : Viewport.Width;
-        int posTopLeftTee = _vertical ? _posTopTee + 1 : _posLeftTee + 1;
-        int posBottomRightTee = _vertical ? _posBottomTee + 1 : _posRightTee + 1;
-        barsize -= 2;
-        int pos = Position;
-
-        if (mouseEvent.Flags != MouseFlags.Button1Released && (Application.MouseGrabView is null || Application.MouseGrabView != this))
-        {
-            Application.GrabMouse (this);
-        }
-        else if (mouseEvent.Flags == MouseFlags.Button1Released && Application.MouseGrabView is { } && Application.MouseGrabView == this)
-        {
-            _lastLocation = -1;
-            Application.UngrabMouse ();
-
-            return true;
-        }
-
-        if (ShowScrollIndicator
-            && (mouseEvent.Flags == MouseFlags.WheeledDown
-                || mouseEvent.Flags == MouseFlags.WheeledUp
-                || mouseEvent.Flags == MouseFlags.WheeledRight
-                || mouseEvent.Flags == MouseFlags.WheeledLeft))
-        {
-            return Host.NewMouseEvent (mouseEvent) == true;
-        }
-
-        if (mouseEvent.Flags == MouseFlags.Button1Pressed && location == 0)
-        {
-            if (pos > 0)
-            {
-                Position = pos - 1;
-            }
-        }
-        else if (mouseEvent.Flags == MouseFlags.Button1Pressed && location == barsize + 1)
-        {
-            if (CanScroll (1, out _, _vertical))
-            {
-                Position = pos + 1;
-            }
-        }
-        else if (location > 0 && location < barsize + 1)
-        {
-            //var b1 = pos * (Size > 0 ? barsize / Size : 0);
-            //var b2 = Size > 0
-            //	? (KeepContentAlwaysInViewport ? Math.Min (((pos + barsize) * barsize / Size) + 1, barsize - 1) : (pos + barsize) * barsize / Size)
-            //	: 0;
-            //if (KeepContentAlwaysInViewport && b1 == b2) {
-            //	b1 = Math.Max (b1 - 1, 0);
-            //}
-
-            if (_lastLocation > -1
-                || (location >= posTopLeftTee
-                    && location <= posBottomRightTee
-                    && mouseEvent.Flags.HasFlag (
-                                                 MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition
-                                                )))
-            {
-                if (_lastLocation == -1)
-                {
-                    _lastLocation = location;
-
-                    _posBarOffset = _keepContentAlwaysInViewport
-                                        ? Math.Max (location - posTopLeftTee, 1)
-                                        : 0;
-
-                    return true;
-                }
-
-                if (location > _lastLocation)
-                {
-                    if (location - _posBarOffset < barsize)
-                    {
-                        int np = (location - _posBarOffset) * Size / barsize + Size / barsize;
-
-                        if (CanScroll (np - pos, out int nv, _vertical))
-                        {
-                            Position = pos + nv;
-                        }
-                    }
-                    else if (CanScroll (Size - pos, out int nv, _vertical))
-                    {
-                        Position = Math.Min (pos + nv, Size);
-                    }
-                }
-                else if (location < _lastLocation)
-                {
-                    if (location - _posBarOffset > 0)
-                    {
-                        int np = (location - _posBarOffset) * Size / barsize - Size / barsize;
-
-                        if (CanScroll (np - pos, out int nv, _vertical))
-                        {
-                            Position = pos + nv;
-                        }
-                    }
-                    else
-                    {
-                        Position = 0;
-                    }
-                }
-                else if (location - _posBarOffset >= barsize && posBottomRightTee - posTopLeftTee >= 3 && CanScroll (Size - pos, out int nv, _vertical))
-                {
-                    Position = Math.Min (pos + nv, Size);
-                }
-                else if (location - _posBarOffset >= barsize - 1 && posBottomRightTee - posTopLeftTee <= 3 && CanScroll (Size - pos, out nv, _vertical))
-                {
-                    Position = Math.Min (pos + nv, Size);
-                }
-                else if (location - _posBarOffset <= 0 && posBottomRightTee - posTopLeftTee <= 3)
-                {
-                    Position = 0;
-                }
-            }
-            else if (location > posBottomRightTee)
-            {
-                if (CanScroll (barsize, out int nv, _vertical))
-                {
-                    Position = pos + nv;
-                }
-            }
-            else if (location < posTopLeftTee)
-            {
-                if (CanScroll (-barsize, out int nv, _vertical))
-                {
-                    Position = pos + nv;
-                }
-            }
-            else if (location == 1 && posTopLeftTee <= 3)
-            {
-                Position = 0;
-            }
-            else if (location == barsize)
-            {
-                if (CanScroll (Size - pos, out int nv, _vertical))
-                {
-                    Position = Math.Min (pos + nv, Size);
-                }
-            }
-        }
-
-        return true;
-    }
-
-    /// <summary>Virtual method to invoke the <see cref="ChangedPosition"/> action event.</summary>
-    public virtual void OnChangedPosition () { ChangedPosition?.Invoke (this, EventArgs.Empty); }
-
-    /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
-    {
-        if (ColorScheme is null || ((!ShowScrollIndicator || Size == 0) && AutoHideScrollBars && Visible))
-        {
-            if ((!ShowScrollIndicator || Size == 0) && AutoHideScrollBars && Visible)
-            {
-                ShowHideScrollBars (false);
-            }
-
-            return false;
-        }
-
-        if (Size == 0 || (_vertical && Viewport.Height == 0) || (!_vertical && Viewport.Width == 0))
-        {
-            return false;
-        }
-
-        SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ());
-
-        if (_vertical)
-        {
-            if (Viewport.Right < Viewport.Width - 1)
-            {
-                return true;
-            }
-
-            int col = Viewport.Width - 1;
-            int bh = Viewport.Height;
-            Rune special;
-
-            if (bh < 4)
-            {
-                int by1 = _position * bh / Size;
-                int by2 = (_position + bh) * bh / Size;
-
-                Move (col, 0);
-
-                if (Viewport.Height == 1)
-                {
-                    Driver.AddRune (Glyphs.Diamond);
-                }
-                else
-                {
-                    Driver.AddRune (Glyphs.UpArrow);
-                }
-
-                if (Viewport.Height == 3)
-                {
-                    Move (col, 1);
-                    Driver.AddRune (Glyphs.Diamond);
-                }
-
-                if (Viewport.Height > 1)
-                {
-                    Move (col, Viewport.Height - 1);
-                    Driver.AddRune (Glyphs.DownArrow);
-                }
-            }
-            else
-            {
-                bh -= 2;
-
-                int by1 = KeepContentAlwaysInViewport
-                              ? _position * bh / Size
-                              : _position * bh / (Size + bh);
-
-                int by2 = KeepContentAlwaysInViewport
-                              ? Math.Min ((_position + bh) * bh / Size + 1, bh - 1)
-                              : (_position + bh) * bh / (Size + bh);
-
-                if (KeepContentAlwaysInViewport && by1 == by2)
-                {
-                    by1 = Math.Max (by1 - 1, 0);
-                }
-
-                AddRune (col, 0, Glyphs.UpArrow);
-
-                var hasTopTee = false;
-                var hasDiamond = false;
-                var hasBottomTee = false;
-
-                for (var y = 0; y < bh; y++)
-                {
-
-                    if ((y < by1 || y > by2) && ((_position > 0 && !hasTopTee) || (hasTopTee && hasBottomTee)))
-                    {
-                        special = Glyphs.Stipple;
-                    }
-                    else
-                    {
-                        if (y != by2 && y > 1 && by2 - by1 == 0 && by1 < bh - 1 && hasTopTee && !hasDiamond)
-                        {
-                            hasDiamond = true;
-                            special = Glyphs.Diamond;
-                        }
-                        else
-                        {
-                            if (y == by1 && !hasTopTee)
-                            {
-                                hasTopTee = true;
-                                _posTopTee = y;
-                                special = Glyphs.TopTee;
-                            }
-                            else if (((_position == 0 && y == bh - 1) || y >= by2 || by2 == 0) && !hasBottomTee)
-                            {
-                                hasBottomTee = true;
-                                _posBottomTee = y;
-                                special = Glyphs.BottomTee;
-                            }
-                            else
-                            {
-                                special = Glyphs.VLine;
-                            }
-                        }
-                    }
-
-                    AddRune (col, y + 1, special);
-                }
-
-                if (!hasTopTee)
-                {
-                    AddRune (col, Viewport.Height - 2, Glyphs.TopTee);
-                }
-
-                AddRune (col, Viewport.Height - 1, Glyphs.DownArrow);
-            }
-        }
-        else
-        {
-            if (Viewport.Bottom < Viewport.Height - 1)
-            {
-                return true;
-            }
-
-            int row = Viewport.Height - 1;
-            int bw = Viewport.Width;
-            Rune special;
-
-            if (bw < 4)
-            {
-                int bx1 = _position * bw / Size;
-                int bx2 = (_position + bw) * bw / Size;
-
-                Move (0, row);
-                Driver.AddRune (Glyphs.LeftArrow);
-                Driver.AddRune (Glyphs.RightArrow);
-            }
-            else
-            {
-                bw -= 2;
-
-                int bx1 = KeepContentAlwaysInViewport
-                              ? _position * bw / Size
-                              : _position * bw / (Size + bw);
-
-                int bx2 = KeepContentAlwaysInViewport
-                              ? Math.Min ((_position + bw) * bw / Size + 1, bw - 1)
-                              : (_position + bw) * bw / (Size + bw);
-
-                if (KeepContentAlwaysInViewport && bx1 == bx2)
-                {
-                    bx1 = Math.Max (bx1 - 1, 0);
-                }
-
-                Move (0, row);
-                Driver.AddRune (Glyphs.LeftArrow);
-
-                var hasLeftTee = false;
-                var hasDiamond = false;
-                var hasRightTee = false;
-
-                for (var x = 0; x < bw; x++)
-                {
-                    if ((x < bx1 || x >= bx2 + 1) && ((_position > 0 && !hasLeftTee) || (hasLeftTee && hasRightTee)))
-                    {
-                        special = Glyphs.Stipple;
-                    }
-                    else
-                    {
-                        if (x != bx2 && x > 1 && bx2 - bx1 == 0 && bx1 < bw - 1 && hasLeftTee && !hasDiamond)
-                        {
-                            hasDiamond = true;
-                            special = Glyphs.Diamond;
-                        }
-                        else
-                        {
-                            if (x == bx1 && !hasLeftTee)
-                            {
-                                hasLeftTee = true;
-                                _posLeftTee = x;
-                                special = Glyphs.LeftTee;
-                            }
-                            else if (((_position == 0 && x == bw - 1) || x >= bx2 || bx2 == 0) && !hasRightTee)
-                            {
-                                hasRightTee = true;
-                                _posRightTee = x;
-                                special = Glyphs.RightTee;
-                            }
-                            else
-                            {
-                                special = Glyphs.HLine;
-                            }
-                        }
-                    }
-
-                    Driver.AddRune (special);
-                }
-
-                if (!hasLeftTee)
-                {
-                    Move (Viewport.Width - 2, row);
-                    Driver.AddRune (Glyphs.LeftTee);
-                }
-
-                Driver.AddRune (Glyphs.RightArrow);
-            }
-        }
-
-        return false;
-    }
-
-
-    /// <summary>Only used for a hosted view that will update and redraw the scrollbars.</summary>
-    public virtual void Refresh () { ShowHideScrollBars (); }
-
-    internal bool CanScroll (int n, out int max, bool isVertical = false)
-    {
-        if (Host?.Viewport.IsEmpty != false)
-        {
-            max = 0;
-
-            return false;
-        }
-
-        int s = GetBarsize (isVertical);
-        int newSize = Math.Max (Math.Min (_size - s, _position + n), 0);
-        max = _size > s + newSize ? newSize == 0 ? -_position : n : _size - (s + _position) - 1;
-
-        if (_size >= s + newSize && max != 0)
-        {
-            return true;
-        }
-
-        return false;
-    }
-
-    private bool CheckBothScrollBars (ScrollBarView scrollBarView, bool pending = false)
-    {
-        int barsize = scrollBarView._vertical ? scrollBarView.Viewport.Height : scrollBarView.Viewport.Width;
-
-        if (barsize == 0 || barsize >= scrollBarView._size)
-        {
-            if (scrollBarView.ShowScrollIndicator)
-            {
-                scrollBarView.ShowScrollIndicator = false;
-            }
-
-            if (scrollBarView.Visible)
-            {
-                scrollBarView.Visible = false;
-            }
-        }
-        else if (barsize > 0 && barsize == scrollBarView._size && scrollBarView.OtherScrollBarView is { } && pending)
-        {
-            if (scrollBarView.ShowScrollIndicator)
-            {
-                scrollBarView.ShowScrollIndicator = false;
-            }
-
-            if (scrollBarView.Visible)
-            {
-                scrollBarView.Visible = false;
-            }
-
-            if (scrollBarView.OtherScrollBarView is { } && scrollBarView._showBothScrollIndicator)
-            {
-                scrollBarView.OtherScrollBarView.ShowScrollIndicator = false;
-            }
-
-            if (scrollBarView.OtherScrollBarView.Visible)
-            {
-                scrollBarView.OtherScrollBarView.Visible = false;
-            }
-        }
-        else if (barsize > 0 && barsize == _size && scrollBarView.OtherScrollBarView is { } && !pending)
-        {
-            pending = true;
-        }
-        else
-        {
-            if (scrollBarView.OtherScrollBarView is { } && pending)
-            {
-                if (!scrollBarView._showBothScrollIndicator)
-                {
-                    scrollBarView.OtherScrollBarView.ShowScrollIndicator = true;
-                }
-
-                if (!scrollBarView.OtherScrollBarView.Visible)
-                {
-                    scrollBarView.OtherScrollBarView.Visible = true;
-                }
-            }
-
-            if (!scrollBarView.ShowScrollIndicator)
-            {
-                scrollBarView.ShowScrollIndicator = true;
-            }
-
-            if (!scrollBarView.Visible)
-            {
-                scrollBarView.Visible = true;
-            }
-        }
-
-        return pending;
-    }
-
-    private void ContentBottomRightCorner_DrawContent (object sender, DrawEventArgs e)
-    {
-        SetAttribute (Host.HasFocus ? ColorScheme.Focus : GetNormalColor ());
-
-        // I'm forced to do this here because the Clear method is
-        // changing the color attribute and is different of this one
-        Driver.FillRect (Driver.Clip.GetBounds());
-        e.Cancel = true;
-    }
-
-    //private void Host_CanFocusChanged ()
-    //{
-    //	CanFocus = Host.CanFocus;
-    //	if (otherScrollBarView is { }) {
-    //		otherScrollBarView.CanFocus = CanFocus;
-    //	}
-    //}
-
-    private void ContentBottomRightCorner_MouseClick (object sender, MouseEventArgs me)
-    {
-        if (me.Flags == MouseFlags.WheeledDown
-            || me.Flags == MouseFlags.WheeledUp
-            || me.Flags == MouseFlags.WheeledRight
-            || me.Flags == MouseFlags.WheeledLeft)
-        {
-            NewMouseEvent (me);
-        }
-        else if (me.Flags == MouseFlags.Button1Clicked)
-        {
-            Host.SetFocus ();
-        }
-
-        me.Handled = true;
-    }
-
-    private void CreateBottomRightCorner (View host)
-    {
-        if (Host is null)
-        {
-            Host = host;
-        }
-
-        if (Host != null
-            && ((_contentBottomRightCorner is null && OtherScrollBarView is null)
-                || (_contentBottomRightCorner is null && OtherScrollBarView is { } && OtherScrollBarView._contentBottomRightCorner is null)))
-        {
-            _contentBottomRightCorner = new ContentBottomRightCorner { Visible = Host.Visible };
-
-            if (_hosted)
-            {
-                Host.SuperView.Add (_contentBottomRightCorner);
-                _contentBottomRightCorner.X = Pos.Right (Host) - 1;
-                _contentBottomRightCorner.Y = Pos.Bottom (Host) - 1;
-            }
-            else
-            {
-                Host.Add (_contentBottomRightCorner);
-                _contentBottomRightCorner.X = Pos.AnchorEnd (1);
-                _contentBottomRightCorner.Y = Pos.AnchorEnd (1);
-            }
-
-            _contentBottomRightCorner.Width = 1;
-            _contentBottomRightCorner.Height = 1;
-            _contentBottomRightCorner.MouseClick += ContentBottomRightCorner_MouseClick;
-            _contentBottomRightCorner.DrawingContent += ContentBottomRightCorner_DrawContent;
-        }
-    }
-
-    private int GetBarsize (bool isVertical)
-    {
-        if (Host?.Viewport.IsEmpty != false)
-        {
-            return 0;
-        }
-
-        return isVertical ? KeepContentAlwaysInViewport
-                                ? Host.Viewport.Height + (_showBothScrollIndicator ? -2 : -1)
-                                : 0 :
-               KeepContentAlwaysInViewport ? Host.Viewport.Width + (_showBothScrollIndicator ? -2 : -1) : 0;
-    }
-
-    private void Host_EnabledChanged (object sender, EventArgs e)
-    {
-        Enabled = Host.Enabled;
-
-        if (_otherScrollBarView is { })
-        {
-            _otherScrollBarView.Enabled = Enabled;
-        }
-
-        _contentBottomRightCorner.Enabled = Enabled;
-    }
-
-    private void Host_VisibleChanged (object sender, EventArgs e)
-    {
-        if (!Host.Visible)
-        {
-            Visible = Host.Visible;
-
-            if (_otherScrollBarView is { })
-            {
-                _otherScrollBarView.Visible = Visible;
-            }
-
-            _contentBottomRightCorner.Visible = Visible;
-        }
-        else
-        {
-            ShowHideScrollBars ();
-        }
-    }
-
-    private void ScrollBarView_Initialized (object sender, EventArgs e)
-    {
-        SetWidthHeight ();
-        SetRelativeLayout (SuperView?.Frame.Size ?? Host?.Frame.Size ?? Frame.Size);
-
-        if (OtherScrollBarView is null)
-        {
-            // Only do this once if both scrollbars are enabled
-            ShowHideScrollBars ();
-        }
-
-        SetPosition (Position);
-    }
-
-    // Helper to assist Initialized event handler
-    private void SetPosition (int newPosition)
-    {
-        if (!IsInitialized)
-        {
-            // We're not initialized so we can't do anything fancy. Just cache value.
-            _position = newPosition;
-
-            return;
-        }
-
-        if (newPosition < 0)
-        {
-            _position = 0;
-            SetNeedsDraw ();
-
-            return;
-        }
-        else if (CanScroll (newPosition - _position, out int max, _vertical))
-        {
-            if (max == newPosition - _position)
-            {
-                _position = newPosition;
-            }
-            else
-            {
-                _position = Math.Max (_position + max, 0);
-            }
-        }
-        else if (max < 0)
-        {
-            _position = Math.Max (_position + max, 0);
-        }
-        else
-        {
-            _position = Math.Max (newPosition, 0);
-        }
-
-        OnChangedPosition ();
-        SetNeedsDraw ();
-    }
-
-    // BUGBUG: v2 - rationalize this with View.SetMinWidthHeight
-    private void SetWidthHeight ()
-    {
-        // BUGBUG: v2 - If Host is also the ScrollBarView's superview, this is all bogus because it's not
-        // supported that a view can reference it's superview's Dims. This code also assumes the host does 
-        //  not have a margin/borderframe/padding.
-        if (!IsInitialized || _otherScrollBarView is { IsInitialized: false })
-        {
-            return;
-        }
-
-        if (_showBothScrollIndicator)
-        {
-            Width = _vertical ? 1 :
-                    Host != SuperView ? Dim.Width (Host) - 1 : Dim.Fill () - 1;
-            Height = _vertical ? Host != SuperView ? Dim.Height (Host) - 1 : Dim.Fill () - 1 : 1;
-
-            _otherScrollBarView.Width = _otherScrollBarView._vertical ? 1 :
-                                        Host != SuperView ? Dim.Width (Host) - 1 : Dim.Fill () - 1;
-
-            _otherScrollBarView.Height = _otherScrollBarView._vertical
-                                             ? Host != SuperView ? Dim.Height (Host) - 1 : Dim.Fill () - 1
-                                             : 1;
-        }
-        else if (ShowScrollIndicator)
-        {
-            Width = _vertical ? 1 :
-                    Host != SuperView ? Dim.Width (Host) : Dim.Fill ();
-            Height = _vertical ? Host != SuperView ? Dim.Height (Host) : Dim.Fill () : 1;
-        }
-        else if (_otherScrollBarView?.ShowScrollIndicator == true)
-        {
-            _otherScrollBarView.Width = _otherScrollBarView._vertical ? 1 :
-                                        Host != SuperView ? Dim.Width (Host) : Dim.Fill () - 0;
-
-            _otherScrollBarView.Height = _otherScrollBarView._vertical
-                                             ? Host != SuperView ? Dim.Height (Host) : Dim.Fill () - 0
-                                             : 1;
-        }
-    }
-
-    private void ShowHideScrollBars (bool redraw = true)
-    {
-        if (!_hosted || (_hosted && !_autoHideScrollBars))
-        {
-            if (_contentBottomRightCorner is { } && _contentBottomRightCorner.Visible)
-            {
-                _contentBottomRightCorner.Visible = false;
-            }
-            else if (_otherScrollBarView != null
-                     && _otherScrollBarView._contentBottomRightCorner != null
-                     && _otherScrollBarView._contentBottomRightCorner.Visible)
-            {
-                _otherScrollBarView._contentBottomRightCorner.Visible = false;
-            }
-
-            return;
-        }
-
-        bool pending = CheckBothScrollBars (this);
-
-        if (_otherScrollBarView is { })
-        {
-            CheckBothScrollBars (_otherScrollBarView, pending);
-        }
-
-        SetWidthHeight ();
-        SetRelativeLayout (SuperView?.Frame.Size ?? Host.Frame.Size);
-
-        if (_otherScrollBarView is { })
-        {
-            OtherScrollBarView.SetRelativeLayout (SuperView?.Frame.Size ?? Host.Frame.Size);
-        }
-
-        if (_showBothScrollIndicator)
-        {
-            if (_contentBottomRightCorner is { })
-            {
-                _contentBottomRightCorner.Visible = true;
-            }
-            else if (_otherScrollBarView is { } && _otherScrollBarView._contentBottomRightCorner is { })
-            {
-                _otherScrollBarView._contentBottomRightCorner.Visible = true;
-            }
-        }
-        else if (!ShowScrollIndicator)
-        {
-            if (_contentBottomRightCorner is { })
-            {
-                _contentBottomRightCorner.Visible = false;
-            }
-            else if (_otherScrollBarView is { } && _otherScrollBarView._contentBottomRightCorner is { })
-            {
-                _otherScrollBarView._contentBottomRightCorner.Visible = false;
-            }
-
-            if (Application.MouseGrabView is { } && Application.MouseGrabView == this)
-            {
-                Application.UngrabMouse ();
-            }
-        }
-        else if (_contentBottomRightCorner is { })
-        {
-            _contentBottomRightCorner.Visible = false;
-        }
-        else if (_otherScrollBarView is { } && _otherScrollBarView._contentBottomRightCorner is { })
-        {
-            _otherScrollBarView._contentBottomRightCorner.Visible = false;
-        }
-
-        if (Host?.Visible == true && ShowScrollIndicator && !Visible)
-        {
-            Visible = true;
-        }
-
-        if (Host?.Visible == true && _otherScrollBarView?.ShowScrollIndicator == true && !_otherScrollBarView.Visible)
-        {
-            _otherScrollBarView.Visible = true;
-        }
-
-        if (!redraw)
-        {
-            return;
-        }
-
-        if (ShowScrollIndicator)
-        {
-            Draw ();
-        }
-
-        if (_otherScrollBarView is { } && _otherScrollBarView.ShowScrollIndicator)
-        {
-            _otherScrollBarView.Draw ();
-        }
-
-        if (_contentBottomRightCorner is { } && _contentBottomRightCorner.Visible)
-        {
-            _contentBottomRightCorner.Draw ();
-        }
-        else if (_otherScrollBarView is { } && _otherScrollBarView._contentBottomRightCorner is { } && _otherScrollBarView._contentBottomRightCorner.Visible)
-        {
-            _otherScrollBarView._contentBottomRightCorner.Draw ();
-        }
-    }
-
-    internal class ContentBottomRightCorner : View
-    {
-        public ContentBottomRightCorner ()
-        {
-            ColorScheme = ColorScheme;
-        }
-    }
-}

+ 0 - 774
Terminal.Gui/Views/ScrollView.cs

@@ -1,774 +0,0 @@
-//
-// ScrollView.cs: ScrollView view.
-//
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-//
-// TODO:
-// - focus in scrollview
-// - focus handling in scrollview to auto scroll to focused view
-// - Raise events
-// - Perhaps allow an option to not display the scrollbar arrow indicators?
-
-using System.ComponentModel;
-
-namespace Terminal.Gui;
-
-/// <summary>
-///     Scrollviews are views that present a window into a virtual space where subviews are added.  Similar to the iOS
-///     UIScrollView.
-/// </summary>
-/// <remarks>
-///     <para>
-///         The subviews that are added to this <see cref="Gui.ScrollView"/> are offset by the
-///         <see cref="ContentOffset"/> property.  The view itself is a window into the space represented by the
-///         <see cref="View.GetContentSize ()"/>.
-///     </para>
-///     <para>Use the</para>
-/// </remarks>
-public class ScrollView : View
-{
-    private readonly ContentView _contentView;
-    private readonly ScrollBarView _horizontal;
-    private readonly ScrollBarView _vertical;
-    private bool _autoHideScrollBars = true;
-    private View _contentBottomRightCorner;
-    private Point _contentOffset;
-    private bool _keepContentAlwaysInViewport = true;
-    private bool _showHorizontalScrollIndicator;
-    private bool _showVerticalScrollIndicator;
-
-    /// <summary>
-    ///     Initializes a new instance of the <see cref="Gui.ScrollView"/> class.
-    /// </summary>
-    public ScrollView ()
-    {
-        _contentView = new ContentView ();
-
-        _vertical = new ScrollBarView
-        {
-            X = Pos.AnchorEnd (1),
-            Y = 0,
-            Width = 1,
-            Height = Dim.Fill (_showHorizontalScrollIndicator ? 1 : 0),
-            Size = 1,
-            IsVertical = true,
-            Host = this
-        };
-
-        _horizontal = new ScrollBarView
-        {
-            X = 0,
-            Y = Pos.AnchorEnd (1),
-            Width = Dim.Fill (_showVerticalScrollIndicator ? 1 : 0),
-            Height = 1,
-            Size = 1,
-            IsVertical = false,
-            Host = this
-        };
-
-        _vertical.OtherScrollBarView = _horizontal;
-        _horizontal.OtherScrollBarView = _vertical;
-        base.Add (_contentView);
-        CanFocus = true;
-        TabStop = TabBehavior.TabGroup;
-
-        MouseEnter += View_MouseEnter;
-        MouseLeave += View_MouseLeave;
-        _contentView.MouseEnter += View_MouseEnter;
-        _contentView.MouseLeave += View_MouseLeave;
-
-        Application.UnGrabbedMouse += Application_UnGrabbedMouse;
-
-        // Things this view knows how to do
-        AddCommand (Command.ScrollUp, () => ScrollUp (1));
-        AddCommand (Command.ScrollDown, () => ScrollDown (1));
-        AddCommand (Command.ScrollLeft, () => ScrollLeft (1));
-        AddCommand (Command.ScrollRight, () => ScrollRight (1));
-        AddCommand (Command.PageUp, () => ScrollUp (Viewport.Height));
-        AddCommand (Command.PageDown, () => ScrollDown (Viewport.Height));
-        AddCommand (Command.PageLeft, () => ScrollLeft (Viewport.Width));
-        AddCommand (Command.PageRight, () => ScrollRight (Viewport.Width));
-        AddCommand (Command.Start, () => ScrollUp (GetContentSize ().Height));
-        AddCommand (Command.End, () => ScrollDown (GetContentSize ().Height));
-        AddCommand (Command.LeftStart, () => ScrollLeft (GetContentSize ().Width));
-        AddCommand (Command.RightEnd, () => ScrollRight (GetContentSize ().Width));
-
-        // Default keybindings for this view
-        KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
-        KeyBindings.Add (Key.CursorDown, Command.ScrollDown);
-        KeyBindings.Add (Key.CursorLeft, Command.ScrollLeft);
-        KeyBindings.Add (Key.CursorRight, Command.ScrollRight);
-
-        KeyBindings.Add (Key.PageUp, Command.PageUp);
-        KeyBindings.Add (Key.V.WithAlt, Command.PageUp);
-
-        KeyBindings.Add (Key.PageDown, Command.PageDown);
-        KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
-
-        KeyBindings.Add (Key.PageUp.WithCtrl, Command.PageLeft);
-        KeyBindings.Add (Key.PageDown.WithCtrl, Command.PageRight);
-        KeyBindings.Add (Key.Home, Command.Start);
-        KeyBindings.Add (Key.End, Command.End);
-        KeyBindings.Add (Key.Home.WithCtrl, Command.LeftStart);
-        KeyBindings.Add (Key.End.WithCtrl, Command.RightEnd);
-
-        Initialized += (s, e) =>
-                       {
-                           if (!_vertical.IsInitialized)
-                           {
-                               _vertical.BeginInit ();
-                               _vertical.EndInit ();
-                           }
-
-                           if (!_horizontal.IsInitialized)
-                           {
-                               _horizontal.BeginInit ();
-                               _horizontal.EndInit ();
-                           }
-
-                           SetContentOffset (_contentOffset);
-                           _contentView.Frame = new Rectangle (ContentOffset, GetContentSize ());
-
-                           // PERF: How about calls to Point.Offset instead?
-                           _vertical.ChangedPosition += delegate { ContentOffset = new Point (ContentOffset.X, _vertical.Position); };
-                           _horizontal.ChangedPosition += delegate { ContentOffset = new Point (_horizontal.Position, ContentOffset.Y); };
-                       };
-        ContentSizeChanged += ScrollViewContentSizeChanged;
-    }
-
-    private void ScrollViewContentSizeChanged (object sender, SizeChangedEventArgs e)
-    {
-        if (e.Size is null)
-        {
-            return;
-        }
-        _contentView.Frame = new Rectangle (ContentOffset, e.Size.Value with { Width = e.Size.Value.Width - 1, Height = e.Size.Value.Height - 1 });
-        _vertical.Size = e.Size.Value.Height;
-        _horizontal.Size = e.Size.Value.Width;
-    }
-
-    private void Application_UnGrabbedMouse (object sender, ViewEventArgs e)
-    {
-        var parent = e.View is Adornment adornment ? adornment.Parent : e.View;
-
-        if (parent is { })
-        {
-            var supView = parent.SuperView;
-
-            while (supView is { })
-            {
-                if (supView == _contentView)
-                {
-                    Application.GrabMouse (this);
-
-                    break;
-                }
-
-                supView = supView.SuperView;
-            }
-        }
-    }
-
-    /// <summary>If true the vertical/horizontal scroll bars won't be showed if it's not needed.</summary>
-    public bool AutoHideScrollBars
-    {
-        get => _autoHideScrollBars;
-        set
-        {
-            if (_autoHideScrollBars != value)
-            {
-                _autoHideScrollBars = value;
-
-                if (Subviews.Contains (_vertical))
-                {
-                    _vertical.AutoHideScrollBars = value;
-                }
-
-                if (Subviews.Contains (_horizontal))
-                {
-                    _horizontal.AutoHideScrollBars = value;
-                }
-
-                SetNeedsDraw ();
-            }
-        }
-    }
-
-    /// <summary>Represents the top left corner coordinate that is displayed by the scrollview</summary>
-    /// <value>The content offset.</value>
-    public Point ContentOffset
-    {
-        get => _contentOffset;
-        set
-        {
-            if (!IsInitialized)
-            {
-                // We're not initialized so we can't do anything fancy. Just cache value.
-                _contentOffset = new Point (-Math.Abs (value.X), -Math.Abs (value.Y));
-
-                return;
-            }
-
-            SetContentOffset (value);
-        }
-    }
-
-    ///// <summary>Represents the contents of the data shown inside the scrollview</summary>
-    ///// <value>The size of the content.</value>
-    //public new Size ContentSize
-    //{
-    //    get => ContentSize;
-    //    set
-    //    {
-    //        if (GetContentSize () != value)
-    //        {
-    //            ContentSize = value;
-    //            _contentView.Frame = new Rectangle (_contentOffset, value);
-    //            _vertical.Size = GetContentSize ().Height;
-    //            _horizontal.Size = GetContentSize ().Width;
-    //            SetNeedsDraw ();
-    //        }
-    //    }
-    //}
-
-    /// <summary>Get or sets if the view-port is kept always visible in the area of this <see cref="ScrollView"/></summary>
-    public bool KeepContentAlwaysInViewport
-    {
-        get => _keepContentAlwaysInViewport;
-        set
-        {
-            if (_keepContentAlwaysInViewport != value)
-            {
-                _keepContentAlwaysInViewport = value;
-                _vertical.OtherScrollBarView.KeepContentAlwaysInViewport = value;
-                _horizontal.OtherScrollBarView.KeepContentAlwaysInViewport = value;
-                Point p = default;
-
-                if (value && -_contentOffset.X + Viewport.Width > GetContentSize ().Width)
-                {
-                    p = new Point (
-                                   GetContentSize ().Width - Viewport.Width + (_showVerticalScrollIndicator ? 1 : 0),
-                                   -_contentOffset.Y
-                                  );
-                }
-
-                if (value && -_contentOffset.Y + Viewport.Height > GetContentSize ().Height)
-                {
-                    if (p == default (Point))
-                    {
-                        p = new Point (
-                                       -_contentOffset.X,
-                                       GetContentSize ().Height - Viewport.Height + (_showHorizontalScrollIndicator ? 1 : 0)
-                                      );
-                    }
-                    else
-                    {
-                        p.Y = GetContentSize ().Height - Viewport.Height + (_showHorizontalScrollIndicator ? 1 : 0);
-                    }
-                }
-
-                if (p != default (Point))
-                {
-                    ContentOffset = p;
-                }
-            }
-        }
-    }
-
-    /// <summary>Gets or sets the visibility for the horizontal scroll indicator.</summary>
-    /// <value><c>true</c> if show horizontal scroll indicator; otherwise, <c>false</c>.</value>
-    public bool ShowHorizontalScrollIndicator
-    {
-        get => _showHorizontalScrollIndicator;
-        set
-        {
-            if (value != _showHorizontalScrollIndicator)
-            {
-                _showHorizontalScrollIndicator = value;
-                SetNeedsLayout ();
-
-                if (value)
-                {
-                    _horizontal.OtherScrollBarView = _vertical;
-                    base.Add (_horizontal);
-                    _horizontal.ShowScrollIndicator = value;
-                    _horizontal.AutoHideScrollBars = _autoHideScrollBars;
-                    _horizontal.OtherScrollBarView.ShowScrollIndicator = value;
-                    _horizontal.MouseEnter += View_MouseEnter;
-                    _horizontal.MouseLeave += View_MouseLeave;
-                }
-                else
-                {
-                    base.Remove (_horizontal);
-                    _horizontal.OtherScrollBarView = null;
-                    _horizontal.MouseEnter -= View_MouseEnter;
-                    _horizontal.MouseLeave -= View_MouseLeave;
-                }
-            }
-
-            _vertical.Height = Dim.Fill (_showHorizontalScrollIndicator ? 1 : 0);
-        }
-    }
-
-    /// <summary>Gets or sets the visibility for the vertical scroll indicator.</summary>
-    /// <value><c>true</c> if show vertical scroll indicator; otherwise, <c>false</c>.</value>
-    public bool ShowVerticalScrollIndicator
-    {
-        get => _showVerticalScrollIndicator;
-        set
-        {
-            if (value != _showVerticalScrollIndicator)
-            {
-                _showVerticalScrollIndicator = value;
-                SetNeedsLayout ();
-
-                if (value)
-                {
-                    _vertical.OtherScrollBarView = _horizontal;
-                    base.Add (_vertical);
-                    _vertical.ShowScrollIndicator = value;
-                    _vertical.AutoHideScrollBars = _autoHideScrollBars;
-                    _vertical.OtherScrollBarView.ShowScrollIndicator = value;
-                    _vertical.MouseEnter += View_MouseEnter;
-                    _vertical.MouseLeave += View_MouseLeave;
-                }
-                else
-                {
-                    Remove (_vertical);
-                    _vertical.OtherScrollBarView = null;
-                    _vertical.MouseEnter -= View_MouseEnter;
-                    _vertical.MouseLeave -= View_MouseLeave;
-                }
-            }
-
-            _horizontal.Width = Dim.Fill (_showVerticalScrollIndicator ? 1 : 0);
-        }
-    }
-
-    /// <summary>Adds the view to the scrollview.</summary>
-    /// <param name="view">The view to add to the scrollview.</param>
-    public override View Add (View view)
-    {
-        if (view is ScrollBarView.ContentBottomRightCorner)
-        {
-            _contentBottomRightCorner = view;
-            base.Add (view);
-        }
-        else
-        {
-            if (!IsOverridden (view, "OnMouseEvent"))
-            {
-                view.MouseEnter += View_MouseEnter;
-                view.MouseLeave += View_MouseLeave;
-            }
-
-            _contentView.Add (view);
-        }
-
-        SetNeedsLayout ();
-        return view;
-    }
-
-    /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
-    {
-        SetViewsNeedsDraw ();
-
-        // TODO: It's bad practice for views to always clear a view. It negates clipping.
-        ClearViewport ();
-
-        if (!string.IsNullOrEmpty (_contentView.Text) || _contentView.Subviews.Count > 0)
-        {
-            Region? saved = ClipFrame();
-            _contentView.Draw ();
-            View.SetClip (saved);
-        }
-
-        DrawScrollBars ();
-
-        return true;
-    }
-
-    /// <inheritdoc/>
-    protected override bool OnKeyDown (Key a)
-    {
-        if (base.OnKeyDown (a))
-        {
-            return true;
-        }
-
-        bool? result = InvokeCommands (a, KeyBindingScope.HotKey | KeyBindingScope.Focused);
-
-        if (result is { })
-        {
-            return (bool)result;
-        }
-
-        return false;
-    }
-
-    /// <inheritdoc/>
-    protected override bool OnMouseEvent (MouseEventArgs me)
-    {
-        if (!Enabled)
-        {
-            // A disabled view should not eat mouse events
-            return false;
-        }
-
-        if (me.Flags == MouseFlags.WheeledDown && ShowVerticalScrollIndicator)
-        {
-            return ScrollDown (1);
-        }
-        else if (me.Flags == MouseFlags.WheeledUp && ShowVerticalScrollIndicator)
-        {
-            return ScrollUp (1);
-        }
-        else if (me.Flags == MouseFlags.WheeledRight && _showHorizontalScrollIndicator)
-        {
-            return ScrollRight (1);
-        }
-        else if (me.Flags == MouseFlags.WheeledLeft && ShowVerticalScrollIndicator)
-        {
-            return ScrollLeft (1);
-        }
-        else if (me.Position.X == _vertical.Frame.X && ShowVerticalScrollIndicator)
-        {
-            _vertical.NewMouseEvent (me);
-        }
-        else if (me.Position.Y == _horizontal.Frame.Y && ShowHorizontalScrollIndicator)
-        {
-            _horizontal.NewMouseEvent (me);
-        }
-        else if (IsOverridden (me.View, "OnMouseEvent"))
-        {
-            Application.UngrabMouse ();
-        }
-
-        return me.Handled;
-    }
-
-    /// <inheritdoc/>
-    public override Point? PositionCursor ()
-    {
-        if (InternalSubviews.Count == 0)
-        {
-            Move (0, 0);
-
-            return null; // Don't show the cursor
-        }
-        return base.PositionCursor ();
-    }
-
-    /// <summary>Removes the view from the scrollview.</summary>
-    /// <param name="view">The view to remove from the scrollview.</param>
-    public override View Remove (View view)
-    {
-        if (view is null)
-        {
-            return view;
-        }
-
-        SetNeedsDraw ();
-        View container = view?.SuperView;
-
-        if (container == this)
-        {
-            base.Remove (view);
-        }
-        else
-        {
-            container?.Remove (view);
-        }
-
-        if (_contentView.InternalSubviews.Count < 1)
-        {
-            CanFocus = false;
-        }
-
-        return view;
-    }
-
-    /// <summary>Removes all widgets from this container.</summary>
-    public override void RemoveAll () { _contentView.RemoveAll (); }
-
-    /// <summary>Scrolls the view down.</summary>
-    /// <returns><c>true</c>, if left was scrolled, <c>false</c> otherwise.</returns>
-    /// <param name="lines">Number of lines to scroll.</param>
-    public bool ScrollDown (int lines)
-    {
-        if (_vertical.CanScroll (lines, out _, true))
-        {
-            ContentOffset = new Point (_contentOffset.X, _contentOffset.Y - lines);
-
-            return true;
-        }
-
-        return false;
-    }
-
-    /// <summary>Scrolls the view to the left</summary>
-    /// <returns><c>true</c>, if left was scrolled, <c>false</c> otherwise.</returns>
-    /// <param name="cols">Number of columns to scroll by.</param>
-    public bool ScrollLeft (int cols)
-    {
-        if (_contentOffset.X < 0)
-        {
-            ContentOffset = new Point (Math.Min (_contentOffset.X + cols, 0), _contentOffset.Y);
-
-            return true;
-        }
-
-        return false;
-    }
-
-    /// <summary>Scrolls the view to the right.</summary>
-    /// <returns><c>true</c>, if right was scrolled, <c>false</c> otherwise.</returns>
-    /// <param name="cols">Number of columns to scroll by.</param>
-    public bool ScrollRight (int cols)
-    {
-        if (_horizontal.CanScroll (cols, out _))
-        {
-            ContentOffset = new Point (_contentOffset.X - cols, _contentOffset.Y);
-
-            return true;
-        }
-
-        return false;
-    }
-
-    /// <summary>Scrolls the view up.</summary>
-    /// <returns><c>true</c>, if left was scrolled, <c>false</c> otherwise.</returns>
-    /// <param name="lines">Number of lines to scroll.</param>
-    public bool ScrollUp (int lines)
-    {
-        if (_contentOffset.Y < 0)
-        {
-            ContentOffset = new Point (_contentOffset.X, Math.Min (_contentOffset.Y + lines, 0));
-
-            return true;
-        }
-
-        return false;
-    }
-
-    /// <inheritdoc/>
-    protected override void Dispose (bool disposing)
-    {
-        if (!_showVerticalScrollIndicator)
-        {
-            // It was not added to SuperView, so it won't get disposed automatically
-            _vertical?.Dispose ();
-        }
-
-        if (!_showHorizontalScrollIndicator)
-        {
-            // It was not added to SuperView, so it won't get disposed automatically
-            _horizontal?.Dispose ();
-        }
-
-        Application.UnGrabbedMouse -= Application_UnGrabbedMouse;
-
-        base.Dispose (disposing);
-    }
-
-    private void DrawScrollBars ()
-    {
-        if (_autoHideScrollBars)
-        {
-            ShowHideScrollBars ();
-        }
-        else
-        {
-            if (ShowVerticalScrollIndicator)
-            {
-                Region? saved = View.SetClipToScreen ();
-                _vertical.Draw ();
-                View.SetClip (saved);
-            }
-
-            if (ShowHorizontalScrollIndicator)
-            {
-                Region? saved = View.SetClipToScreen ();
-                _horizontal.Draw ();
-                View.SetClip (saved);
-            }
-
-            if (ShowVerticalScrollIndicator && ShowHorizontalScrollIndicator)
-            {
-                SetContentBottomRightCornerVisibility ();
-                Region? saved = View.SetClipToScreen ();
-                _contentBottomRightCorner.Draw ();
-                View.SetClip (saved);
-            }
-        }
-    }
-
-    private void SetContentBottomRightCornerVisibility ()
-    {
-        if (_showHorizontalScrollIndicator && _showVerticalScrollIndicator)
-        {
-            _contentBottomRightCorner.Visible = true;
-        }
-        else if (_horizontal.IsAdded || _vertical.IsAdded)
-        {
-            _contentBottomRightCorner.Visible = false;
-        }
-    }
-
-    private void SetContentOffset (Point offset)
-    {
-        // INTENT: Unclear intent. How about a call to Offset?
-        _contentOffset = new Point (-Math.Abs (offset.X), -Math.Abs (offset.Y));
-        _contentView.Frame = new Rectangle (_contentOffset, GetContentSize ());
-        int p = Math.Max (0, -_contentOffset.Y);
-
-        if (_vertical.Position != p)
-        {
-            _vertical.Position = Math.Max (0, -_contentOffset.Y);
-        }
-
-        p = Math.Max (0, -_contentOffset.X);
-
-        if (_horizontal.Position != p)
-        {
-            _horizontal.Position = Math.Max (0, -_contentOffset.X);
-        }
-        SetNeedsDraw ();
-    }
-
-    private void SetViewsNeedsDraw ()
-    {
-        foreach (View view in _contentView.Subviews)
-        {
-            view.SetNeedsDraw ();
-        }
-    }
-
-    private void ShowHideScrollBars ()
-    {
-        bool v = false, h = false;
-        var p = false;
-
-        if (GetContentSize () is { } && (Viewport.Height == 0 || Viewport.Height > GetContentSize ().Height))
-        {
-            if (ShowVerticalScrollIndicator)
-            {
-                ShowVerticalScrollIndicator = false;
-            }
-
-            v = false;
-        }
-        else if (GetContentSize () is { } && Viewport.Height > 0 && Viewport.Height == GetContentSize ().Height)
-        {
-            p = true;
-        }
-        else
-        {
-            if (!ShowVerticalScrollIndicator)
-            {
-                ShowVerticalScrollIndicator = true;
-            }
-
-            v = true;
-        }
-
-        if (GetContentSize () is { } && (Viewport.Width == 0 || Viewport.Width > GetContentSize ().Width))
-        {
-            if (ShowHorizontalScrollIndicator)
-            {
-                ShowHorizontalScrollIndicator = false;
-            }
-
-            h = false;
-        }
-        else if (GetContentSize () is { } && Viewport.Width > 0 && Viewport.Width == GetContentSize ().Width && p)
-        {
-            if (ShowHorizontalScrollIndicator)
-            {
-                ShowHorizontalScrollIndicator = false;
-            }
-
-            h = false;
-
-            if (ShowVerticalScrollIndicator)
-            {
-                ShowVerticalScrollIndicator = false;
-            }
-
-            v = false;
-        }
-        else
-        {
-            if (p)
-            {
-                if (!ShowVerticalScrollIndicator)
-                {
-                    ShowVerticalScrollIndicator = true;
-                }
-
-                v = true;
-            }
-
-            if (!ShowHorizontalScrollIndicator)
-            {
-                ShowHorizontalScrollIndicator = true;
-            }
-
-            h = true;
-        }
-
-        Dim dim = Dim.Fill (h ? 1 : 0);
-
-        if (!_vertical.Height.Equals (dim))
-        {
-            _vertical.Height = dim;
-        }
-
-        dim = Dim.Fill (v ? 1 : 0);
-
-        if (!_horizontal.Width.Equals (dim))
-        {
-            _horizontal.Width = dim;
-        }
-
-        if (v)
-        {
-            _vertical.SetRelativeLayout (Viewport.Size);
-            _vertical.Draw ();
-        }
-
-        if (h)
-        {
-            _horizontal.SetRelativeLayout (Viewport.Size);
-            _horizontal.Draw ();
-        }
-
-        SetContentBottomRightCornerVisibility ();
-
-        if (v && h)
-        {
-            _contentBottomRightCorner.SetRelativeLayout (Viewport.Size);
-            _contentBottomRightCorner.Draw ();
-        }
-    }
-
-    private void View_MouseEnter (object sender, CancelEventArgs e) { Application.GrabMouse (this); }
-
-    private void View_MouseLeave (object sender, EventArgs e)
-    {
-        if (Application.MouseGrabView is { } && Application.MouseGrabView != this && Application.MouseGrabView != _vertical && Application.MouseGrabView != _horizontal)
-        {
-            Application.UngrabMouse ();
-        }
-    }
-
-    // The ContentView is the view that contains the subviews  and content that are being scrolled
-    // The ContentView is the size of the ContentSize and is offset by the ContentOffset
-    private class ContentView : View
-    {
-        public ContentView () { CanFocus = true; }
-    }
-}

+ 1 - 0
Terminal.Gui/Views/Shortcut.cs

@@ -486,6 +486,7 @@ public class Shortcut : View, IOrientation, IDesignable
                     InvokeCommand (Command.Select, new (Command.Select, null, null, this));
                 }
 
+                // BUGBUG: This prevents NumericUpDown on statusbar in HexEditor from working
                 e.Cancel = true;
             }
         }

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

@@ -47,7 +47,7 @@ public class Slider<T> : View, IOrientation
 
         _options = options ?? new List<SliderOption<T>> ();
 
-        _orientationHelper = new (this);
+        _orientationHelper = new (this); // Do not use object initializer!
         _orientationHelper.Orientation = _config._sliderOrientation = orientation;
         _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
         _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);

+ 0 - 1404
Terminal.Gui/Views/TabView.cs

@@ -1,1404 +0,0 @@
-#nullable enable
-namespace Terminal.Gui;
-
-/// <summary>Control that hosts multiple sub views, presenting a single one at once.</summary>
-public class TabView : View
-{
-    /// <summary>The default <see cref="MaxTabTextWidth"/> to set on new <see cref="TabView"/> controls.</summary>
-    public const uint DefaultMaxTabTextWidth = 30;
-
-    /// <summary>
-    ///     This sub view is the main client area of the current tab.  It hosts the <see cref="Tab.View"/> of the tab, the
-    ///     <see cref="SelectedTab"/>.
-    /// </summary>
-    private readonly View _contentView;
-
-    private readonly List<Tab> _tabs = new ();
-
-    /// <summary>This sub view is the 2 or 3 line control that represents the actual tabs themselves.</summary>
-    private readonly TabRowView _tabsBar;
-
-    private Tab? _selectedTab;
-    private TabToRender []? _tabLocations;
-    private int _tabScrollOffset;
-
-    /// <summary>Initializes a <see cref="TabView"/> class.</summary>
-    public TabView ()
-    {
-        CanFocus = true;
-        TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup
-        _tabsBar = new TabRowView (this);
-        _contentView = new View ()
-        {
-            //Id = "TabView._contentView",
-        };
-        ApplyStyleChanges ();
-
-        base.Add (_tabsBar);
-        base.Add (_contentView);
-
-        // Things this view knows how to do
-        AddCommand (Command.Left, () => SwitchTabBy (-1));
-
-        AddCommand (Command.Right, () => SwitchTabBy (1));
-
-        AddCommand (
-                    Command.LeftStart,
-                    () =>
-                    {
-                        TabScrollOffset = 0;
-                        SelectedTab = Tabs.FirstOrDefault ()!;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.RightEnd,
-                    () =>
-                    {
-                        TabScrollOffset = Tabs.Count - 1;
-                        SelectedTab = Tabs.LastOrDefault ()!;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.PageDown,
-                    () =>
-                    {
-                        TabScrollOffset += _tabLocations!.Length;
-                        SelectedTab = Tabs.ElementAt (TabScrollOffset);
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.PageUp,
-                    () =>
-                    {
-                        TabScrollOffset -= _tabLocations!.Length;
-                        SelectedTab = Tabs.ElementAt (TabScrollOffset);
-
-                        return true;
-                    }
-                   );
-
-        // Default keybindings for this view
-        KeyBindings.Add (Key.CursorLeft, Command.Left);
-        KeyBindings.Add (Key.CursorRight, Command.Right);
-        KeyBindings.Add (Key.Home, Command.LeftStart);
-        KeyBindings.Add (Key.End, Command.RightEnd);
-        KeyBindings.Add (Key.PageDown, Command.PageDown);
-        KeyBindings.Add (Key.PageUp, Command.PageUp);
-    }
-
-    /// <summary>
-    ///     The maximum number of characters to render in a Tab header.  This prevents one long tab from pushing out all
-    ///     the others.
-    /// </summary>
-    public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth;
-
-    /// <summary>The currently selected member of <see cref="Tabs"/> chosen by the user.</summary>
-    /// <value></value>
-    public Tab? SelectedTab
-    {
-        get => _selectedTab;
-        set
-        {
-            UnSetCurrentTabs ();
-
-            Tab? old = _selectedTab;
-
-            if (_selectedTab is { })
-            {
-                if (_selectedTab.View is { })
-                {
-                    _selectedTab.View.CanFocusChanged -= ContentViewCanFocus!;
-                    // remove old content
-                    _contentView.Remove (_selectedTab.View);
-                }
-            }
-
-            _selectedTab = value;
-
-            // add new content
-            if (_selectedTab?.View != null)
-            {
-                _selectedTab.View.CanFocusChanged += ContentViewCanFocus!;
-                _contentView.Add (_selectedTab.View);
-                // _contentView.Id = $"_contentView for {_selectedTab.DisplayText}";
-            }
-
-            ContentViewCanFocus (null!, null!);
-
-            EnsureSelectedTabIsVisible ();
-
-            if (old != _selectedTab)
-            {
-                if (old?.HasFocus == true)
-                {
-                    SelectedTab?.SetFocus ();
-                }
-
-                OnSelectedTabChanged (old!, _selectedTab!);
-            }
-            SetNeedsLayout ();
-        }
-    }
-
-    private void ContentViewCanFocus (object sender, EventArgs eventArgs)
-    {
-        _contentView.CanFocus = _contentView.Subviews.Count (v => v.CanFocus) > 0;
-    }
-
-    private TabStyle _style = new ();
-
-    /// <summary>Render choices for how to display tabs.  After making changes, call <see cref="ApplyStyleChanges()"/>.</summary>
-    /// <value></value>
-    public TabStyle Style
-    {
-        get => _style;
-        set
-        {
-            if (_style == value)
-            {
-                return;
-            }
-            _style = value;
-            SetNeedsLayout ();
-        }
-    }
-
-    /// <summary>All tabs currently hosted by the control.</summary>
-    /// <value></value>
-    public IReadOnlyCollection<Tab> Tabs => _tabs.AsReadOnly ();
-
-    /// <summary>When there are too many tabs to render, this indicates the first tab to render on the screen.</summary>
-    /// <value></value>
-    public int TabScrollOffset
-    {
-        get => _tabScrollOffset;
-        set
-        {
-            _tabScrollOffset = EnsureValidScrollOffsets (value);
-            SetNeedsLayout ();
-        }
-    }
-
-    /// <summary>Adds the given <paramref name="tab"/> to <see cref="Tabs"/>.</summary>
-    /// <param name="tab"></param>
-    /// <param name="andSelect">True to make the newly added Tab the <see cref="SelectedTab"/>.</param>
-    public void AddTab (Tab tab, bool andSelect)
-    {
-        if (_tabs.Contains (tab))
-        {
-            return;
-        }
-
-        _tabs.Add (tab);
-        _tabsBar.Add (tab);
-
-        if (SelectedTab is null || andSelect)
-        {
-            SelectedTab = tab;
-
-            EnsureSelectedTabIsVisible ();
-
-            tab.View?.SetFocus ();
-        }
-
-        SetNeedsLayout ();
-    }
-
-    /// <summary>
-    ///     Updates the control to use the latest state settings in <see cref="Style"/>. This can change the size of the
-    ///     client area of the tab (for rendering the selected tab's content).  This method includes a call to
-    ///     <see cref="View.SetNeedsDraw()"/>.
-    /// </summary>
-    public void ApplyStyleChanges ()
-    {
-        _contentView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None;
-        _contentView.Width = Dim.Fill ();
-
-        if (Style.TabsOnBottom)
-        {
-            // Tabs are along the bottom so just dodge the border
-            if (Style.ShowBorder)
-            {
-                _contentView.Border.Thickness = new Thickness (1, 1, 1, 0);
-            }
-
-            _contentView.Y = 0;
-
-            int tabHeight = GetTabHeight (false);
-
-            // Fill client area leaving space at bottom for tabs
-            _contentView.Height = Dim.Fill (tabHeight);
-
-            _tabsBar.Height = tabHeight;
-
-            _tabsBar.Y = Pos.Bottom (_contentView);
-        }
-        else
-        {
-            // Tabs are along the top
-            if (Style.ShowBorder)
-            {
-                _contentView.Border.Thickness = new Thickness (1, 0, 1, 1);
-            }
-
-            _tabsBar.Y = 0;
-
-            int tabHeight = GetTabHeight (true);
-
-            //move content down to make space for tabs
-            _contentView.Y = Pos.Bottom (_tabsBar);
-
-            // Fill client area leaving space at bottom for border
-            _contentView.Height = Dim.Fill ();
-
-            // The top tab should be 2 or 3 rows high and on the top
-
-            _tabsBar.Height = tabHeight;
-
-            // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0
-        }
-
-        SetNeedsLayout ();
-    }
-
-    /// <summary>Updates <see cref="TabScrollOffset"/> to ensure that <see cref="SelectedTab"/> is visible.</summary>
-    public void EnsureSelectedTabIsVisible ()
-    {
-        if (!IsInitialized || SelectedTab is null)
-        {
-            return;
-        }
-
-        // if current viewport does not include the selected tab
-        if (!CalculateViewport (Viewport).Any (r => Equals (SelectedTab, r.Tab)))
-        {
-            // Set scroll offset so the first tab rendered is the
-            TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab));
-        }
-    }
-
-    /// <summary>Updates <see cref="TabScrollOffset"/> to be a valid index of <see cref="Tabs"/>.</summary>
-    /// <param name="value">The value to validate.</param>
-    /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDraw()"/>.</remarks>
-    /// <returns>The valid <see cref="TabScrollOffset"/> for the given value.</returns>
-    public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); }
-
-    /// <inheritdoc />
-    protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
-    {
-        if (SelectedTab is { } && !_contentView.CanFocus && focusedView == this)
-        {
-            SelectedTab?.SetFocus ();
-
-            return;
-        }
-
-        base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView);
-    }
-
-    /// <inheritdoc/>
-    protected override bool OnDrawingContent ()
-    {
-        if (Tabs.Any ())
-        {
-            // Region savedClip = SetClip ();
-            _tabsBar.Draw ();
-            _contentView.SetNeedsDraw ();
-            _contentView.Draw ();
-
-            //if (Driver is { })
-            //{
-            //    Driver.Clip = savedClip;
-            //}
-        }
-
-        return true;
-    }
-
-    /// <summary>
-    ///     Removes the given <paramref name="tab"/> from <see cref="Tabs"/>. Caller is responsible for disposing the
-    ///     tab's hosted <see cref="Tab.View"/> if appropriate.
-    /// </summary>
-    /// <param name="tab"></param>
-    public void RemoveTab (Tab? tab)
-    {
-        if (tab is null || !_tabs.Contains (tab))
-        {
-            return;
-        }
-
-        // what tab was selected before closing
-        int idx = _tabs.IndexOf (tab);
-
-        _tabs.Remove (tab);
-
-        // if the currently selected tab is no longer a member of Tabs
-        if (SelectedTab is null || !Tabs.Contains (SelectedTab))
-        {
-            // select the tab closest to the one that disappeared
-            int toSelect = Math.Max (idx - 1, 0);
-
-            if (toSelect < Tabs.Count)
-            {
-                SelectedTab = Tabs.ElementAt (toSelect);
-            }
-            else
-            {
-                SelectedTab = Tabs.LastOrDefault ();
-            }
-        }
-
-        EnsureSelectedTabIsVisible ();
-        SetNeedsLayout ();
-    }
-
-    /// <summary>Event for when <see cref="SelectedTab"/> changes.</summary>
-    public event EventHandler<TabChangedEventArgs>? SelectedTabChanged;
-
-    /// <summary>
-    ///     Changes the <see cref="SelectedTab"/> by the given <paramref name="amount"/>. Positive for right, negative for
-    ///     left.  If no tab is currently selected then the first tab will become selected.
-    /// </summary>
-    /// <param name="amount"></param>
-    public bool SwitchTabBy (int amount)
-    {
-        if (Tabs.Count == 0)
-        {
-            return false;
-        }
-
-        // if there is only one tab anyway or nothing is selected
-        if (Tabs.Count == 1 || SelectedTab is null)
-        {
-            SelectedTab = Tabs.ElementAt (0);
-
-            return SelectedTab is { };
-        }
-
-        int currentIdx = Tabs.IndexOf (SelectedTab);
-
-        // Currently selected tab has vanished!
-        if (currentIdx == -1)
-        {
-            SelectedTab = Tabs.ElementAt (0);
-            return true;
-        }
-
-        int newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1));
-
-        if (newIdx == currentIdx)
-        {
-            return false;
-        }
-
-        SelectedTab = _tabs [newIdx];
-
-        EnsureSelectedTabIsVisible ();
-
-        return true;
-    }
-
-    /// <summary>
-    ///     Event fired when a <see cref="Tab"/> is clicked.  Can be used to cancel navigation, show context menu (e.g. on
-    ///     right click) etc.
-    /// </summary>
-    public event EventHandler<TabMouseEventArgs>? TabClicked;
-
-    /// <summary>Disposes the control and all <see cref="Tabs"/>.</summary>
-    /// <param name="disposing"></param>
-    protected override void Dispose (bool disposing)
-    {
-        base.Dispose (disposing);
-
-        // The selected tab will automatically be disposed but
-        // any tabs not visible will need to be manually disposed
-
-        foreach (Tab tab in Tabs)
-        {
-            if (!Equals (SelectedTab, tab))
-            {
-                tab.View?.Dispose ();
-            }
-        }
-    }
-
-    /// <summary>Raises the <see cref="SelectedTabChanged"/> event.</summary>
-    protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab)
-    {
-        SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab));
-    }
-
-    /// <summary>Returns which tabs to render at each x location.</summary>
-    /// <returns></returns>
-    private IEnumerable<TabToRender> CalculateViewport (Rectangle bounds)
-    {
-        UnSetCurrentTabs ();
-
-        var i = 1;
-        View? prevTab = null;
-
-        // Starting at the first or scrolled to tab
-        foreach (Tab tab in Tabs.Skip (TabScrollOffset))
-        {
-            if (prevTab is { })
-            {
-                tab.X = Pos.Right (prevTab);
-            }
-            else
-            {
-                tab.X = 0;
-            }
-
-            tab.Y = 0;
-
-            // while there is space for the tab
-            int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ());
-
-            string text = tab.DisplayText;
-
-            // The maximum number of characters to use for the tab name as specified
-            // by the user (MaxTabTextWidth).  But not more than the width of the view
-            // or we won't even be able to render a single tab!
-            long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth));
-
-            prevTab = tab;
-
-            tab.Width = 2;
-            tab.Height = Style.ShowTopLine ? 3 : 2;
-
-            // if tab view is width <= 3 don't render any tabs
-            if (maxWidth == 0)
-            {
-                tab.Visible = true;
-                tab.MouseClick += Tab_MouseClick!;
-
-                yield return new TabToRender (tab, string.Empty, Equals (SelectedTab, tab));
-
-                break;
-            }
-
-            if (tabTextWidth > maxWidth)
-            {
-                text = tab.DisplayText.Substring (0, (int)maxWidth);
-                tabTextWidth = (int)maxWidth;
-            }
-
-            tab.Width = Math.Max (tabTextWidth + 2, 1);
-            tab.Height = Style.ShowTopLine ? 3 : 2;
-
-            // if there is not enough space for this tab
-            if (i + tabTextWidth >= bounds.Width)
-            {
-                tab.Visible = false;
-
-                break;
-            }
-
-            // there is enough space!
-            tab.Visible = true;
-            tab.MouseClick += Tab_MouseClick!;
-
-            yield return new TabToRender (tab, text, Equals (SelectedTab, tab));
-
-            i += tabTextWidth + 1;
-        }
-    }
-
-    /// <summary>
-    ///     Returns the number of rows occupied by rendering the tabs, this depends on <see cref="TabStyle.ShowTopLine"/>
-    ///     and can be 0 (e.g. if <see cref="TabStyle.TabsOnBottom"/> and you ask for <paramref name="top"/>).
-    /// </summary>
-    /// <param name="top">True to measure the space required at the top of the control, false to measure space at the bottom.</param>
-    /// .
-    /// <returns></returns>
-    private int GetTabHeight (bool top)
-    {
-        if (top && Style.TabsOnBottom)
-        {
-            return 0;
-        }
-
-        if (!top && !Style.TabsOnBottom)
-        {
-            return 0;
-        }
-
-        return Style.ShowTopLine ? 3 : 2;
-    }
-
-    private void Tab_MouseClick (object sender, MouseEventArgs e)
-    {
-        e.Handled = _tabsBar.NewMouseEvent (e) == true;
-    }
-
-    private void UnSetCurrentTabs ()
-    {
-        if (_tabLocations is { })
-        {
-            foreach (TabToRender tabToRender in _tabLocations)
-            {
-                tabToRender.Tab.MouseClick -= Tab_MouseClick!;
-                tabToRender.Tab.Visible = false;
-            }
-
-            _tabLocations = null;
-        }
-    }
-
-    /// <summary>Raises the <see cref="TabClicked"/> event.</summary>
-    /// <param name="tabMouseEventArgs"></param>
-    private protected virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); }
-
-    private class TabRowView : View
-    {
-        private readonly TabView _host;
-        private readonly View _leftScrollIndicator;
-        private readonly View _rightScrollIndicator;
-
-        public TabRowView (TabView host)
-        {
-            _host = host;
-            Id = "tabRowView";
-
-            CanFocus = true;
-            Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
-            Width = Dim.Fill ();
-
-            _rightScrollIndicator = new View
-            {
-                Id = "rightScrollIndicator",
-                Width = 1,
-                Height = 1,
-                Visible = false,
-                Text = Glyphs.RightArrow.ToString ()
-            };
-            _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!;
-
-            _leftScrollIndicator = new View
-            {
-                Id = "leftScrollIndicator",
-                Width = 1,
-                Height = 1,
-                Visible = false,
-                Text = Glyphs.LeftArrow.ToString ()
-            };
-            _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!;
-
-            Add (_rightScrollIndicator, _leftScrollIndicator);
-        }
-
-        protected override bool OnMouseEvent (MouseEventArgs me)
-        {
-            Tab? hit = me.View as Tab;
-
-            if (me.IsSingleClicked)
-            {
-                _host.OnTabClicked (new TabMouseEventArgs (hit, me));
-
-                // user canceled click
-                if (me.Handled)
-                {
-                    return true;
-                }
-            }
-
-            if (!me.IsSingleDoubleOrTripleClicked)
-            {
-                return false;
-            }
-
-            if (!HasFocus && CanFocus)
-            {
-                SetFocus ();
-            }
-
-            if (me.IsSingleDoubleOrTripleClicked)
-            {
-                var scrollIndicatorHit = 0;
-
-                if (me.View is { } && me.View.Id == "rightScrollIndicator")
-                {
-                    scrollIndicatorHit = 1;
-                }
-                else if (me.View is { } && me.View.Id == "leftScrollIndicator")
-                {
-                    scrollIndicatorHit = -1;
-                }
-
-                if (scrollIndicatorHit != 0)
-                {
-                    _host.SwitchTabBy (scrollIndicatorHit);
-
-                    SetNeedsLayout ();
-
-                    return true;
-                }
-
-                if (hit is { })
-                {
-                    _host.SelectedTab = hit;
-                    SetNeedsLayout ();
-
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        /// <inheritdoc />
-        protected override bool OnClearingViewport ()
-        {
-            // clear any old text
-            ClearViewport ();
-
-            return true;
-        }
-
-        protected override bool OnDrawingContent ()
-        {
-            _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
-
-            RenderTabLine ();
-
-            RenderUnderline ();
-
-            SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
-
-            return true;
-        }
-
-        /// <inheritdoc />
-        protected override bool OnDrawingSubviews ()
-        {
-           // RenderTabLine ();
-
-            return false;
-        }
-
-        protected override void OnDrawComplete ()
-        {
-            if (_host._tabLocations is null)
-            {
-                return;
-            }
-
-            TabToRender [] tabLocations = _host._tabLocations;
-            int selectedTab = -1;
-
-            for (var i = 0; i < tabLocations.Length; i++)
-            {
-                View tab = tabLocations [i].Tab;
-                Rectangle vts = tab.ViewportToScreen (tab.Viewport);
-                var lc = new LineCanvas ();
-                int selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1;
-
-                if (tabLocations [i].IsSelected)
-                {
-                    selectedTab = i;
-
-                    if (i == 0 && _host.TabScrollOffset == 0)
-                    {
-                        if (_host.Style.TabsOnBottom)
-                        {
-                            // Upper left vertical line
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Y - 1),
-                                        -1,
-                                        Orientation.Vertical,
-                                        tab.BorderStyle
-                                       );
-                        }
-                        else
-                        {
-                            // Lower left vertical line
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Bottom - selectedOffset),
-                                        -1,
-                                        Orientation.Vertical,
-                                        tab.BorderStyle
-                                       );
-                        }
-                    }
-                    else if (i > 0 && i <= tabLocations.Length - 1)
-                    {
-                        if (_host.Style.TabsOnBottom)
-                        {
-                            // URCorner
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Y - 1),
-                                        1,
-                                        Orientation.Vertical,
-                                        tab.BorderStyle
-                                       );
-
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Y - 1),
-                                        -1,
-                                        Orientation.Horizontal,
-                                        tab.BorderStyle
-                                       );
-                        }
-                        else
-                        {
-                            // LRCorner
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Bottom - selectedOffset),
-                                        -1,
-                                        Orientation.Vertical,
-                                        tab.BorderStyle
-                                       );
-
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Bottom - selectedOffset),
-                                        -1,
-                                        Orientation.Horizontal,
-                                        tab.BorderStyle
-                                       );
-                        }
-
-                        if (_host.Style.ShowTopLine)
-                        {
-                            if (_host.Style.TabsOnBottom)
-                            {
-                                // Lower left tee
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Bottom),
-                                            -1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Bottom),
-                                            0,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-                            else
-                            {
-                                // Upper left tee
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Y - 1),
-                                            1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Y - 1),
-                                            0,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-                        }
-                    }
-
-                    if (i < tabLocations.Length - 1)
-                    {
-                        if (_host.Style.ShowTopLine)
-                        {
-                            if (_host.Style.TabsOnBottom)
-                            {
-                                // Lower right tee
-                                lc.AddLine (
-                                            new Point (vts.Right, vts.Bottom),
-                                            -1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-
-                                lc.AddLine (
-                                            new Point (vts.Right, vts.Bottom),
-                                            0,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-                            else
-                            {
-                                // Upper right tee
-                                lc.AddLine (
-                                            new Point (vts.Right, vts.Y - 1),
-                                            1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-
-                                lc.AddLine (
-                                            new Point (vts.Right, vts.Y - 1),
-                                            0,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-                        }
-                    }
-
-                    if (_host.Style.TabsOnBottom)
-                    {
-                        //URCorner
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    1,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                    else
-                    {
-                        //LLCorner
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Bottom - selectedOffset),
-                                    -1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Bottom - selectedOffset),
-                                    1,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                }
-                else if (selectedTab == -1)
-                {
-                    if (i == 0 && string.IsNullOrEmpty (tab.Text))
-                    {
-                        if (_host.Style.TabsOnBottom)
-                        {
-                            if (_host.Style.ShowTopLine)
-                            {
-                                // LLCorner
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Bottom),
-                                            -1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Bottom),
-                                            1,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-
-                            // ULCorner
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Y - 1),
-                                        1,
-                                        Orientation.Vertical,
-                                        tab.BorderStyle
-                                       );
-
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Y - 1),
-                                        1,
-                                        Orientation.Horizontal,
-                                        tab.BorderStyle
-                                       );
-                        }
-                        else
-                        {
-                            if (_host.Style.ShowTopLine)
-                            {
-                                // ULCorner
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Y - 1),
-                                            1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-
-                                lc.AddLine (
-                                            new Point (vts.X - 1, vts.Y - 1),
-                                            1,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-
-                            // LLCorner
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Bottom),
-                                        -1,
-                                        Orientation.Vertical,
-                                        tab.BorderStyle
-                                       );
-
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Bottom),
-                                        1,
-                                        Orientation.Horizontal,
-                                        tab.BorderStyle
-                                       );
-                        }
-                    }
-                    else if (i > 0)
-                    {
-                        if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom)
-                        {
-                            // Upper left tee
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Y - 1),
-                                        1,
-                                        Orientation.Vertical,
-                                        tab.BorderStyle
-                                       );
-
-                            lc.AddLine (
-                                        new Point (vts.X - 1, vts.Y - 1),
-                                        0,
-                                        Orientation.Horizontal,
-                                        tab.BorderStyle
-                                       );
-                        }
-
-                        // Lower left tee
-                        lc.AddLine (
-                                    new Point (vts.X - 1, vts.Bottom),
-                                    -1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.X - 1, vts.Bottom),
-                                    0,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                }
-                else if (i < tabLocations.Length - 1)
-                {
-                    if (_host.Style.ShowTopLine)
-                    {
-                        // Upper right tee
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    0,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-
-                    if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom)
-                    {
-                        // Lower right tee
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Bottom),
-                                    -1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Bottom),
-                                    0,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                    else
-                    {
-                        // Upper right tee
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    0,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                }
-
-                if (i == 0 && i != selectedTab && _host.TabScrollOffset == 0 && _host.Style.ShowBorder)
-                {
-                    if (_host.Style.TabsOnBottom)
-                    {
-                        // Upper left vertical line
-                        lc.AddLine (
-                                    new Point (vts.X - 1, vts.Y - 1),
-                                    0,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.X - 1, vts.Y - 1),
-                                    1,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                    else
-                    {
-                        // Lower left vertical line
-                        lc.AddLine (
-                                    new Point (vts.X - 1, vts.Bottom),
-                                    0,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.X - 1, vts.Bottom),
-                                    1,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                }
-
-                if (i == tabLocations.Length - 1 && i != selectedTab)
-                {
-                    if (_host.Style.TabsOnBottom)
-                    {
-                        // Upper right tee
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Y - 1),
-                                    0,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                    else
-                    {
-                        // Lower right tee
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Bottom),
-                                    -1,
-                                    Orientation.Vertical,
-                                    tab.BorderStyle
-                                   );
-
-                        lc.AddLine (
-                                    new Point (vts.Right, vts.Bottom),
-                                    0,
-                                    Orientation.Horizontal,
-                                    tab.BorderStyle
-                                   );
-                    }
-                }
-
-                if (i == tabLocations.Length - 1)
-                {
-                    var arrowOffset = 1;
-
-                    int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 :
-                                          _host.Style.TabsOnBottom ? 1 : 0;
-                    Rectangle tabsBarVts = ViewportToScreen (Viewport);
-                    int lineLength = tabsBarVts.Right - vts.Right;
-
-                    // Right horizontal line
-                    if (ShouldDrawRightScrollIndicator ())
-                    {
-                        if (lineLength - arrowOffset > 0)
-                        {
-                            if (_host.Style.TabsOnBottom)
-                            {
-                                lc.AddLine (
-                                            new Point (vts.Right, vts.Y - lastSelectedTab),
-                                            lineLength - arrowOffset,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-                            else
-                            {
-                                lc.AddLine (
-                                            new Point (
-                                                       vts.Right,
-                                                       vts.Bottom - lastSelectedTab
-                                                      ),
-                                            lineLength - arrowOffset,
-                                            Orientation.Horizontal,
-                                            tab.BorderStyle
-                                           );
-                            }
-                        }
-                    }
-                    else
-                    {
-                        if (_host.Style.TabsOnBottom)
-                        {
-                            lc.AddLine (
-                                        new Point (vts.Right, vts.Y - lastSelectedTab),
-                                        lineLength,
-                                        Orientation.Horizontal,
-                                        tab.BorderStyle
-                                       );
-                        }
-                        else
-                        {
-                            lc.AddLine (
-                                        new Point (vts.Right, vts.Bottom - lastSelectedTab),
-                                        lineLength,
-                                        Orientation.Horizontal,
-                                        tab.BorderStyle
-                                       );
-                        }
-
-                        if (_host.Style.ShowBorder)
-                        {
-                            if (_host.Style.TabsOnBottom)
-                            {
-                                // More LRCorner
-                                lc.AddLine (
-                                            new Point (
-                                                       tabsBarVts.Right - 1,
-                                                       vts.Y - lastSelectedTab
-                                                      ),
-                                            -1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-                            }
-                            else
-                            {
-                                // More URCorner
-                                lc.AddLine (
-                                            new Point (
-                                                       tabsBarVts.Right - 1,
-                                                       vts.Bottom - lastSelectedTab
-                                                      ),
-                                            1,
-                                            Orientation.Vertical,
-                                            tab.BorderStyle
-                                           );
-                            }
-                        }
-                    }
-                }
-
-                tab.LineCanvas.Merge (lc);
-                tab.RenderLineCanvas ();
-
-               // RenderUnderline ();
-            }
-        }
-
-        private int GetUnderlineYPosition ()
-        {
-            if (_host.Style.TabsOnBottom)
-            {
-                return 0;
-            }
-
-            return _host.Style.ShowTopLine ? 2 : 1;
-        }
-
-        /// <summary>Renders the line with the tab names in it.</summary>
-        private void RenderTabLine ()
-        {
-            TabToRender []? tabLocations = _host._tabLocations;
-
-            if (tabLocations is null)
-            {
-                return;
-            }
-
-            View? selected = null;
-            int topLine = _host.Style.ShowTopLine ? 1 : 0;
-
-            foreach (TabToRender toRender in tabLocations)
-            {
-                Tab tab = toRender.Tab;
-
-                if (toRender.IsSelected)
-                {
-                    selected = tab;
-
-                    if (_host.Style.TabsOnBottom)
-                    {
-                        tab.Border.Thickness = new Thickness (1, 0, 1, topLine);
-                        tab.Margin.Thickness = new Thickness (0, 1, 0, 0);
-                    }
-                    else
-                    {
-                        tab.Border.Thickness = new Thickness (1, topLine, 1, 0);
-                        tab.Margin.Thickness = new Thickness (0, 0, 0, topLine);
-                    }
-                }
-                else if (selected is null)
-                {
-                    if (_host.Style.TabsOnBottom)
-                    {
-                        tab.Border.Thickness = new Thickness (1, 1, 0, topLine);
-                        tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
-                    }
-                    else
-                    {
-                        tab.Border.Thickness = new Thickness (1, topLine, 0, 1);
-                        tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
-                    }
-
-                    tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1);
-                }
-                else
-                {
-                    if (_host.Style.TabsOnBottom)
-                    {
-                        tab.Border.Thickness = new Thickness (0, 1, 1, topLine);
-                        tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
-                    }
-                    else
-                    {
-                        tab.Border.Thickness = new Thickness (0, topLine, 1, 1);
-                        tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
-                    }
-
-                    tab.Width = Math.Max (tab.Width!.GetAnchor (0) - 1, 1);
-                }
-
-                tab.Text = toRender.TextToRender;
-
-                // BUGBUG: Layout should only be called from Mainloop iteration!
-                Layout ();
-
-                tab.DrawBorderAndPadding ();
-
-                Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default;
-
-                // if tab is the selected one and focus is inside this control
-                if (toRender.IsSelected && _host.HasFocus)
-                {
-                    if (_host.Focused == this)
-                    {
-                        // if focus is the tab bar itself then show that they can switch tabs
-                        prevAttr = ColorScheme.HotFocus;
-                    }
-                    else
-                    {
-                        // Focus is inside the tab
-                        prevAttr = ColorScheme.HotNormal;
-                    }
-                }
-
-                tab.TextFormatter.Draw (
-                                        tab.ViewportToScreen (tab.Viewport),
-                                        prevAttr,
-                                        ColorScheme.HotNormal
-                                       );
-
-                tab.DrawBorderAndPadding ();
-
-
-                SetAttribute (GetNormalColor ());
-            }
-        }
-
-        /// <summary>Renders the line of the tab that adjoins the content of the tab.</summary>
-        private void RenderUnderline ()
-        {
-            int y = GetUnderlineYPosition ();
-
-            TabToRender? selected = _host._tabLocations?.FirstOrDefault (t => t.IsSelected);
-
-            if (selected is null)
-            {
-                return;
-            }
-
-            // draw scroll indicators
-
-            // if there are more tabs to the left not visible
-            if (_host.TabScrollOffset > 0)
-            {
-                _leftScrollIndicator.X = 0;
-                _leftScrollIndicator.Y = y;
-
-                // indicate that
-                _leftScrollIndicator.Visible = true;
-
-                // Ensures this is clicked instead of the first tab
-                MoveSubviewToEnd (_leftScrollIndicator);
-                _leftScrollIndicator.Draw ();
-            }
-            else
-            {
-                _leftScrollIndicator.Visible = false;
-            }
-
-            // if there are more tabs to the right not visible
-            if (ShouldDrawRightScrollIndicator ())
-            {
-                _rightScrollIndicator.X = Viewport.Width - 1;
-                _rightScrollIndicator.Y = y;
-
-                // indicate that
-                _rightScrollIndicator.Visible = true;
-
-                // Ensures this is clicked instead of the last tab if under this
-                MoveSubviewToStart (_rightScrollIndicator);
-                _rightScrollIndicator.Draw ();
-            }
-            else
-            {
-                _rightScrollIndicator.Visible = false;
-            }
-        }
-
-        private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault (); }
-    }
-
-    private class TabToRender
-    {
-        public TabToRender (Tab tab, string textToRender, bool isSelected)
-        {
-            Tab = tab;
-            IsSelected = isSelected;
-            TextToRender = textToRender;
-        }
-
-        /// <summary>True if the tab that is being rendered is the selected one.</summary>
-        /// <value></value>
-        public bool IsSelected { get; }
-
-        public Tab Tab { get; }
-        public string TextToRender { get; }
-    }
-}

+ 1 - 1
Terminal.Gui/Views/Tab.cs → Terminal.Gui/Views/TabView/Tab.cs

@@ -22,7 +22,7 @@ public class Tab : View
         set
         {
             _displayText = value;
-            SetNeedsDraw ();
+            SetNeedsLayout ();
         }
     }
 

+ 0 - 0
Terminal.Gui/Views/TabChangedEventArgs.cs → Terminal.Gui/Views/TabView/TabChangedEventArgs.cs


+ 0 - 0
Terminal.Gui/Views/TabMouseEventArgs.cs → Terminal.Gui/Views/TabView/TabMouseEventArgs.cs


+ 793 - 0
Terminal.Gui/Views/TabView/TabRowView.cs

@@ -0,0 +1,793 @@
+#nullable enable
+namespace Terminal.Gui;
+
+internal class TabRowView : View
+{
+    private readonly TabView _host;
+    private readonly View _leftScrollIndicator;
+    private readonly View _rightScrollIndicator;
+
+    public TabRowView (TabView host)
+    {
+        _host = host;
+        Id = "tabRowView";
+
+        CanFocus = true;
+        Width = Dim.Fill ();
+
+        _rightScrollIndicator = new View
+        {
+            Id = "rightScrollIndicator",
+            Width = 1,
+            Height = 1,
+            Visible = false,
+            Text = Glyphs.RightArrow.ToString ()
+        };
+        _rightScrollIndicator.MouseClick += _host.Tab_MouseClick!;
+
+        _leftScrollIndicator = new View
+        {
+            Id = "leftScrollIndicator",
+            Width = 1,
+            Height = 1,
+            Visible = false,
+            Text = Glyphs.LeftArrow.ToString ()
+        };
+        _leftScrollIndicator.MouseClick += _host.Tab_MouseClick!;
+
+        Add (_rightScrollIndicator, _leftScrollIndicator);
+    }
+
+    protected override bool OnMouseEvent (MouseEventArgs me)
+    {
+        View? parent = me.View is Adornment adornment ? adornment.Parent : me.View;
+        Tab? hit = parent as Tab;
+
+        if (me.IsSingleClicked)
+        {
+            _host.OnTabClicked (new TabMouseEventArgs (hit!, me));
+
+            // user canceled click
+            if (me.Handled)
+            {
+                return true;
+            }
+
+            if (parent == _host.SelectedTab)
+            {
+                _host.SelectedTab?.SetFocus ();
+            }
+        }
+
+        if (!me.IsSingleDoubleOrTripleClicked)
+        {
+            return false;
+        }
+
+        if (!HasFocus && CanFocus)
+        {
+            SetFocus ();
+        }
+
+        if (me.IsSingleDoubleOrTripleClicked)
+        {
+            var scrollIndicatorHit = 0;
+
+            if (me.View is { Id: "rightScrollIndicator" })
+            {
+                scrollIndicatorHit = 1;
+            }
+            else if (me.View is { Id: "leftScrollIndicator" })
+            {
+                scrollIndicatorHit = -1;
+            }
+
+            if (scrollIndicatorHit != 0)
+            {
+                _host.SwitchTabBy (scrollIndicatorHit);
+
+                return true;
+            }
+
+            if (hit is { })
+            {
+                _host.SelectedTab = hit;
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc />
+    protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
+    {
+        if (_host.SelectedTab is { HasFocus: false, CanFocus: true } && focusedView == this)
+        {
+            _host.SelectedTab?.SetFocus ();
+
+            return;
+        }
+
+        base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView);
+    }
+
+    /// <inheritdoc/>
+    protected override void OnSubviewLayout (LayoutEventArgs args)
+    {
+        _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
+
+        RenderTabLine ();
+
+        RenderUnderline ();
+
+        base.OnSubviewLayout (args);
+    }
+
+    /// <inheritdoc />
+    protected override bool OnRenderingLineCanvas ()
+    {
+        RenderTabLineCanvas ();
+
+        return false;
+    }
+
+    private void RenderTabLineCanvas ()
+    {
+        if (_host._tabLocations is null)
+        {
+            return;
+        }
+
+        Tab [] tabLocations = _host._tabLocations;
+        int selectedTab = -1;
+        var lc = new LineCanvas ();
+
+        for (var i = 0; i < tabLocations.Length; i++)
+        {
+            View tab = tabLocations [i];
+            Rectangle vts = tab.ViewportToScreen (tab.Viewport);
+            int selectedOffset = _host.Style.ShowTopLine && tabLocations [i] == _host.SelectedTab ? 0 : 1;
+
+            if (tabLocations [i] == _host.SelectedTab)
+            {
+                selectedTab = i;
+
+                if (i == 0 && _host.TabScrollOffset == 0)
+                {
+                    if (_host.Style.TabsOnBottom)
+                    {
+                        // Upper left vertical line
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Y - 1),
+                                    -1,
+                                    Orientation.Vertical,
+                                    tab.BorderStyle
+                                   );
+                    }
+                    else
+                    {
+                        // Lower left vertical line
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Bottom - selectedOffset),
+                                    -1,
+                                    Orientation.Vertical,
+                                    tab.BorderStyle
+                                   );
+                    }
+                }
+                else if (i > 0 && i <= tabLocations.Length - 1)
+                {
+                    if (_host.Style.TabsOnBottom)
+                    {
+                        // URCorner
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Y - 1),
+                                    1,
+                                    Orientation.Vertical,
+                                    tab.BorderStyle
+                                   );
+
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Y - 1),
+                                    -1,
+                                    Orientation.Horizontal,
+                                    tab.BorderStyle
+                                   );
+                    }
+                    else
+                    {
+                        // LRCorner
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Bottom - selectedOffset),
+                                    -1,
+                                    Orientation.Vertical,
+                                    tab.BorderStyle
+                                   );
+
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Bottom - selectedOffset),
+                                    -1,
+                                    Orientation.Horizontal,
+                                    tab.BorderStyle
+                                   );
+                    }
+
+                    if (_host.Style.ShowTopLine)
+                    {
+                        if (_host.Style.TabsOnBottom)
+                        {
+                            // Lower left tee
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Bottom),
+                                        -1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Bottom),
+                                        0,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+                        else
+                        {
+                            // Upper left tee
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Y - 1),
+                                        1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Y - 1),
+                                        0,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+                    }
+                }
+
+                if (i < tabLocations.Length - 1)
+                {
+                    if (_host.Style.ShowTopLine)
+                    {
+                        if (_host.Style.TabsOnBottom)
+                        {
+                            // Lower right tee
+                            lc.AddLine (
+                                        new Point (vts.Right, vts.Bottom),
+                                        -1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+
+                            lc.AddLine (
+                                        new Point (vts.Right, vts.Bottom),
+                                        0,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+                        else
+                        {
+                            // Upper right tee
+                            lc.AddLine (
+                                        new Point (vts.Right, vts.Y - 1),
+                                        1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+
+                            lc.AddLine (
+                                        new Point (vts.Right, vts.Y - 1),
+                                        0,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+                    }
+                }
+
+                if (_host.Style.TabsOnBottom)
+                {
+                    //URCorner
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                1,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+                else
+                {
+                    //LLCorner
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Bottom - selectedOffset),
+                                -1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Bottom - selectedOffset),
+                                1,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+            }
+            else if (selectedTab == -1)
+            {
+                if (i == 0 && string.IsNullOrEmpty (tab.Text))
+                {
+                    if (_host.Style.TabsOnBottom)
+                    {
+                        if (_host.Style.ShowTopLine)
+                        {
+                            // LLCorner
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Bottom),
+                                        -1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Bottom),
+                                        1,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+
+                        // ULCorner
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Y - 1),
+                                    1,
+                                    Orientation.Vertical,
+                                    tab.BorderStyle
+                                   );
+
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Y - 1),
+                                    1,
+                                    Orientation.Horizontal,
+                                    tab.BorderStyle
+                                   );
+                    }
+                    else
+                    {
+                        if (_host.Style.ShowTopLine)
+                        {
+                            // ULCorner
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Y - 1),
+                                        1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+
+                            lc.AddLine (
+                                        new Point (vts.X - 1, vts.Y - 1),
+                                        1,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+
+                        // LLCorner
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Bottom),
+                                    -1,
+                                    Orientation.Vertical,
+                                    tab.BorderStyle
+                                   );
+
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Bottom),
+                                    1,
+                                    Orientation.Horizontal,
+                                    tab.BorderStyle
+                                   );
+                    }
+                }
+                else if (i > 0)
+                {
+                    if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom)
+                    {
+                        // Upper left tee
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Y - 1),
+                                    1,
+                                    Orientation.Vertical,
+                                    tab.BorderStyle
+                                   );
+
+                        lc.AddLine (
+                                    new Point (vts.X - 1, vts.Y - 1),
+                                    0,
+                                    Orientation.Horizontal,
+                                    tab.BorderStyle
+                                   );
+                    }
+
+                    // Lower left tee
+                    lc.AddLine (
+                                new Point (vts.X - 1, vts.Bottom),
+                                -1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.X - 1, vts.Bottom),
+                                0,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+            }
+            else if (i < tabLocations.Length - 1)
+            {
+                if (_host.Style.ShowTopLine)
+                {
+                    // Upper right tee
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                0,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+
+                if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom)
+                {
+                    // Lower right tee
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Bottom),
+                                -1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Bottom),
+                                0,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+                else
+                {
+                    // Upper right tee
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                0,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+            }
+
+            if (i == 0 && i != selectedTab && _host is { TabScrollOffset: 0, Style.ShowBorder: true })
+            {
+                if (_host.Style.TabsOnBottom)
+                {
+                    // Upper left vertical line
+                    lc.AddLine (
+                                new Point (vts.X - 1, vts.Y - 1),
+                                0,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.X - 1, vts.Y - 1),
+                                1,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+                else
+                {
+                    // Lower left vertical line
+                    lc.AddLine (
+                                new Point (vts.X - 1, vts.Bottom),
+                                0,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.X - 1, vts.Bottom),
+                                1,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+            }
+
+            if (i == tabLocations.Length - 1 && i != selectedTab)
+            {
+                if (_host.Style.TabsOnBottom)
+                {
+                    // Upper right tee
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Y - 1),
+                                0,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+                else
+                {
+                    // Lower right tee
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Bottom),
+                                -1,
+                                Orientation.Vertical,
+                                tab.BorderStyle
+                               );
+
+                    lc.AddLine (
+                                new Point (vts.Right, vts.Bottom),
+                                0,
+                                Orientation.Horizontal,
+                                tab.BorderStyle
+                               );
+                }
+            }
+
+            if (i == tabLocations.Length - 1)
+            {
+                var arrowOffset = 1;
+
+                int lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 :
+                                      _host.Style.TabsOnBottom ? 1 : 0;
+                Rectangle tabsBarVts = ViewportToScreen (Viewport);
+                int lineLength = tabsBarVts.Right - vts.Right;
+
+                // Right horizontal line
+                if (ShouldDrawRightScrollIndicator ())
+                {
+                    if (lineLength - arrowOffset > 0)
+                    {
+                        if (_host.Style.TabsOnBottom)
+                        {
+                            lc.AddLine (
+                                        new Point (vts.Right, vts.Y - lastSelectedTab),
+                                        lineLength - arrowOffset,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+                        else
+                        {
+                            lc.AddLine (
+                                        new Point (
+                                                   vts.Right,
+                                                   vts.Bottom - lastSelectedTab
+                                                  ),
+                                        lineLength - arrowOffset,
+                                        Orientation.Horizontal,
+                                        tab.BorderStyle
+                                       );
+                        }
+                    }
+                }
+                else
+                {
+                    // Right corner
+                    if (_host.Style.TabsOnBottom)
+                    {
+                        lc.AddLine (
+                                    new Point (vts.Right, vts.Y - lastSelectedTab),
+                                    lineLength,
+                                    Orientation.Horizontal,
+                                    tab.BorderStyle
+                                   );
+                    }
+                    else
+                    {
+                        lc.AddLine (
+                                    new Point (vts.Right, vts.Bottom - lastSelectedTab),
+                                    lineLength,
+                                    Orientation.Horizontal,
+                                    tab.BorderStyle
+                                   );
+                    }
+
+                    if (_host.Style.ShowBorder)
+                    {
+                        if (_host.Style.TabsOnBottom)
+                        {
+                            // More LRCorner
+                            lc.AddLine (
+                                        new Point (
+                                                   tabsBarVts.Right - 1,
+                                                   vts.Y - lastSelectedTab
+                                                  ),
+                                        -1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+                        }
+                        else
+                        {
+                            // More URCorner
+                            lc.AddLine (
+                                        new Point (
+                                                   tabsBarVts.Right - 1,
+                                                   vts.Bottom - lastSelectedTab
+                                                  ),
+                                        1,
+                                        Orientation.Vertical,
+                                        tab.BorderStyle
+                                       );
+                        }
+                    }
+                }
+            }
+        }
+
+        _host.LineCanvas.Merge (lc);
+    }
+
+    private int GetUnderlineYPosition ()
+    {
+        if (_host.Style.TabsOnBottom)
+        {
+            return 0;
+        }
+
+        return _host.Style.ShowTopLine ? 2 : 1;
+    }
+
+    /// <summary>Renders the line with the tab names in it.</summary>
+    private void RenderTabLine ()
+    {
+        if (_host._tabLocations is null)
+        {
+            return;
+        }
+
+        View? selected = null;
+        int topLine = _host.Style.ShowTopLine ? 1 : 0;
+
+        foreach (Tab toRender in _host._tabLocations)
+        {
+            Tab tab = toRender;
+
+            if (toRender == _host.SelectedTab)
+            {
+                selected = tab;
+
+                if (_host.Style.TabsOnBottom)
+                {
+                    tab.Border!.Thickness = new (1, 0, 1, topLine);
+                    tab.Margin!.Thickness = new (0, 1, 0, 0);
+                }
+                else
+                {
+                    tab.Border!.Thickness = new (1, topLine, 1, 0);
+                    tab.Margin!.Thickness = new (0, 0, 0, topLine);
+                }
+            }
+            else if (selected is null)
+            {
+                if (_host.Style.TabsOnBottom)
+                {
+                    tab.Border!.Thickness = new (1, 1, 1, topLine);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+                else
+                {
+                    tab.Border!.Thickness = new (1, topLine, 1, 1);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+            }
+            else
+            {
+                if (_host.Style.TabsOnBottom)
+                {
+                    tab.Border!.Thickness = new (1, 1, 1, topLine);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+                else
+                {
+                    tab.Border!.Thickness = new (1, topLine, 1, 1);
+                    tab.Margin!.Thickness = new (0, 0, 0, 0);
+                }
+            }
+
+            // Ensures updating TextFormatter constrains
+            tab.TextFormatter.ConstrainToWidth = tab.GetContentSize ().Width;
+            tab.TextFormatter.ConstrainToHeight = tab.GetContentSize ().Height;
+        }
+    }
+
+    /// <summary>Renders the line of the tab that adjoins the content of the tab.</summary>
+    private void RenderUnderline ()
+    {
+        int y = GetUnderlineYPosition ();
+
+        Tab? selected = _host._tabLocations?.FirstOrDefault (t => t == _host.SelectedTab);
+
+        if (selected is null)
+        {
+            return;
+        }
+
+        // draw scroll indicators
+
+        // if there are more tabs to the left not visible
+        if (_host.TabScrollOffset > 0)
+        {
+            _leftScrollIndicator.X = 0;
+            _leftScrollIndicator.Y = y;
+
+            // indicate that
+            _leftScrollIndicator.Visible = true;
+
+            // Ensures this is clicked instead of the first tab
+            MoveSubviewToEnd (_leftScrollIndicator);
+        }
+        else
+        {
+            _leftScrollIndicator.Visible = false;
+        }
+
+        // if there are more tabs to the right not visible
+        if (ShouldDrawRightScrollIndicator ())
+        {
+            _rightScrollIndicator.X = Viewport.Width - 1;
+            _rightScrollIndicator.Y = y;
+
+            // indicate that
+            _rightScrollIndicator.Visible = true;
+
+            // Ensures this is clicked instead of the last tab if under this
+            MoveSubviewToStart (_rightScrollIndicator);
+        }
+        else
+        {
+            _rightScrollIndicator.Visible = false;
+        }
+    }
+
+    private bool ShouldDrawRightScrollIndicator () { return _host._tabLocations!.LastOrDefault () != _host.Tabs.LastOrDefault (); }
+}

+ 0 - 0
Terminal.Gui/Views/TabStyle.cs → Terminal.Gui/Views/TabView/TabStyle.cs


+ 585 - 0
Terminal.Gui/Views/TabView/TabView.cs

@@ -0,0 +1,585 @@
+#nullable enable
+namespace Terminal.Gui;
+
+/// <summary>Control that hosts multiple sub views, presenting a single one at once.</summary>
+public class TabView : View
+{
+    /// <summary>The default <see cref="MaxTabTextWidth"/> to set on new <see cref="TabView"/> controls.</summary>
+    public const uint DefaultMaxTabTextWidth = 30;
+
+    /// <summary>
+    ///     This sub view is the main client area of the current tab.  It hosts the <see cref="Tab.View"/> of the tab, the
+    ///     <see cref="SelectedTab"/>.
+    /// </summary>
+    private readonly View _containerView;
+
+    private readonly List<Tab> _tabs = new ();
+
+    /// <summary>This sub view is the 2 or 3 line control that represents the actual tabs themselves.</summary>
+    private readonly TabRowView _tabsBar;
+
+    private Tab? _selectedTab;
+
+    internal Tab []? _tabLocations;
+    private int _tabScrollOffset;
+
+    /// <summary>Initializes a <see cref="TabView"/> class.</summary>
+    public TabView ()
+    {
+        CanFocus = true;
+        TabStop = TabBehavior.TabStop; // Because TabView has focusable subviews, it must be a TabGroup
+        _tabsBar = new TabRowView (this);
+        _containerView = new ();
+        ApplyStyleChanges ();
+
+        base.Add (_tabsBar);
+        base.Add (_containerView);
+
+        // Things this view knows how to do
+        AddCommand (Command.Left, () => SwitchTabBy (-1));
+
+        AddCommand (Command.Right, () => SwitchTabBy (1));
+
+        AddCommand (
+                    Command.LeftStart,
+                    () =>
+                    {
+                        TabScrollOffset = 0;
+                        SelectedTab = Tabs.FirstOrDefault ()!;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.RightEnd,
+                    () =>
+                    {
+                        TabScrollOffset = Tabs.Count - 1;
+                        SelectedTab = Tabs.LastOrDefault ()!;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.PageDown,
+                    () =>
+                    {
+                        TabScrollOffset += _tabLocations!.Length;
+                        SelectedTab = Tabs.ElementAt (TabScrollOffset);
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.PageUp,
+                    () =>
+                    {
+                        TabScrollOffset -= _tabLocations!.Length;
+                        SelectedTab = Tabs.ElementAt (TabScrollOffset);
+
+                        return true;
+                    }
+                   );
+
+        // Default keybindings for this view
+        KeyBindings.Add (Key.CursorLeft, Command.Left);
+        KeyBindings.Add (Key.CursorRight, Command.Right);
+        KeyBindings.Add (Key.Home, Command.LeftStart);
+        KeyBindings.Add (Key.End, Command.RightEnd);
+        KeyBindings.Add (Key.PageDown, Command.PageDown);
+        KeyBindings.Add (Key.PageUp, Command.PageUp);
+    }
+
+    /// <summary>
+    ///     The maximum number of characters to render in a Tab header.  This prevents one long tab from pushing out all
+    ///     the others.
+    /// </summary>
+    public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth;
+
+    // This is needed to hold initial value because it may change during the setter process
+    private bool _selectedTabHasFocus;
+
+    /// <summary>The currently selected member of <see cref="Tabs"/> chosen by the user.</summary>
+    /// <value></value>
+    public Tab? SelectedTab
+    {
+        get => _selectedTab;
+        set
+        {
+            if (value == _selectedTab)
+            {
+                return;
+            }
+
+            Tab? old = _selectedTab;
+            _selectedTabHasFocus = old is { } && (old.HasFocus || !_containerView.CanFocus);
+
+            if (_selectedTab is { })
+            {
+                if (_selectedTab.View is { })
+                {
+                    _selectedTab.View.CanFocusChanged -= ContainerViewCanFocus!;
+                    // remove old content
+                    _containerView.Remove (_selectedTab.View);
+                }
+            }
+
+            _selectedTab = value;
+
+            // add new content
+            if (_selectedTab?.View != null)
+            {
+                _selectedTab.View.CanFocusChanged += ContainerViewCanFocus!;
+                _containerView.Add (_selectedTab.View);
+            }
+
+            ContainerViewCanFocus (null!, null!);
+
+            EnsureSelectedTabIsVisible ();
+
+            if (old != _selectedTab)
+            {
+                if (TabCanSetFocus ())
+                {
+                    SelectedTab?.SetFocus ();
+                }
+
+                OnSelectedTabChanged (old!, _selectedTab!);
+            }
+            SetNeedsLayout ();
+        }
+    }
+
+    private bool TabCanSetFocus ()
+    {
+        return IsInitialized && SelectedTab is { } && (_selectedTabHasFocus || !_containerView.CanFocus);
+    }
+
+    private void ContainerViewCanFocus (object sender, EventArgs eventArgs)
+    {
+        _containerView.CanFocus = _containerView.Subviews.Count (v => v.CanFocus) > 0;
+    }
+
+    private TabStyle _style = new ();
+
+    /// <summary>Render choices for how to display tabs.  After making changes, call <see cref="ApplyStyleChanges()"/>.</summary>
+    /// <value></value>
+    public TabStyle Style
+    {
+        get => _style;
+        set
+        {
+            if (_style == value)
+            {
+                return;
+            }
+            _style = value;
+            SetNeedsLayout ();
+        }
+    }
+
+    /// <summary>All tabs currently hosted by the control.</summary>
+    /// <value></value>
+    public IReadOnlyCollection<Tab> Tabs => _tabs.AsReadOnly ();
+
+    /// <summary>When there are too many tabs to render, this indicates the first tab to render on the screen.</summary>
+    /// <value></value>
+    public int TabScrollOffset
+    {
+        get => _tabScrollOffset;
+        set
+        {
+            _tabScrollOffset = EnsureValidScrollOffsets (value);
+            SetNeedsLayout ();
+        }
+    }
+
+    /// <summary>Adds the given <paramref name="tab"/> to <see cref="Tabs"/>.</summary>
+    /// <param name="tab"></param>
+    /// <param name="andSelect">True to make the newly added Tab the <see cref="SelectedTab"/>.</param>
+    public void AddTab (Tab tab, bool andSelect)
+    {
+        if (_tabs.Contains (tab))
+        {
+            return;
+        }
+
+        _tabs.Add (tab);
+        _tabsBar.Add (tab);
+
+        if (SelectedTab is null || andSelect)
+        {
+            SelectedTab = tab;
+
+            EnsureSelectedTabIsVisible ();
+
+            tab.View?.SetFocus ();
+        }
+
+        SetNeedsLayout ();
+    }
+
+    /// <summary>
+    ///     Updates the control to use the latest state settings in <see cref="Style"/>. This can change the size of the
+    ///     client area of the tab (for rendering the selected tab's content).  This method includes a call to
+    ///     <see cref="View.SetNeedsDraw()"/>.
+    /// </summary>
+    public void ApplyStyleChanges ()
+    {
+        _containerView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None;
+        _containerView.Width = Dim.Fill ();
+
+        if (Style.TabsOnBottom)
+        {
+            // Tabs are along the bottom so just dodge the border
+            if (Style.ShowBorder)
+            {
+                _containerView.Border!.Thickness = new Thickness (1, 1, 1, 0);
+            }
+
+            _containerView.Y = 0;
+
+            int tabHeight = GetTabHeight (false);
+
+            // Fill client area leaving space at bottom for tabs
+            _containerView.Height = Dim.Fill (tabHeight);
+
+            _tabsBar.Height = tabHeight;
+
+            _tabsBar.Y = Pos.Bottom (_containerView);
+        }
+        else
+        {
+            // Tabs are along the top
+            if (Style.ShowBorder)
+            {
+                _containerView.Border!.Thickness = new Thickness (1, 0, 1, 1);
+            }
+
+            _tabsBar.Y = 0;
+
+            int tabHeight = GetTabHeight (true);
+
+            //move content down to make space for tabs
+            _containerView.Y = Pos.Bottom (_tabsBar);
+
+            // Fill client area leaving space at bottom for border
+            _containerView.Height = Dim.Fill ();
+
+            // The top tab should be 2 or 3 rows high and on the top
+
+            _tabsBar.Height = tabHeight;
+
+            // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0
+        }
+
+        SetNeedsLayout ();
+    }
+
+    /// <inheritdoc />
+    protected override void OnViewportChanged (DrawEventArgs e)
+    {
+        _tabLocations = CalculateViewport (Viewport).ToArray ();
+
+        base.OnViewportChanged (e);
+    }
+
+    /// <summary>Updates <see cref="TabScrollOffset"/> to ensure that <see cref="SelectedTab"/> is visible.</summary>
+    public void EnsureSelectedTabIsVisible ()
+    {
+        if (!IsInitialized || SelectedTab is null)
+        {
+            return;
+        }
+
+        // if current viewport does not include the selected tab
+        if (!CalculateViewport (Viewport).Any (t => Equals (SelectedTab, t)))
+        {
+            // Set scroll offset so the first tab rendered is the
+            TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab));
+        }
+    }
+
+    /// <summary>Updates <see cref="TabScrollOffset"/> to be a valid index of <see cref="Tabs"/>.</summary>
+    /// <param name="value">The value to validate.</param>
+    /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDraw()"/>.</remarks>
+    /// <returns>The valid <see cref="TabScrollOffset"/> for the given value.</returns>
+    public int EnsureValidScrollOffsets (int value) { return Math.Max (Math.Min (value, Tabs.Count - 1), 0); }
+
+    /// <inheritdoc />
+    protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView)
+    {
+        if (SelectedTab is { HasFocus: false } && !_containerView.CanFocus && focusedView == this)
+        {
+            SelectedTab?.SetFocus ();
+
+            return;
+        }
+
+        base.OnHasFocusChanged (newHasFocus, previousFocusedView, focusedView);
+    }
+
+    /// <summary>
+    ///     Removes the given <paramref name="tab"/> from <see cref="Tabs"/>. Caller is responsible for disposing the
+    ///     tab's hosted <see cref="Tab.View"/> if appropriate.
+    /// </summary>
+    /// <param name="tab"></param>
+    public void RemoveTab (Tab? tab)
+    {
+        if (tab is null || !_tabs.Contains (tab))
+        {
+            return;
+        }
+
+        // what tab was selected before closing
+        int idx = _tabs.IndexOf (tab);
+
+        _tabs.Remove (tab);
+
+        // if the currently selected tab is no longer a member of Tabs
+        if (SelectedTab is null || !Tabs.Contains (SelectedTab))
+        {
+            // select the tab closest to the one that disappeared
+            int toSelect = Math.Max (idx - 1, 0);
+
+            if (toSelect < Tabs.Count)
+            {
+                SelectedTab = Tabs.ElementAt (toSelect);
+            }
+            else
+            {
+                SelectedTab = Tabs.LastOrDefault ();
+            }
+        }
+
+        EnsureSelectedTabIsVisible ();
+        SetNeedsLayout ();
+    }
+
+    /// <summary>Event for when <see cref="SelectedTab"/> changes.</summary>
+    public event EventHandler<TabChangedEventArgs>? SelectedTabChanged;
+
+    /// <summary>
+    ///     Changes the <see cref="SelectedTab"/> by the given <paramref name="amount"/>. Positive for right, negative for
+    ///     left.  If no tab is currently selected then the first tab will become selected.
+    /// </summary>
+    /// <param name="amount"></param>
+    public bool SwitchTabBy (int amount)
+    {
+        if (Tabs.Count == 0)
+        {
+            return false;
+        }
+
+        // if there is only one tab anyway or nothing is selected
+        if (Tabs.Count == 1 || SelectedTab is null)
+        {
+            SelectedTab = Tabs.ElementAt (0);
+
+            return SelectedTab is { };
+        }
+
+        int currentIdx = Tabs.IndexOf (SelectedTab);
+
+        // Currently selected tab has vanished!
+        if (currentIdx == -1)
+        {
+            SelectedTab = Tabs.ElementAt (0);
+            return true;
+        }
+
+        int newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1));
+
+        if (newIdx == currentIdx)
+        {
+            return false;
+        }
+
+        SelectedTab = _tabs [newIdx];
+
+        EnsureSelectedTabIsVisible ();
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Event fired when a <see cref="Tab"/> is clicked.  Can be used to cancel navigation, show context menu (e.g. on
+    ///     right click) etc.
+    /// </summary>
+    public event EventHandler<TabMouseEventArgs>? TabClicked;
+
+    /// <summary>Disposes the control and all <see cref="Tabs"/>.</summary>
+    /// <param name="disposing"></param>
+    protected override void Dispose (bool disposing)
+    {
+        base.Dispose (disposing);
+
+        // The selected tab will automatically be disposed but
+        // any tabs not visible will need to be manually disposed
+
+        foreach (Tab tab in Tabs)
+        {
+            if (!Equals (SelectedTab, tab))
+            {
+                tab.View?.Dispose ();
+            }
+        }
+    }
+
+    /// <summary>Raises the <see cref="SelectedTabChanged"/> event.</summary>
+    protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab)
+    {
+        SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab));
+    }
+
+    /// <summary>Returns which tabs to render at each x location.</summary>
+    /// <returns></returns>
+    internal IEnumerable<Tab> CalculateViewport (Rectangle bounds)
+    {
+        UnSetCurrentTabs ();
+
+        var i = 1;
+        View? prevTab = null;
+
+        // Starting at the first or scrolled to tab
+        foreach (Tab tab in Tabs.Skip (TabScrollOffset))
+        {
+            if (prevTab is { })
+            {
+                tab.X = Pos.Right (prevTab) - 1;
+            }
+            else
+            {
+                tab.X = 0;
+            }
+
+            tab.Y = 0;
+
+            // while there is space for the tab
+            int tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ());
+
+            // The maximum number of characters to use for the tab name as specified
+            // by the user (MaxTabTextWidth).  But not more than the width of the view
+            // or we won't even be able to render a single tab!
+            long maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth));
+
+            tab.Width = 2;
+            tab.Height = Style.ShowTopLine ? 3 : 2;
+
+            // if tab view is width <= 3 don't render any tabs
+            if (maxWidth == 0)
+            {
+                tab.Visible = true;
+                tab.MouseClick += Tab_MouseClick!;
+                tab.Border!.MouseClick += Tab_MouseClick!;
+
+                yield return tab;
+
+                break;
+            }
+
+            if (tabTextWidth > maxWidth)
+            {
+                tab.Text = tab.DisplayText.Substring (0, (int)maxWidth);
+                tabTextWidth = (int)maxWidth;
+            }
+            else
+            {
+                tab.Text = tab.DisplayText;
+            }
+
+            tab.Width = Math.Max (tabTextWidth + 2, 1);
+            tab.Height = Style.ShowTopLine ? 3 : 2;
+
+            // if there is not enough space for this tab
+            if (i + tabTextWidth >= bounds.Width)
+            {
+                tab.Visible = false;
+
+                break;
+            }
+
+            // there is enough space!
+            tab.Visible = true;
+            tab.MouseClick += Tab_MouseClick!;
+            tab.Border!.MouseClick += Tab_MouseClick!;
+
+            yield return tab;
+
+            prevTab = tab;
+
+            i += tabTextWidth + 1;
+        }
+
+        if (TabCanSetFocus ())
+        {
+            SelectedTab?.SetFocus ();
+        }
+    }
+
+    /// <summary>
+    ///     Returns the number of rows occupied by rendering the tabs, this depends on <see cref="TabStyle.ShowTopLine"/>
+    ///     and can be 0 (e.g. if <see cref="TabStyle.TabsOnBottom"/> and you ask for <paramref name="top"/>).
+    /// </summary>
+    /// <param name="top">True to measure the space required at the top of the control, false to measure space at the bottom.</param>
+    /// .
+    /// <returns></returns>
+    private int GetTabHeight (bool top)
+    {
+        if (top && Style.TabsOnBottom)
+        {
+            return 0;
+        }
+
+        if (!top && !Style.TabsOnBottom)
+        {
+            return 0;
+        }
+
+        return Style.ShowTopLine ? 3 : 2;
+    }
+
+    internal void Tab_MouseClick (object sender, MouseEventArgs e)
+    {
+        e.Handled = _tabsBar.NewMouseEvent (e) == true;
+    }
+
+    private void UnSetCurrentTabs ()
+    {
+        if (_tabLocations is null)
+        {
+            // Ensures unset any visible tab prior to TabScrollOffset
+            for (int i = 0; i < TabScrollOffset; i++)
+            {
+                Tab tab = Tabs.ElementAt (i);
+
+                if (tab.Visible)
+                {
+                    tab.MouseClick -= Tab_MouseClick!;
+                    tab.Border!.MouseClick -= Tab_MouseClick!;
+                    tab.Visible = false;
+                }
+            }
+        }
+        else if (_tabLocations is { })
+        {
+            foreach (Tab tabToRender in _tabLocations)
+            {
+                tabToRender.MouseClick -= Tab_MouseClick!;
+                tabToRender.Border!.MouseClick -= Tab_MouseClick!;
+                tabToRender.Visible = false;
+            }
+
+            _tabLocations = null;
+        }
+    }
+
+    /// <summary>Raises the <see cref="TabClicked"/> event.</summary>
+    /// <param name="tabMouseEventArgs"></param>
+    internal virtual void OnTabClicked (TabMouseEventArgs tabMouseEventArgs) { TabClicked?.Invoke (this, tabMouseEventArgs); }
+
+
+}

+ 0 - 400
UICatalog/Scenarios/ASCIICustomButton.cs

@@ -1,400 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Text;
-using JetBrains.Annotations;
-using Terminal.Gui;
-
-namespace UICatalog.Scenarios;
-
-[ScenarioMetadata ("ASCIICustomButtonTest", "ASCIICustomButton sample")]
-[ScenarioCategory ("Controls")]
-public class ASCIICustomButtonTest : Scenario
-{
-    private static bool _smallerWindow;
-    private MenuItem _miSmallerWindow;
-    private ScrollViewTestWindow _scrollViewTestWindow;
-
-    public override void Main ()
-    {
-        _smallerWindow = false;
-
-        Application.Init ();
-        Toplevel top = new ();
-
-        var menu = new MenuBar
-        {
-            Menus =
-            [
-                new MenuBarItem (
-                                 "_Window Size",
-                                 new []
-                                 {
-                                     _miSmallerWindow =
-                                         new MenuItem (
-                                                       "Smaller Window",
-                                                       "",
-                                                       ChangeWindowSize
-                                                      )
-                                         {
-                                             CheckType = MenuItemCheckStyle
-                                                 .Checked
-                                         },
-                                     null,
-                                     new MenuItem (
-                                                   "Quit",
-                                                   "",
-                                                   () => Application.RequestStop (),
-                                                   null,
-                                                   null,
-                                                   (KeyCode)Application.QuitKey
-                                                  )
-                                 }
-                                )
-            ]
-        };
-
-        _scrollViewTestWindow = new ScrollViewTestWindow { Y = Pos.Bottom (menu) };
-
-        top.Add (menu, _scrollViewTestWindow);
-        Application.Run (top);
-        top.Dispose ();
-
-        Application.Shutdown ();
-
-        return;
-
-        void ChangeWindowSize ()
-        {
-            _smallerWindow = (bool)(_miSmallerWindow.Checked = !_miSmallerWindow.Checked);
-            top.Remove (_scrollViewTestWindow);
-            _scrollViewTestWindow.Dispose ();
-
-            _scrollViewTestWindow = new ScrollViewTestWindow ();
-            top.Add (_scrollViewTestWindow);
-        }
-    }
-
-    public class ASCIICustomButton : Button
-    {
-        private FrameView _border;
-        private Label _fill;
-        public string Description => $"Description of: {Id}";
-
-        public void CustomInitialize ()
-        {
-            _border = new FrameView { Width = Width, Height = Height };
-
-            var fillText = new StringBuilder ();
-
-            for (var i = 0; i < Viewport.Height; i++)
-            {
-                if (i > 0)
-                {
-                    fillText.AppendLine ("");
-                }
-
-                for (var j = 0; j < Viewport.Width; j++)
-                {
-                    fillText.Append ("█");
-                }
-            }
-
-            _fill = new Label { Visible = false, CanFocus = false, Text = fillText.ToString () };
-
-            var title = new Label { X = Pos.Center (), Y = Pos.Center (), Text = Text };
-
-            _border.MouseClick += This_MouseClick;
-            _fill.MouseClick += This_MouseClick;
-            title.MouseClick += This_MouseClick;
-
-            Add (_border, _fill, title);
-        }
-
-        protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedView)
-        {
-            if (newHasFocus)
-            {
-                _border.Visible = false;
-                _fill.Visible = true;
-                PointerEnter?.Invoke (this);
-            }
-            else
-            {
-                _border.Visible = true;
-                _fill.Visible = false;
-            }
-        }
-
-        public event Action<ASCIICustomButton> PointerEnter;
-        private void This_MouseClick (object sender, MouseEventArgs obj) { NewMouseEvent (obj); }
-    }
-
-    public class ScrollViewTestWindow : Window
-    {
-        private const int BUTTON_HEIGHT = 3;
-        private const int BUTTON_WIDTH = 25;
-        private const int BUTTONS_ON_PAGE = 7;
-
-        private readonly List<Button> _buttons;
-        private readonly ScrollView _scrollView;
-        private ASCIICustomButton _selected;
-
-        public ScrollViewTestWindow ()
-        {
-            Title = $"{Application.QuitKey} to Quit - Scenario: ScrollViewTestWindow";
-
-            Label titleLabel = null;
-
-            if (_smallerWindow)
-            {
-                Width = 80;
-                Height = 25;
-
-                _scrollView = new ScrollView
-                {
-                    X = 3,
-                    Y = 1,
-                    Width = 24,
-                    Height = BUTTONS_ON_PAGE * BUTTON_HEIGHT,
-                    ShowVerticalScrollIndicator = true,
-                    ShowHorizontalScrollIndicator = false
-                };
-            }
-            else
-            {
-                Width = Dim.Fill ();
-                Height = Dim.Fill ();
-
-                titleLabel = new Label { X = 0, Y = 0, Text = "DOCUMENTS" };
-
-                _scrollView = new ScrollView
-                {
-                    X = 0,
-                    Y = 1,
-                    Width = 27,
-                    Height = BUTTONS_ON_PAGE * BUTTON_HEIGHT,
-                    ShowVerticalScrollIndicator = true,
-                    ShowHorizontalScrollIndicator = false
-                };
-            }
-
-            _scrollView.KeyBindings.Clear ();
-
-            _buttons = new List<Button> ();
-            Button prevButton = null;
-            var count = 20;
-
-            for (var j = 0; j < count; j++)
-            {
-                Pos yPos = prevButton == null ? 0 : Pos.Bottom (prevButton);
-
-                var button = new ASCIICustomButton
-                {
-                    Id = j.ToString (),
-                    Text = $"section {j}",
-                    Y = yPos,
-                    Width = BUTTON_WIDTH,
-                    Height = BUTTON_HEIGHT
-                };
-                button.Initialized += Button_Initialized;
-                button.Accepting += Button_Clicked;
-                button.PointerEnter += Button_PointerEnter;
-                button.MouseClick += Button_MouseClick;
-                button.KeyDown += Button_KeyPress;
-                _scrollView.Add (button);
-                _buttons.Add (button);
-                prevButton = button;
-            }
-
-            var closeButton = new ASCIICustomButton
-            {
-                Id = "close",
-                Text = "Close",
-                Y = Pos.Bottom (prevButton),
-                Width = BUTTON_WIDTH,
-                Height = BUTTON_HEIGHT
-            };
-            closeButton.Initialized += Button_Initialized;
-            closeButton.Accepting += Button_Clicked;
-            closeButton.PointerEnter += Button_PointerEnter;
-            closeButton.MouseClick += Button_MouseClick;
-            closeButton.KeyDown += Button_KeyPress;
-            _scrollView.Add (closeButton);
-            _buttons.Add (closeButton);
-
-            int pages = _buttons.Count / BUTTONS_ON_PAGE;
-
-            if (_buttons.Count % BUTTONS_ON_PAGE > 0)
-            {
-                pages++;
-            }
-
-            // BUGBUG: set_ContentSize is supposed to be `protected`. 
-            _scrollView.SetContentSize (new (25, pages * BUTTONS_ON_PAGE * BUTTON_HEIGHT));
-
-            if (_smallerWindow)
-            {
-                Add (_scrollView);
-            }
-            else
-            {
-                Add (titleLabel, _scrollView);
-            }
-
-            Y = 1;
-        }
-        private void Button_Initialized (object sender, EventArgs e)
-        {
-            var button = sender as ASCIICustomButton;
-            button?.CustomInitialize ();
-        }
-
-        private void Button_Clicked (object sender, EventArgs e)
-        {
-            MessageBox.Query ("Button clicked.", $"'{_selected.Text}' clicked!", "Ok");
-
-            if (_selected.Text == "Close")
-            {
-                Application.RequestStop ();
-            }
-        }
-
-        private void Button_KeyPress (object sender, Key obj)
-        {
-            switch (obj.KeyCode)
-            {
-                case KeyCode.End:
-                    _scrollView.ContentOffset = new Point (
-                                                           _scrollView.ContentOffset.X,
-                                                           -(_scrollView.GetContentSize ().Height
-                                                             - _scrollView.Frame.Height
-                                                             + (_scrollView.ShowHorizontalScrollIndicator ? 1 : 0))
-                                                          );
-                    obj.Handled = true;
-
-                    return;
-                case KeyCode.Home:
-                    _scrollView.ContentOffset = new Point (_scrollView.ContentOffset.X, 0);
-                    obj.Handled = true;
-
-                    return;
-                case KeyCode.PageDown:
-                    _scrollView.ContentOffset = new Point (
-                                                           _scrollView.ContentOffset.X,
-                                                           Math.Max (
-                                                                     _scrollView.ContentOffset.Y
-                                                                     - _scrollView.Frame.Height,
-                                                                     -(_scrollView.GetContentSize ().Height
-                                                                       - _scrollView.Frame.Height
-                                                                       + (_scrollView.ShowHorizontalScrollIndicator
-                                                                              ? 1
-                                                                              : 0))
-                                                                    )
-                                                          );
-                    obj.Handled = true;
-
-                    return;
-                case KeyCode.PageUp:
-                    _scrollView.ContentOffset = new Point (
-                                                           _scrollView.ContentOffset.X,
-                                                           Math.Min (
-                                                                     _scrollView.ContentOffset.Y
-                                                                     + _scrollView.Frame.Height,
-                                                                     0
-                                                                    )
-                                                          );
-                    obj.Handled = true;
-
-                    return;
-            }
-        }
-
-        private void Button_MouseClick (object sender, MouseEventArgs obj)
-        {
-            if (obj.Flags == MouseFlags.WheeledDown)
-            {
-                _scrollView.ContentOffset = new Point (
-                                                       _scrollView.ContentOffset.X,
-                                                       _scrollView.ContentOffset.Y - BUTTON_HEIGHT
-                                                      );
-                obj.Handled = true;
-            }
-            else if (obj.Flags == MouseFlags.WheeledUp)
-            {
-                _scrollView.ContentOffset = new Point (
-                                                       _scrollView.ContentOffset.X,
-                                                       Math.Min (_scrollView.ContentOffset.Y + BUTTON_HEIGHT, 0)
-                                                      );
-                obj.Handled = true;
-            }
-        }
-
-        private void Button_PointerEnter (ASCIICustomButton obj)
-        {
-            bool? moveDown;
-
-            if (obj.Frame.Y > _selected?.Frame.Y)
-            {
-                moveDown = true;
-            }
-            else if (obj.Frame.Y < _selected?.Frame.Y)
-            {
-                moveDown = false;
-            }
-            else
-            {
-                moveDown = null;
-            }
-
-            int offSet = _selected != null
-                             ? obj.Frame.Y - _selected.Frame.Y + -_scrollView.ContentOffset.Y % BUTTON_HEIGHT
-                             : 0;
-            _selected = obj;
-
-            if (moveDown == true && _selected.Frame.Y + _scrollView.ContentOffset.Y + BUTTON_HEIGHT >= _scrollView.Frame.Height && offSet != BUTTON_HEIGHT)
-            {
-                _scrollView.ContentOffset = new Point (
-                                                       _scrollView.ContentOffset.X,
-                                                       Math.Min (
-                                                                 _scrollView.ContentOffset.Y - BUTTON_HEIGHT,
-                                                                 -(_selected.Frame.Y
-                                                                   - _scrollView.Frame.Height
-                                                                   + BUTTON_HEIGHT)
-                                                                )
-                                                      );
-            }
-            else if (moveDown == true && _selected.Frame.Y + _scrollView.ContentOffset.Y >= _scrollView.Frame.Height)
-            {
-                _scrollView.ContentOffset = new Point (
-                                                       _scrollView.ContentOffset.X,
-                                                       _scrollView.ContentOffset.Y - BUTTON_HEIGHT
-                                                      );
-            }
-            else if (moveDown == true && _selected.Frame.Y + _scrollView.ContentOffset.Y < 0)
-            {
-                _scrollView.ContentOffset = new Point (
-                                                       _scrollView.ContentOffset.X,
-                                                       -_selected.Frame.Y
-                                                      );
-            }
-            else if (moveDown == false && _selected.Frame.Y < -_scrollView.ContentOffset.Y)
-            {
-                _scrollView.ContentOffset = new Point (
-                                                       _scrollView.ContentOffset.X,
-                                                       Math.Max (
-                                                                 _scrollView.ContentOffset.Y + BUTTON_HEIGHT,
-                                                                 _selected.Frame.Y
-                                                                )
-                                                      );
-            }
-            else if (moveDown == false && _selected.Frame.Y + _scrollView.ContentOffset.Y > _scrollView.Frame.Height)
-            {
-                _scrollView.ContentOffset = new Point (
-                                                       _scrollView.ContentOffset.X,
-                                                       -(_selected.Frame.Y - _scrollView.Frame.Height + BUTTON_HEIGHT)
-                                                      );
-            }
-        }
-    }
-}

+ 0 - 166
UICatalog/Scenarios/AdvancedClipping.cs

@@ -1,166 +0,0 @@
-using System.Text;
-using System.Timers;
-using Terminal.Gui;
-
-namespace UICatalog.Scenarios;
-
-[ScenarioMetadata ("AdvancedClipping", "AdvancedClipping Tester")]
-[ScenarioCategory ("AdvancedClipping")]
-public class AdvancedClipping : Scenario
-{
-    private int _hotkeyCount;
-
-    public override void Main ()
-    {
-        Application.Init ();
-
-        Window app = new ()
-        {
-            Title = GetQuitKeyAndName (),
-            //BorderStyle = LineStyle.None
-        };
-
-        app.DrawingContent += (s, e) =>
-                           {
-                               app!.FillRect (app!.Viewport, CM.Glyphs.Dot);
-                               e.Cancel = true;
-                           };
-
-        var arrangementEditor = new ArrangementEditor ()
-        {
-            X = Pos.AnchorEnd (),
-            Y = 0,
-            AutoSelectViewToEdit = true,
-        };
-        app.Add (arrangementEditor);
-
-        View tiledView1 = CreateTiledView (1, 0, 0);
-
-        tiledView1.Width = 30;
-
-        ProgressBar tiledProgressBar1 = new ()
-        {
-            X = 0,
-            Y = Pos.AnchorEnd (),
-            Width = Dim.Fill (),
-            Id = "tiledProgressBar",
-            BidirectionalMarquee = true,
-        };
-        tiledView1.Add (tiledProgressBar1);
-
-        View tiledView2 = CreateTiledView (2, 4, 2);
-
-        ProgressBar tiledProgressBar2 = new ()
-        {
-            X = 0,
-            Y = Pos.AnchorEnd (),
-            Width = Dim.Fill (),
-            Id = "tiledProgressBar",
-            BidirectionalMarquee = true,
-            ProgressBarStyle = ProgressBarStyle.MarqueeBlocks
-            // BorderStyle = LineStyle.Rounded
-        };
-        tiledView2.Add (tiledProgressBar2);
-
-        app.Add (tiledView1);
-        app.Add (tiledView2);
-
-        View tiledView3 = CreateTiledView (3, 8, 4);
-        app.Add (tiledView3);
-
-        // View overlappedView1 = CreateOverlappedView (1, 30, 2);
-
-        //ProgressBar progressBar = new ()
-        //{
-        //    X = Pos.AnchorEnd (),
-        //    Y = Pos.AnchorEnd (),
-        //    Width = Dim.Fill (),
-        //    Id = "progressBar",
-        //    BorderStyle = LineStyle.Rounded
-        //};
-        //overlappedView1.Add (progressBar);
-
-
-        //View overlappedView2 = CreateOverlappedView (2, 32, 4);
-        //View overlappedView3 = CreateOverlappedView (3, 34, 6);
-
-        //app.Add (overlappedView1);
-        //app.Add (overlappedView2);
-        //app.Add (overlappedView3);
-
-        Timer progressTimer = new Timer (150)
-        {
-            AutoReset = true
-        };
-
-        progressTimer.Elapsed += (s, e) =>
-                                 {
-                                     tiledProgressBar1.Pulse ();
-                                     tiledProgressBar2.Pulse ();
-                                     Application.Wakeup ();
-                                 };
-
-        progressTimer.Start ();
-        Application.Run (app);
-        progressTimer.Stop ();
-        app.Dispose ();
-        Application.Shutdown ();
-
-        return;
-    }
-
-    private View CreateOverlappedView (int id, Pos x, Pos y)
-    {
-        var overlapped = new View
-        {
-            X = x,
-            Y = y,
-            Height = Dim.Auto (minimumContentDim: 4),
-            Width = Dim.Auto (minimumContentDim: 14),
-            Title = $"Overlapped{id} _{GetNextHotKey ()}",
-            ColorScheme = Colors.ColorSchemes ["Toplevel"],
-            Id = $"Overlapped{id}",
-            ShadowStyle = ShadowStyle.Transparent,
-            BorderStyle = LineStyle.Double,
-            CanFocus = true, // Can't drag without this? BUGBUG
-            TabStop = TabBehavior.TabGroup,
-            Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped | ViewArrangement.Resizable
-        };
-        return overlapped;
-    }
-
-    private View CreateTiledView (int id, Pos x, Pos y)
-    {
-        var tiled = new View
-        {
-            X = x,
-            Y = y,
-            Height = Dim.Auto (minimumContentDim: 8),
-            Width = Dim.Auto (minimumContentDim: 15),
-            Title = $"Tiled{id} _{GetNextHotKey ()}",
-            Id = $"Tiled{id}",
-            Text = $"Tiled{id}",
-            BorderStyle = LineStyle.Single,
-            CanFocus = true, // Can't drag without this? BUGBUG
-            TabStop = TabBehavior.TabStop,
-            Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
-            ShadowStyle = ShadowStyle.Transparent,
-        };
-        //tiled.Padding.Thickness = new (1);
-        //tiled.Padding.Diagnostics =  ViewDiagnosticFlags.Thickness;
-
-        //tiled.Margin.Thickness = new (1);
-
-        FrameView fv = new ()
-        {
-            Title = "FrameView",
-            Width = 15,
-            Height = 3,
-        };
-        tiled.Add (fv);
-
-        return tiled;
-    }
-
-    private char GetNextHotKey () { return (char)('A' + _hotkeyCount++); }
-}

+ 3 - 2
UICatalog/Scenarios/AllViewsTester.cs

@@ -302,6 +302,7 @@ public class AllViewsTester : Scenario
 
         view.Id = "_curView";
         _curView = view;
+        _curView = view;
 
         _hostPane!.Add (_curView);
         _layoutEditor!.ViewToEdit = _curView;
@@ -348,12 +349,12 @@ public class AllViewsTester : Scenario
             return;
         }
 
-        if (!view.Width!.Has<DimAuto> (out _) || view.Width is null)
+        if (view.Width == Dim.Absolute(0) || view.Width is null)
         {
             view.Width = Dim.Fill ();
         }
 
-        if (!view.Height!.Has<DimAuto> (out _) || view.Height is null)
+        if (view.Height == Dim.Absolute (0) || view.Height is null)
         {
             view.Height = Dim.Fill ();
         }

+ 0 - 1252
UICatalog/Scenarios/CharacterMap.cs

@@ -1,1252 +0,0 @@
-#define OTHER_CONTROLS
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Reflection;
-using System.Text;
-using System.Text.Json;
-using System.Text.Unicode;
-using System.Threading.Tasks;
-using Terminal.Gui;
-using static Terminal.Gui.SpinnerStyle;
-
-namespace UICatalog.Scenarios;
-
-/// <summary>
-///     This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
-///     "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
-///     Illustrates how to do infinite scrolling
-/// </summary>
-[ScenarioMetadata ("Character Map", "Unicode viewer demonstrating infinite content, scrolling, and Unicode.")]
-[ScenarioCategory ("Text and Formatting")]
-[ScenarioCategory ("Drawing")]
-[ScenarioCategory ("Controls")]
-[ScenarioCategory ("Layout")]
-[ScenarioCategory ("Scrolling")]
-
-public class CharacterMap : Scenario
-{
-    public Label _errorLabel;
-    private TableView _categoryList;
-    private CharMap _charMap;
-
-    // Don't create a Window, just return the top-level view
-    public override void Main ()
-    {
-        Application.Init ();
-
-        var top = new Window
-        {
-            BorderStyle = LineStyle.None
-        };
-
-        _charMap = new ()
-        {
-            X = 0,
-            Y = 0,
-            Width = Dim.Fill (),
-            Height = Dim.Fill ()
-        };
-        top.Add (_charMap);
-
-#if OTHER_CONTROLS
-        _charMap.Y = 1;
-
-        var jumpLabel = new Label
-        {
-            X = Pos.Right (_charMap) + 1,
-            Y = Pos.Y (_charMap),
-            HotKeySpecifier = (Rune)'_',
-            Text = "_Jump To Code Point:"
-        };
-        top.Add (jumpLabel);
-
-        var jumpEdit = new TextField
-        {
-            X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3"
-        };
-        top.Add (jumpEdit);
-
-        _errorLabel = new ()
-        {
-            X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap), ColorScheme = Colors.ColorSchemes ["error"], Text = "err"
-        };
-        top.Add (_errorLabel);
-
-        jumpEdit.Accepting += JumpEditOnAccept;
-
-        _categoryList = new () { X = Pos.Right (_charMap), Y = Pos.Bottom (jumpLabel), Height = Dim.Fill () };
-        _categoryList.FullRowSelect = true;
-        _categoryList.MultiSelect = false;
-        //jumpList.Style.ShowHeaders = false;
-        //jumpList.Style.ShowHorizontalHeaderOverline = false;
-        //jumpList.Style.ShowHorizontalHeaderUnderline = false;
-        _categoryList.Style.ShowHorizontalBottomline = true;
-
-        //jumpList.Style.ShowVerticalCellLines = false;
-        //jumpList.Style.ShowVerticalHeaderLines = false;
-        _categoryList.Style.AlwaysShowHeaders = true;
-
-        var isDescending = false;
-
-        _categoryList.Table = CreateCategoryTable (0, isDescending);
-
-        // if user clicks the mouse in TableView
-        _categoryList.MouseClick += (s, e) =>
-                                    {
-                                        _categoryList.ScreenToCell (e.Position, out int? clickedCol);
-
-                                        if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked))
-                                        {
-                                            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-                                            string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
-                                            isDescending = !isDescending;
-
-                                            _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
-
-                                            table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-
-                                            _categoryList.SelectedRow = table.Data
-                                                                             .Select ((item, index) => new { item, index })
-                                                                             .FirstOrDefault (x => x.item.Category == prevSelection)
-                                                                             ?.index
-                                                                        ?? -1;
-                                        }
-                                    };
-
-        int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ());
-
-        _categoryList.Style.ColumnStyles.Add (
-                                              0,
-                                              new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
-                                             );
-        _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 });
-        _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 });
-
-        _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4;
-
-        _categoryList.SelectedCellChanged += (s, args) =>
-                                             {
-                                                 EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-                                                 _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start;
-                                             };
-
-        top.Add (_categoryList);
-
-        // TODO: Replace this with Dim.Auto when that's ready
-        _categoryList.Initialized += _categoryList_Initialized;
-
-        var menu = new MenuBar
-        {
-            Menus =
-            [
-                new (
-                     "_File",
-                     new MenuItem []
-                     {
-                         new (
-                              "_Quit",
-                              $"{Application.QuitKey}",
-                              () => Application.RequestStop ()
-                             )
-                     }
-                    ),
-                new (
-                     "_Options",
-                     new [] { CreateMenuShowWidth () }
-                    )
-            ]
-        };
-        top.Add (menu);
-#endif // OTHER_CONTROLS
-
-        _charMap.SelectedCodePoint = 0;
-        _charMap.SetFocus ();
-
-        Application.Run (top);
-        top.Dispose ();
-        Application.Shutdown ();
-
-        return;
-
-        void JumpEditOnAccept (object sender, CommandEventArgs e)
-        {
-            if (jumpEdit.Text.Length == 0)
-            {
-                return;
-            }
-
-            uint result = 0;
-
-            if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
-            {
-                try
-                {
-                    result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber);
-                }
-                catch (FormatException)
-                {
-                    _errorLabel.Text = "Invalid hex value";
-
-                    return;
-                }
-            }
-            else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
-            {
-                try
-                {
-                    result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber);
-                }
-                catch (FormatException)
-                {
-                    _errorLabel.Text = "Invalid hex value";
-
-                    return;
-                }
-            }
-            else
-            {
-                try
-                {
-                    result = uint.Parse (jumpEdit.Text, NumberStyles.Integer);
-                }
-                catch (FormatException)
-                {
-                    _errorLabel.Text = "Invalid value";
-
-                    return;
-                }
-            }
-
-            if (result > RuneExtensions.MaxUnicodeCodePoint)
-            {
-                _errorLabel.Text = "Beyond maximum codepoint";
-
-                return;
-            }
-
-            _errorLabel.Text = $"U+{result:x5}";
-
-            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-
-            _categoryList.SelectedRow = table.Data
-                                             .Select ((item, index) => new { item, index })
-                                             .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)
-                                             ?.index
-                                        ?? -1;
-            _categoryList.EnsureSelectedCellIsVisible ();
-
-            // Ensure the typed glyph is selected 
-            _charMap.SelectedCodePoint = (int)result;
-
-
-            // Cancel the event to prevent ENTER from being handled elsewhere
-            e.Cancel = true;
-        }
-    }
-
-    private void _categoryList_Initialized (object sender, EventArgs e) { _charMap.Width = Dim.Fill () - _categoryList.Width; }
-
-    private EnumerableTableSource<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
-    {
-        Func<UnicodeRange, object> orderBy;
-        var categorySort = string.Empty;
-        var startSort = string.Empty;
-        var endSort = string.Empty;
-
-        string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString ();
-
-        switch (sortByColumn)
-        {
-            case 0:
-                orderBy = r => r.Category;
-                categorySort = sortIndicator;
-
-                break;
-            case 1:
-                orderBy = r => r.Start;
-                startSort = sortIndicator;
-
-                break;
-            case 2:
-                orderBy = r => r.End;
-                endSort = sortIndicator;
-
-                break;
-            default:
-                throw new ArgumentException ("Invalid column number.");
-        }
-
-        IOrderedEnumerable<UnicodeRange> sortedRanges = descending
-                                                            ? UnicodeRange.Ranges.OrderByDescending (orderBy)
-                                                            : UnicodeRange.Ranges.OrderBy (orderBy);
-
-        return new (
-                    sortedRanges,
-                    new ()
-                    {
-                        { $"Category{categorySort}", s => s.Category },
-                        { $"Start{startSort}", s => $"{s.Start:x5}" },
-                        { $"End{endSort}", s => $"{s.End:x5}" }
-                    }
-                   );
-    }
-
-    private MenuItem CreateMenuShowWidth ()
-    {
-        var item = new MenuItem { Title = "_Show Glyph Width" };
-        item.CheckType |= MenuItemCheckStyle.Checked;
-        item.Checked = _charMap?.ShowGlyphWidths;
-        item.Action += () => { _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked); };
-
-        return item;
-    }
-
-    public override List<Key> GetDemoKeyStrokes ()
-    {
-        var keys = new List<Key> ();
-
-        for (int i = 0; i < 200; i++)
-        {
-            keys.Add (Key.CursorDown);
-        }
-
-        // Category table
-        keys.Add (Key.Tab.WithShift);
-
-        // Block elements
-        keys.Add (Key.B);
-        keys.Add (Key.L);
-
-        keys.Add (Key.Tab);
-
-        for (int i = 0; i < 200; i++)
-        {
-            keys.Add (Key.CursorLeft);
-        }
-        return keys;
-    }
-}
-
-internal class CharMap : View, IDesignable
-{
-    private const int COLUMN_WIDTH = 3;
-
-    private ContextMenu _contextMenu = new ();
-    private int _rowHeight = 1;
-    private int _selected;
-    private int _start;
-
-    public CharMap ()
-    {
-        ColorScheme = Colors.ColorSchemes ["Dialog"];
-        CanFocus = true;
-        CursorVisibility = CursorVisibility.Default;
-
-        SetContentSize (new (RowWidth, (MaxCodePoint / 16 + 2) * _rowHeight));
-
-        AddCommand (
-                    Command.ScrollUp,
-                    () =>
-                    {
-                        if (SelectedCodePoint >= 16)
-                        {
-                            SelectedCodePoint -= 16;
-                        }
-
-                        ScrollVertical (-_rowHeight);
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.ScrollDown,
-                    () =>
-                    {
-                        if (SelectedCodePoint <= MaxCodePoint - 16)
-                        {
-                            SelectedCodePoint += 16;
-                        }
-
-                        if (Cursor.Y >= Viewport.Height)
-                        {
-                            ScrollVertical (_rowHeight);
-                        }
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.ScrollLeft,
-                    () =>
-                    {
-                        if (SelectedCodePoint > 0)
-                        {
-                            SelectedCodePoint--;
-                        }
-
-                        if (Cursor.X > RowLabelWidth + 1)
-                        {
-                            ScrollHorizontal (-COLUMN_WIDTH);
-                        }
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.ScrollRight,
-                    () =>
-                    {
-                        if (SelectedCodePoint < MaxCodePoint)
-                        {
-                            SelectedCodePoint++;
-                        }
-
-                        if (Cursor.X >= Viewport.Width)
-                        {
-                            ScrollHorizontal (COLUMN_WIDTH);
-                        }
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.PageUp,
-                    () =>
-                    {
-                        int page = (Viewport.Height - 1 / _rowHeight) * 16;
-                        SelectedCodePoint -= Math.Min (page, SelectedCodePoint);
-                        Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.PageDown,
-                    () =>
-                    {
-                        int page = (Viewport.Height - 1 / _rowHeight) * 16;
-                        SelectedCodePoint += Math.Min (page, MaxCodePoint - SelectedCodePoint);
-                        Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Start,
-                    () =>
-                    {
-                        SelectedCodePoint = 0;
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.End,
-                    () =>
-                    {
-                        SelectedCodePoint = MaxCodePoint;
-                        Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Accept,
-                    () =>
-                    {
-                        ShowDetails ();
-
-                        return true;
-                    }
-                   );
-
-        KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
-        KeyBindings.Add (Key.CursorDown, Command.ScrollDown);
-        KeyBindings.Add (Key.CursorLeft, Command.ScrollLeft);
-        KeyBindings.Add (Key.CursorRight, Command.ScrollRight);
-        KeyBindings.Add (Key.PageUp, Command.PageUp);
-        KeyBindings.Add (Key.PageDown, Command.PageDown);
-        KeyBindings.Add (Key.Home, Command.Start);
-        KeyBindings.Add (Key.End, Command.End);
-
-        MouseClick += Handle_MouseClick;
-        MouseEvent += Handle_MouseEvent;
-
-        // Prototype scrollbars
-        Padding.Thickness = new (0, 0, 1, 1);
-
-        var up = new Button
-        {
-            X = Pos.AnchorEnd (1),
-            Y = 0,
-            Height = 1,
-            Width = 1,
-            NoPadding = true,
-            NoDecorations = true,
-            Title = CM.Glyphs.UpArrow.ToString (),
-            WantContinuousButtonPressed = true,
-            ShadowStyle = ShadowStyle.None,
-            CanFocus = false
-        };
-        up.Accepting += (sender, args) => { args.Cancel = ScrollVertical (-1) == true; };
-
-        var down = new Button
-        {
-            X = Pos.AnchorEnd (1),
-            Y = Pos.AnchorEnd (2),
-            Height = 1,
-            Width = 1,
-            NoPadding = true,
-            NoDecorations = true,
-            Title = CM.Glyphs.DownArrow.ToString (),
-            WantContinuousButtonPressed = true,
-            ShadowStyle = ShadowStyle.None,
-            CanFocus = false
-        };
-        down.Accepting += (sender, args) => { ScrollVertical (1); };
-
-        var left = new Button
-        {
-            X = 0,
-            Y = Pos.AnchorEnd (1),
-            Height = 1,
-            Width = 1,
-            NoPadding = true,
-            NoDecorations = true,
-            Title = CM.Glyphs.LeftArrow.ToString (),
-            WantContinuousButtonPressed = true,
-            ShadowStyle = ShadowStyle.None,
-            CanFocus = false
-        };
-        left.Accepting += (sender, args) => { ScrollHorizontal (-1); };
-
-        var right = new Button
-        {
-            X = Pos.AnchorEnd (2),
-            Y = Pos.AnchorEnd (1),
-            Height = 1,
-            Width = 1,
-            NoPadding = true,
-            NoDecorations = true,
-            Title = CM.Glyphs.RightArrow.ToString (),
-            WantContinuousButtonPressed = true,
-            ShadowStyle = ShadowStyle.None,
-            CanFocus = false
-        };
-        right.Accepting += (sender, args) => { ScrollHorizontal (1); };
-
-        Padding.Add (up, down, left, right);
-    }
-
-    private void Handle_MouseEvent (object sender, MouseEventArgs e)
-    {
-        if (e.Flags == MouseFlags.WheeledDown)
-        {
-            ScrollVertical (1);
-            e.Handled = true;
-
-            return;
-        }
-
-        if (e.Flags == MouseFlags.WheeledUp)
-        {
-            ScrollVertical (-1);
-            e.Handled = true;
-
-            return;
-        }
-
-        if (e.Flags == MouseFlags.WheeledRight)
-        {
-            ScrollHorizontal (1);
-            e.Handled = true;
-
-            return;
-        }
-
-        if (e.Flags == MouseFlags.WheeledLeft)
-        {
-            ScrollHorizontal (-1);
-            e.Handled = true;
-        }
-    }
-
-    /// <summary>Gets the coordinates of the Cursor based on the SelectedCodePoint in screen coordinates</summary>
-    public Point Cursor
-    {
-        get
-        {
-            int row = SelectedCodePoint / 16 * _rowHeight - Viewport.Y + 1;
-
-            int col = SelectedCodePoint % 16 * COLUMN_WIDTH - Viewport.X + RowLabelWidth + 1; // + 1 for padding between label and first column
-
-            return new (col, row);
-        }
-        set => throw new NotImplementedException ();
-    }
-
-    public static int MaxCodePoint = UnicodeRange.Ranges.Max (r => r.End);
-
-    /// <summary>
-    ///     Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
-    ///     characters.
-    /// </summary>
-    public int SelectedCodePoint
-    {
-        get => _selected;
-        set
-        {
-            if (_selected == value)
-            {
-                return;
-            }
-
-            _selected = value;
-
-            if (IsInitialized)
-            {
-                int row = SelectedCodePoint / 16 * _rowHeight;
-                int col = SelectedCodePoint % 16 * COLUMN_WIDTH;
-
-                if (row - Viewport.Y < 0)
-                {
-                    // Moving up.
-                    Viewport = Viewport with { Y = row };
-                }
-                else if (row - Viewport.Y >= Viewport.Height)
-                {
-                    // Moving down.
-                    Viewport = Viewport with { Y = row - Viewport.Height };
-                }
-
-                int width = Viewport.Width / COLUMN_WIDTH * COLUMN_WIDTH - RowLabelWidth;
-
-                if (col - Viewport.X < 0)
-                {
-                    // Moving left.
-                    Viewport = Viewport with { X = col };
-                }
-                else if (col - Viewport.X >= width)
-                {
-                    // Moving right.
-                    Viewport = Viewport with { X = col - width };
-                }
-            }
-
-            SetNeedsDraw ();
-            SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null));
-        }
-    }
-
-    public bool ShowGlyphWidths
-    {
-        get => _rowHeight == 2;
-        set
-        {
-            _rowHeight = value ? 2 : 1;
-            SetNeedsDraw ();
-        }
-    }
-
-    /// <summary>
-    ///     Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
-    ///     characters.
-    /// </summary>
-    public int StartCodePoint
-    {
-        get => _start;
-        set
-        {
-            _start = value;
-            SelectedCodePoint = value;
-            Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
-            SetNeedsDraw ();
-        }
-    }
-
-    private static int RowLabelWidth => $"U+{MaxCodePoint:x5}".Length + 1;
-    private static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16;
-    public event EventHandler<ListViewItemEventArgs> Hover;
-
-    protected override bool OnDrawingContent ()
-    {
-        if (Viewport.Height == 0 || Viewport.Width == 0)
-        {
-            return true;
-        }
-
-        ClearViewport ();
-
-        int cursorCol = Cursor.X + Viewport.X - RowLabelWidth - 1;
-        int cursorRow = Cursor.Y + Viewport.Y - 1;
-
-        SetAttribute (GetHotNormalColor ());
-        Move (0, 0);
-        AddStr (new (' ', RowLabelWidth + 1));
-
-        int firstColumnX = RowLabelWidth - Viewport.X;
-
-        // Header
-        for (var hexDigit = 0; hexDigit < 16; hexDigit++)
-        {
-            int x = firstColumnX + hexDigit * COLUMN_WIDTH;
-
-            if (x > RowLabelWidth - 2)
-            {
-                Move (x, 0);
-                SetAttribute (GetHotNormalColor ());
-                AddStr (" ");
-                SetAttribute (HasFocus && cursorCol + firstColumnX == x ? ColorScheme.HotFocus : GetHotNormalColor ());
-                AddStr ($"{hexDigit:x}");
-                SetAttribute (GetHotNormalColor ());
-                AddStr (" ");
-            }
-        }
-
-        // Even though the Clip is set to prevent us from drawing on the row potentially occupied by the horizontal
-        // scroll bar, we do the smart thing and not actually draw that row if not necessary.
-        for (var y = 1; y < Viewport.Height; y++)
-        {
-            // What row is this?
-            int row = (y + Viewport.Y - 1) / _rowHeight;
-
-            int val = row * 16;
-
-            if (val > MaxCodePoint)
-            {
-                break;
-            }
-
-            Move (firstColumnX + COLUMN_WIDTH, y);
-            SetAttribute (GetNormalColor ());
-
-            for (var col = 0; col < 16; col++)
-            {
-                int x = firstColumnX + COLUMN_WIDTH * col + 1;
-
-                if (x < 0 || x > Viewport.Width - 1)
-                {
-                    continue;
-                }
-
-                Move (x, y);
-
-                // If we're at the cursor position, and we don't have focus, invert the colors.
-                if (row == cursorRow && x == cursorCol && !HasFocus)
-                {
-                    SetAttribute (GetFocusColor ());
-                }
-
-                int scalar = val + col;
-                var rune = (Rune)'?';
-
-                if (Rune.IsValid (scalar))
-                {
-                    rune = new (scalar);
-                }
-
-                int width = rune.GetColumns ();
-
-                if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
-                {
-                    // Draw the rune
-                    if (width > 0)
-                    {
-                        AddRune (rune);
-                    }
-                    else
-                    {
-                        if (rune.IsCombiningMark ())
-                        {
-                            // This is a hack to work around the fact that combining marks
-                            // a) can't be rendered on their own
-                            // b) that don't normalize are not properly supported in 
-                            //    any known terminal (esp Windows/AtlasEngine). 
-                            // See Issue #2616
-                            var sb = new StringBuilder ();
-                            sb.Append ('a');
-                            sb.Append (rune);
-
-                            // Try normalizing after combining with 'a'. If it normalizes, at least 
-                            // it'll show on the 'a'. If not, just show the replacement char.
-                            string normal = sb.ToString ().Normalize (NormalizationForm.FormC);
-
-                            if (normal.Length == 1)
-                            {
-                                AddRune ((Rune)normal [0]);
-                            }
-                            else
-                            {
-                                AddRune (Rune.ReplacementChar);
-                            }
-                        }
-                    }
-                }
-                else
-                {
-                    // Draw the width of the rune
-                    SetAttribute (ColorScheme.HotNormal);
-                    AddStr ($"{width}");
-                }
-
-                // If we're at the cursor position, and we don't have focus, revert the colors to normal
-                if (row == cursorRow && x == cursorCol && !HasFocus)
-                {
-                    SetAttribute (GetNormalColor ());
-                }
-            }
-
-            // Draw row label (U+XXXX_)
-            Move (0, y);
-
-            SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? ColorScheme.HotFocus : ColorScheme.HotNormal);
-
-            if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
-            {
-                AddStr ($"U+{val / 16:x5}_ ");
-            }
-            else
-            {
-                AddStr (new (' ', RowLabelWidth));
-            }
-        }
-
-        return true;
-    }
-
-    public override Point? PositionCursor ()
-    {
-        if (HasFocus
-            && Cursor.X >= RowLabelWidth
-            && Cursor.X < Viewport.Width
-            && Cursor.Y > 0
-            && Cursor.Y < Viewport.Height)
-        {
-            Move (Cursor.X, Cursor.Y);
-        }
-        else
-        {
-            return null;
-        }
-
-        return Cursor;
-    }
-
-    public event EventHandler<ListViewItemEventArgs> SelectedCodePointChanged;
-
-    public static string ToCamelCase (string str)
-    {
-        if (string.IsNullOrEmpty (str))
-        {
-            return str;
-        }
-
-        TextInfo textInfo = new CultureInfo ("en-US", false).TextInfo;
-
-        str = textInfo.ToLower (str);
-        str = textInfo.ToTitleCase (str);
-
-        return str;
-    }
-
-    private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
-    private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
-
-    private void Handle_MouseClick (object sender, MouseEventArgs me)
-    {
-        if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
-        {
-            return;
-        }
-
-        if (me.Position.Y == 0)
-        {
-            me.Position = me.Position with { Y = Cursor.Y };
-        }
-
-        if (me.Position.X < RowLabelWidth || me.Position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1)
-        {
-            me.Position = me.Position with { X = Cursor.X };
-        }
-
-        int row = (me.Position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header
-        int col = (me.Position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH;
-
-        if (col > 15)
-        {
-            col = 15;
-        }
-
-        int val = row * 16 + col;
-
-        if (val > MaxCodePoint)
-        {
-            return;
-        }
-
-        if (me.Flags == MouseFlags.ReportMousePosition)
-        {
-            Hover?.Invoke (this, new (val, null));
-        }
-
-        if (!HasFocus && CanFocus)
-        {
-            SetFocus ();
-        }
-
-        me.Handled = true;
-
-        if (me.Flags == MouseFlags.Button1Clicked)
-        {
-            SelectedCodePoint = val;
-            return;
-        }
-
-        if (me.Flags == MouseFlags.Button1DoubleClicked)
-        {
-            SelectedCodePoint = val;
-            ShowDetails ();
-
-            return;
-        }
-
-        if (me.Flags == _contextMenu.MouseFlags)
-        {
-            SelectedCodePoint = val;
-
-            _contextMenu = new ()
-            {
-                Position = new (me.Position.X + 1, me.Position.Y + 1)
-            };
-
-            MenuBarItem menuItems = new (
-                                         new MenuItem []
-                                         {
-                                             new (
-                                                  "_Copy Glyph",
-                                                  "",
-                                                  CopyGlyph,
-                                                  null,
-                                                  null,
-                                                  (KeyCode)Key.C.WithCtrl
-                                                 ),
-                                             new (
-                                                  "Copy Code _Point",
-                                                  "",
-                                                  CopyCodePoint,
-                                                  null,
-                                                  null,
-                                                  (KeyCode)Key.C.WithCtrl
-                                                              .WithShift
-                                                 )
-                                         }
-                                        );
-            _contextMenu.Show (menuItems);
-        }
-    }
-
-    private void ShowDetails ()
-    {
-        var client = new UcdApiClient ();
-        var decResponse = string.Empty;
-        var getCodePointError = string.Empty;
-
-        var waitIndicator = new Dialog
-        {
-            Title = "Getting Code Point Information",
-            X = Pos.Center (),
-            Y = Pos.Center (),
-            Height = 7,
-            Width = 50,
-            Buttons = [new () { Text = "Cancel" }]
-        };
-
-        var errorLabel = new Label
-        {
-            Text = UcdApiClient.BaseUrl,
-            X = 0,
-            Y = 1,
-            Width = Dim.Fill (),
-            Height = Dim.Fill (1),
-            TextAlignment = Alignment.Center
-        };
-        var spinner = new SpinnerView { X = Pos.Center (), Y = Pos.Center (), Style = new Aesthetic () };
-        spinner.AutoSpin = true;
-        waitIndicator.Add (errorLabel);
-        waitIndicator.Add (spinner);
-
-        waitIndicator.Ready += async (s, a) =>
-                               {
-                                   try
-                                   {
-                                       decResponse = await client.GetCodepointDec (SelectedCodePoint);
-                                       Application.Invoke (() => waitIndicator.RequestStop ());
-                                   }
-                                   catch (HttpRequestException e)
-                                   {
-                                       getCodePointError = errorLabel.Text = e.Message;
-                                       Application.Invoke (() => waitIndicator.RequestStop ());
-                                   }
-                               };
-        Application.Run (waitIndicator);
-        waitIndicator.Dispose ();
-
-        if (!string.IsNullOrEmpty (decResponse))
-        {
-            var name = string.Empty;
-
-            using (JsonDocument document = JsonDocument.Parse (decResponse))
-            {
-                JsonElement root = document.RootElement;
-
-                // Get a property by name and output its value
-                if (root.TryGetProperty ("name", out JsonElement nameElement))
-                {
-                    name = nameElement.GetString ();
-                }
-
-                //// Navigate to a nested property and output its value
-                //if (root.TryGetProperty ("property3", out JsonElement property3Element)
-                //&& property3Element.TryGetProperty ("nestedProperty", out JsonElement nestedPropertyElement)) {
-                //	Console.WriteLine (nestedPropertyElement.GetString ());
-                //}
-                decResponse = JsonSerializer.Serialize (
-                                                        document.RootElement,
-                                                        new
-                                                            JsonSerializerOptions
-                                                        { WriteIndented = true }
-                                                       );
-            }
-
-            var title = $"{ToCamelCase (name)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
-
-            var copyGlyph = new Button { Text = "Copy _Glyph" };
-            var copyCP = new Button { Text = "Copy Code _Point" };
-            var cancel = new Button { Text = "Cancel" };
-
-            var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCP, cancel] };
-
-            copyGlyph.Accepting += (s, a) =>
-                                {
-                                    CopyGlyph ();
-                                    dlg.RequestStop ();
-                                };
-
-            copyCP.Accepting += (s, a) =>
-                             {
-                                 CopyCodePoint ();
-                                 dlg.RequestStop ();
-                             };
-            cancel.Accepting += (s, a) => dlg.RequestStop ();
-
-            var rune = (Rune)SelectedCodePoint;
-            var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
-            dlg.Add (label);
-
-            label = new () { Text = $"{rune.IsAscii}", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = ", Bmp: ", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = $"{rune.IsBmp}", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = ", CombiningMark: ", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = $"{rune.IsCombiningMark ()}", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = ", SurrogatePair: ", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = $"{rune.IsSurrogatePair ()}", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = ", Plane: ", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = $"{rune.Plane}", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = "Columns: ", X = 0, Y = Pos.Bottom (label) };
-            dlg.Add (label);
-
-            label = new () { Text = $"{rune.GetColumns ()}", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = ", Utf16SequenceLength: ", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new () { Text = $"{rune.Utf16SequenceLength}", X = Pos.Right (label), Y = Pos.Top (label) };
-            dlg.Add (label);
-
-            label = new ()
-            {
-                Text =
-                    $"Code Point Information from {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
-                X = 0,
-                Y = Pos.Bottom (label)
-            };
-            dlg.Add (label);
-
-            var json = new TextView ()
-            {
-                X = 0,
-                Y = Pos.Bottom (label),
-                Width = Dim.Fill (),
-                Height = Dim.Fill (2),
-                ReadOnly = true,
-                Text = decResponse
-            };
-
-            dlg.Add (json);
-
-            Application.Run (dlg);
-            dlg.Dispose ();
-        }
-        else
-        {
-            MessageBox.ErrorQuery (
-                                   "Code Point API",
-                                   $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
-                                   "Ok"
-                                  );
-        }
-
-        // BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug
-        Application.GrabMouse (this);
-    }
-}
-
-public class UcdApiClient
-{
-    public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
-    private static readonly HttpClient _httpClient = new ();
-
-    public async Task<string> GetChars (string chars)
-    {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
-
-    public async Task<string> GetCharsName (string chars)
-    {
-        HttpResponseMessage response =
-            await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
-
-    public async Task<string> GetCodepointDec (int dec)
-    {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
-
-    public async Task<string> GetCodepointHex (string hex)
-    {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
-}
-
-internal class UnicodeRange
-{
-    public static List<UnicodeRange> Ranges = GetRanges ();
-
-    public string Category;
-    public int End;
-    public int Start;
-
-    public UnicodeRange (int start, int end, string category)
-    {
-        Start = start;
-        End = end;
-        Category = category;
-    }
-
-    public static List<UnicodeRange> GetRanges ()
-    {
-        IEnumerable<UnicodeRange> ranges =
-            from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public)
-            let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange
-            let name = string.IsNullOrEmpty (r.Name)
-                           ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}"
-                           : r.Name
-            where name != "None" && name != "All"
-            select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name);
-
-        // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0
-        List<UnicodeRange> nonBmpRanges = new ()
-        {
-            new (
-                 0x1F130,
-                 0x1F149,
-                 "Squared Latin Capital Letters"
-                ),
-            new (
-                 0x12400,
-                 0x1240f,
-                 "Cuneiform Numbers and Punctuation"
-                ),
-            new (0x10000, 0x1007F, "Linear B Syllabary"),
-            new (0x10080, 0x100FF, "Linear B Ideograms"),
-            new (0x10100, 0x1013F, "Aegean Numbers"),
-            new (0x10300, 0x1032F, "Old Italic"),
-            new (0x10330, 0x1034F, "Gothic"),
-            new (0x10380, 0x1039F, "Ugaritic"),
-            new (0x10400, 0x1044F, "Deseret"),
-            new (0x10450, 0x1047F, "Shavian"),
-            new (0x10480, 0x104AF, "Osmanya"),
-            new (0x10800, 0x1083F, "Cypriot Syllabary"),
-            new (
-                 0x1D000,
-                 0x1D0FF,
-                 "Byzantine Musical Symbols"
-                ),
-            new (0x1D100, 0x1D1FF, "Musical Symbols"),
-            new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"),
-            new (
-                 0x1D400,
-                 0x1D7FF,
-                 "Mathematical Alphanumeric Symbols"
-                ),
-            new (0x1F600, 0x1F532, "Emojis Symbols"),
-            new (
-                 0x20000,
-                 0x2A6DF,
-                 "CJK Unified Ideographs Extension B"
-                ),
-            new (
-                 0x2F800,
-                 0x2FA1F,
-                 "CJK Compatibility Ideographs Supplement"
-                ),
-            new (0xE0000, 0xE007F, "Tags")
-        };
-
-        return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList ();
-    }
-}

+ 785 - 0
UICatalog/Scenarios/CharacterMap/CharMap.cs

@@ -0,0 +1,785 @@
+#nullable enable
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     A scrollable map of the Unicode codepoints.
+/// </summary>
+/// <remarks>
+///     See <see href="CharacterMap/README.md"/> for details.
+/// </remarks>
+public class CharMap : View, IDesignable
+{
+    private const int COLUMN_WIDTH = 3; // Width of each column of glyphs
+    private const int HEADER_HEIGHT = 1; // Height of the header
+    private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested
+
+    private ContextMenu _contextMenu = new ();
+
+    /// <summary>
+    ///     Initializes a new instance.
+    /// </summary>
+    public CharMap ()
+    {
+        base.ColorScheme = Colors.ColorSchemes ["Dialog"];
+        CanFocus = true;
+        CursorVisibility = CursorVisibility.Default;
+
+        AddCommand (
+                    Command.Up,
+                    () =>
+                    {
+                        SelectedCodePoint -= 16;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Down,
+                    () =>
+                    {
+                        SelectedCodePoint += 16;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Left,
+                    () =>
+                    {
+                        SelectedCodePoint--;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Right,
+                    () =>
+                    {
+                        SelectedCodePoint++;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.PageUp,
+                    () =>
+                    {
+                        int page = (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16;
+                        SelectedCodePoint -= page;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.PageDown,
+                    () =>
+                    {
+                        int page = (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16;
+                        SelectedCodePoint += page;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Start,
+                    () =>
+                    {
+                        SelectedCodePoint = 0;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.End,
+                    () =>
+                    {
+                        SelectedCodePoint = MAX_CODE_POINT;
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Accept,
+                    () =>
+                    {
+                        ShowDetails ();
+
+                        return true;
+                    }
+                   );
+
+        KeyBindings.Add (Key.CursorUp, Command.Up);
+        KeyBindings.Add (Key.CursorDown, Command.Down);
+        KeyBindings.Add (Key.CursorLeft, Command.Left);
+        KeyBindings.Add (Key.CursorRight, Command.Right);
+        KeyBindings.Add (Key.PageUp, Command.PageUp);
+        KeyBindings.Add (Key.PageDown, Command.PageDown);
+        KeyBindings.Add (Key.Home, Command.Start);
+        KeyBindings.Add (Key.End, Command.End);
+
+        MouseClick += Handle_MouseClick;
+
+        SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + HEADER_HEIGHT));
+
+        // Set up the horizontal scrollbar. Turn off AutoShow since we do it manually.
+        HorizontalScrollBar.AutoShow = false;
+        HorizontalScrollBar.Increment = COLUMN_WIDTH;
+
+        // This prevents scrolling past the last column
+        HorizontalScrollBar.ScrollableContentSize = GetContentSize ().Width - RowLabelWidth;
+        HorizontalScrollBar.X = RowLabelWidth;
+        HorizontalScrollBar.Y = Pos.AnchorEnd ();
+        HorizontalScrollBar.Width = Dim.Fill (1);
+
+        // We want the horizontal scrollbar to only show when needed.
+        // We can't use ScrollBar.AutoShow because we are using custom ContentSize
+        // So, we do it manually on ViewportChanged events.
+        ViewportChanged += (sender, args) =>
+                           {
+                               if (Viewport.Width < GetContentSize ().Width)
+                               {
+                                   HorizontalScrollBar.Visible = true;
+                               }
+                               else
+                               {
+                                   HorizontalScrollBar.Visible = false;
+                               }
+                           };
+
+        // Set up the vertical scrollbar. Turn off AutoShow since it's always visible.
+        VerticalScrollBar.AutoShow = false;
+        VerticalScrollBar.Visible = true; // Force always visible
+        VerticalScrollBar.X = Pos.AnchorEnd ();
+        VerticalScrollBar.Y = HEADER_HEIGHT; // Header
+    }
+
+    private void ScrollToMakeCursorVisible (Point offsetToNewCursor)
+    {
+        // Adjust vertical scrolling
+        if (offsetToNewCursor.Y < 1) // Header is at Y = 0
+        {
+            ScrollVertical (offsetToNewCursor.Y - HEADER_HEIGHT);
+        }
+        else if (offsetToNewCursor.Y >= Viewport.Height)
+        {
+            ScrollVertical (offsetToNewCursor.Y - Viewport.Height + HEADER_HEIGHT);
+        }
+
+        // Adjust horizontal scrolling
+        if (offsetToNewCursor.X < RowLabelWidth + 1)
+        {
+            ScrollHorizontal (offsetToNewCursor.X - (RowLabelWidth + 1));
+        }
+        else if (offsetToNewCursor.X >= Viewport.Width)
+        {
+            ScrollHorizontal (offsetToNewCursor.X - Viewport.Width + 1);
+        }
+    }
+
+    #region Cursor
+
+    private Point GetCursor (int codePoint)
+    {
+        // + 1 for padding between label and first column
+        int x = codePoint % 16 * COLUMN_WIDTH + RowLabelWidth + 1 - Viewport.X;
+        int y = codePoint / 16 * _rowHeight + HEADER_HEIGHT - Viewport.Y;
+
+        return new (x, y);
+    }
+
+    public override Point? PositionCursor ()
+    {
+        Point cursor = GetCursor (SelectedCodePoint);
+
+        if (HasFocus
+            && cursor.X >= RowLabelWidth
+            && cursor.X < Viewport.Width
+            && cursor.Y > 0
+            && cursor.Y < Viewport.Height)
+        {
+            Move (cursor.X, cursor.Y);
+        }
+        else
+        {
+            return null;
+        }
+
+        return cursor;
+    }
+
+    #endregion Cursor
+
+    // ReSharper disable once InconsistentNaming
+    private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End);
+    private int _selectedCodepoint; // Currently selected codepoint
+    private int _startCodepoint; // The codepoint that will be displayed at the top of the Viewport
+
+    /// <summary>
+    ///     Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point
+    ///     visible.
+    /// </summary>
+    public int SelectedCodePoint
+    {
+        get => _selectedCodepoint;
+        set
+        {
+            if (_selectedCodepoint == value)
+            {
+                return;
+            }
+
+            int newSelectedCodePoint = Math.Clamp (value, 0, MAX_CODE_POINT);
+
+            Point offsetToNewCursor = GetCursor (newSelectedCodePoint);
+
+            _selectedCodepoint = newSelectedCodePoint;
+
+            // Ensure the new cursor position is visible
+            ScrollToMakeCursorVisible (offsetToNewCursor);
+
+            SetNeedsDraw ();
+            SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint));
+        }
+    }
+
+    /// <summary>
+    ///     Raised when the selected code point changes.
+    /// </summary>
+    public event EventHandler<EventArgs<int>>? SelectedCodePointChanged;
+
+    /// <summary>
+    ///     Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
+    ///     characters.
+    /// </summary>
+    public int StartCodePoint
+    {
+        get => _startCodepoint;
+        set
+        {
+            _startCodepoint = value;
+            SelectedCodePoint = value;
+        }
+    }
+
+    /// <summary>
+    ///     Gets or sets whether the number of columns each glyph is displayed.
+    /// </summary>
+    public bool ShowGlyphWidths
+    {
+        get => _rowHeight == 2;
+        set
+        {
+            _rowHeight = value ? 2 : 1;
+            SetNeedsDraw ();
+        }
+    }
+
+    private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
+    private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
+
+    #region Drawing
+
+    private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1;
+
+    protected override bool OnDrawingContent ()
+    {
+        if (Viewport.Height == 0 || Viewport.Width == 0)
+        {
+            return true;
+        }
+
+        int cursorCol = GetCursor (SelectedCodePoint).X + Viewport.X - RowLabelWidth - 1;
+        int cursorRow = GetCursor (SelectedCodePoint).Y + Viewport.Y - 1;
+
+        SetAttribute (GetHotNormalColor ());
+        Move (0, 0);
+        AddStr (new (' ', RowLabelWidth + 1));
+
+        int firstColumnX = RowLabelWidth - Viewport.X;
+
+        // Header
+        for (var hexDigit = 0; hexDigit < 16; hexDigit++)
+        {
+            int x = firstColumnX + hexDigit * COLUMN_WIDTH;
+
+            if (x > RowLabelWidth - 2)
+            {
+                Move (x, 0);
+                SetAttribute (GetHotNormalColor ());
+                AddStr (" ");
+                SetAttribute (HasFocus && cursorCol + firstColumnX == x ? GetHotFocusColor () : GetHotNormalColor ());
+                AddStr ($"{hexDigit:x}");
+                SetAttribute (GetHotNormalColor ());
+                AddStr (" ");
+            }
+        }
+
+        // Start at 1 because Header.
+        for (var y = 1; y < Viewport.Height; y++)
+        {
+            // What row is this?
+            int row = (y + Viewport.Y - 1) / _rowHeight;
+
+            int val = row * 16;
+
+            if (val > MAX_CODE_POINT)
+            {
+                break;
+            }
+
+            Move (firstColumnX + COLUMN_WIDTH, y);
+            SetAttribute (GetNormalColor ());
+
+            for (var col = 0; col < 16; col++)
+            {
+                int x = firstColumnX + COLUMN_WIDTH * col + 1;
+
+                if (x < 0 || x > Viewport.Width - 1)
+                {
+                    continue;
+                }
+
+                Move (x, y);
+
+                // If we're at the cursor position, and we don't have focus, invert the colors.
+                if (row == cursorRow && x == cursorCol && !HasFocus)
+                {
+                    SetAttribute (GetFocusColor ());
+                }
+
+                int scalar = val + col;
+                var rune = (Rune)'?';
+
+                if (Rune.IsValid (scalar))
+                {
+                    rune = new (scalar);
+                }
+
+                int width = rune.GetColumns ();
+
+                if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
+                {
+                    // Draw the rune
+                    if (width > 0)
+                    {
+                        AddRune (rune);
+                    }
+                    else
+                    {
+                        if (rune.IsCombiningMark ())
+                        {
+                            // This is a hack to work around the fact that combining marks
+                            // a) can't be rendered on their own
+                            // b) that don't normalize are not properly supported in 
+                            //    any known terminal (esp Windows/AtlasEngine). 
+                            // See Issue #2616
+                            var sb = new StringBuilder ();
+                            sb.Append ('a');
+                            sb.Append (rune);
+
+                            // Try normalizing after combining with 'a'. If it normalizes, at least 
+                            // it'll show on the 'a'. If not, just show the replacement char.
+                            string normal = sb.ToString ().Normalize (NormalizationForm.FormC);
+
+                            if (normal.Length == 1)
+                            {
+                                AddRune ((Rune)normal [0]);
+                            }
+                            else
+                            {
+                                AddRune (Rune.ReplacementChar);
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // Draw the width of the rune
+                    SetAttribute (GetHotNormalColor ());
+                    AddStr ($"{width}");
+                }
+
+                // If we're at the cursor position, and we don't have focus, revert the colors to normal
+                if (row == cursorRow && x == cursorCol && !HasFocus)
+                {
+                    SetAttribute (GetNormalColor ());
+                }
+            }
+
+            // Draw row label (U+XXXX_)
+            Move (0, y);
+
+            SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? GetHotFocusColor () : GetHotNormalColor ());
+
+            if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
+            {
+                AddStr ($"U+{val / 16:x5}_ ");
+            }
+            else
+            {
+                AddStr (new (' ', RowLabelWidth));
+            }
+        }
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Helper to convert a string into camel case.
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
+    public static string ToCamelCase (string str)
+    {
+        if (string.IsNullOrEmpty (str))
+        {
+            return str;
+        }
+
+        TextInfo textInfo = new CultureInfo ("en-US", false).TextInfo;
+
+        str = textInfo.ToLower (str);
+        str = textInfo.ToTitleCase (str);
+
+        return str;
+    }
+
+    #endregion Drawing
+
+    #region Mouse Handling
+
+    // TODO: Use this to demonstrate using a popover to show glyph info on hover
+    public event EventHandler<ListViewItemEventArgs>? Hover;
+
+    /// <inheritdoc />
+    protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
+    {
+        if (mouseEvent.Flags == MouseFlags.WheeledDown)
+        {
+            if (Viewport.Y + Viewport.Height - HEADER_HEIGHT < GetContentSize ().Height)
+            {
+                ScrollVertical (1);
+            }
+            return mouseEvent.Handled = true;
+        }
+
+        if (mouseEvent.Flags == MouseFlags.WheeledUp)
+        {
+            ScrollVertical (-1);
+            return mouseEvent.Handled = true;
+        }
+
+        if (mouseEvent.Flags == MouseFlags.WheeledRight)
+        {
+            if (Viewport.X + Viewport.Width < GetContentSize ().Width)
+            {
+                ScrollHorizontal (1);
+            }
+            return mouseEvent.Handled = true;
+        }
+
+        if (mouseEvent.Flags == MouseFlags.WheeledLeft)
+        {
+            ScrollHorizontal (-1);
+            return mouseEvent.Handled = true;
+        }
+
+        return false;
+    }
+
+    private void Handle_MouseClick (object? sender, MouseEventArgs me)
+    {
+        if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
+        {
+            return;
+        }
+
+        if (me.Position.Y == 0)
+        {
+            me.Position = me.Position with { Y = GetCursor (SelectedCodePoint).Y };
+        }
+
+        if (me.Position.X < RowLabelWidth || me.Position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1)
+        {
+            me.Position = me.Position with { X = GetCursor (SelectedCodePoint).X };
+        }
+
+        int row = (me.Position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header
+        int col = (me.Position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH;
+
+        if (col > 15)
+        {
+            col = 15;
+        }
+
+        int val = row * 16 + col;
+
+        if (val > MAX_CODE_POINT)
+        {
+            return;
+        }
+
+        if (me.Flags == MouseFlags.ReportMousePosition)
+        {
+            Hover?.Invoke (this, new (val, null));
+        }
+
+        if (!HasFocus && CanFocus)
+        {
+            SetFocus ();
+        }
+
+        me.Handled = true;
+
+        if (me.Flags == MouseFlags.Button1Clicked)
+        {
+            SelectedCodePoint = val;
+
+            return;
+        }
+
+        if (me.Flags == MouseFlags.Button1DoubleClicked)
+        {
+            SelectedCodePoint = val;
+            ShowDetails ();
+
+            return;
+        }
+
+        if (me.Flags == _contextMenu.MouseFlags)
+        {
+            SelectedCodePoint = val;
+
+            _contextMenu = new ()
+            {
+                Position = new (me.Position.X + 1, me.Position.Y + 1)
+            };
+
+            MenuBarItem menuItems = new (
+                                         new MenuItem []
+                                         {
+                                             new (
+                                                  "_Copy Glyph",
+                                                  "",
+                                                  CopyGlyph,
+                                                  null,
+                                                  null,
+                                                  (KeyCode)Key.C.WithCtrl
+                                                 ),
+                                             new (
+                                                  "Copy Code _Point",
+                                                  "",
+                                                  CopyCodePoint,
+                                                  null,
+                                                  null,
+                                                  (KeyCode)Key.C.WithCtrl
+                                                              .WithShift
+                                                 )
+                                         }
+                                        );
+            _contextMenu.Show (menuItems);
+        }
+    }
+
+    #endregion Mouse Handling
+
+    #region Details Dialog
+
+    private void ShowDetails ()
+    {
+        UcdApiClient? client = new ();
+        var decResponse = string.Empty;
+        var getCodePointError = string.Empty;
+
+        Dialog? waitIndicator = new ()
+        {
+            Title = "Getting Code Point Information",
+            X = Pos.Center (),
+            Y = Pos.Center (),
+            Width = 40,
+            Height = 10,
+            Buttons = [new () { Text = "_Cancel" }]
+        };
+
+        var errorLabel = new Label
+        {
+            Text = UcdApiClient.BaseUrl,
+            X = 0,
+            Y = 0,
+            Width = Dim.Fill (),
+            Height = Dim.Fill (3),
+            TextAlignment = Alignment.Center
+        };
+
+        var spinner = new SpinnerView
+        {
+            X = Pos.Center (),
+            Y = Pos.Bottom (errorLabel),
+            Style = new SpinnerStyle.Aesthetic ()
+        };
+        spinner.AutoSpin = true;
+        waitIndicator.Add (errorLabel);
+        waitIndicator.Add (spinner);
+
+        waitIndicator.Ready += async (s, a) =>
+                               {
+                                   try
+                                   {
+                                       decResponse = await client.GetCodepointDec (SelectedCodePoint);
+                                       Application.Invoke (() => waitIndicator.RequestStop ());
+                                   }
+                                   catch (HttpRequestException e)
+                                   {
+                                       getCodePointError = errorLabel.Text = e.Message;
+                                       Application.Invoke (() => waitIndicator.RequestStop ());
+                                   }
+                               };
+        Application.Run (waitIndicator);
+        waitIndicator.Dispose ();
+
+        if (!string.IsNullOrEmpty (decResponse))
+        {
+            var name = string.Empty;
+
+            using (JsonDocument document = JsonDocument.Parse (decResponse))
+            {
+                JsonElement root = document.RootElement;
+
+                // Get a property by name and output its value
+                if (root.TryGetProperty ("name", out JsonElement nameElement))
+                {
+                    name = nameElement.GetString ();
+                }
+
+                //// Navigate to a nested property and output its value
+                //if (root.TryGetProperty ("property3", out JsonElement property3Element)
+                //&& property3Element.TryGetProperty ("nestedProperty", out JsonElement nestedPropertyElement)) {
+                //	Console.WriteLine (nestedPropertyElement.GetString ());
+                //}
+                decResponse = JsonSerializer.Serialize (
+                                                        document.RootElement,
+                                                        new
+                                                            JsonSerializerOptions
+                                                        { WriteIndented = true }
+                                                       );
+            }
+
+            var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
+
+            Button? copyGlyph = new () { Text = "Copy _Glyph" };
+            Button? copyCodepoint = new () { Text = "Copy Code _Point" };
+            Button? cancel = new () { Text = "Cancel" };
+
+            var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] };
+
+            copyGlyph.Accepting += (s, a) =>
+                                   {
+                                       CopyGlyph ();
+                                       dlg!.RequestStop ();
+                                   };
+
+            copyCodepoint.Accepting += (s, a) =>
+                                       {
+                                           CopyCodePoint ();
+                                           dlg!.RequestStop ();
+                                       };
+            cancel.Accepting += (s, a) => dlg!.RequestStop ();
+
+            var rune = (Rune)SelectedCodePoint;
+            var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
+            dlg.Add (label);
+
+            label = new () { Text = $"{rune.IsAscii}", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = ", Bmp: ", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = $"{rune.IsBmp}", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = ", CombiningMark: ", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = $"{rune.IsCombiningMark ()}", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = ", SurrogatePair: ", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = $"{rune.IsSurrogatePair ()}", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = ", Plane: ", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = $"{rune.Plane}", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = "Columns: ", X = 0, Y = Pos.Bottom (label) };
+            dlg.Add (label);
+
+            label = new () { Text = $"{rune.GetColumns ()}", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = ", Utf16SequenceLength: ", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new () { Text = $"{rune.Utf16SequenceLength}", X = Pos.Right (label), Y = Pos.Top (label) };
+            dlg.Add (label);
+
+            label = new ()
+            {
+                Text =
+                    $"Code Point Information from {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:",
+                X = 0,
+                Y = Pos.Bottom (label)
+            };
+            dlg.Add (label);
+
+            var json = new TextView
+            {
+                X = 0,
+                Y = Pos.Bottom (label),
+                Width = Dim.Fill (),
+                Height = Dim.Fill (2),
+                ReadOnly = true,
+                Text = decResponse
+            };
+
+            dlg.Add (json);
+
+            Application.Run (dlg);
+            dlg.Dispose ();
+        }
+        else
+        {
+            MessageBox.ErrorQuery (
+                                   "Code Point API",
+                                   $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
+                                   "_Ok"
+                                  );
+        }
+
+        // BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug
+        Application.GrabMouse (this);
+    }
+
+    #endregion Details Dialog
+}

+ 352 - 0
UICatalog/Scenarios/CharacterMap/CharacterMap.cs

@@ -0,0 +1,352 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
+///     "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
+///     Illustrates how to do infinite scrolling
+/// </summary>
+/// <remarks>
+///     See <see href="CharacterMap/README.md"/>.
+/// </remarks>
+[ScenarioMetadata ("Character Map", "Unicode viewer. Demos infinite content drawing and scrolling.")]
+[ScenarioCategory ("Text and Formatting")]
+[ScenarioCategory ("Drawing")]
+[ScenarioCategory ("Controls")]
+[ScenarioCategory ("Layout")]
+[ScenarioCategory ("Scrolling")]
+public class CharacterMap : Scenario
+{
+    private Label? _errorLabel;
+    private TableView? _categoryList;
+    private CharMap? _charMap;
+
+    // Don't create a Window, just return the top-level view
+    public override void Main ()
+    {
+        Application.Init ();
+
+        var top = new Window
+        {
+            BorderStyle = LineStyle.None
+        };
+
+        _charMap = new ()
+        {
+            X = 0,
+            Y = 1,
+            Width = Dim.Fill (Dim.Func (() => _categoryList!.Frame.Width)),
+            Height = Dim.Fill ()
+        };
+        top.Add (_charMap);
+
+        var jumpLabel = new Label
+        {
+            X = Pos.Right (_charMap) + 1,
+            Y = Pos.Y (_charMap),
+            HotKeySpecifier = (Rune)'_',
+            Text = "_Jump To:"
+        };
+        top.Add (jumpLabel);
+
+        var jumpEdit = new TextField
+        {
+            X = Pos.Right (jumpLabel) + 1,
+            Y = Pos.Y (_charMap),
+            Width = 17,
+            Caption = "e.g. 01BE3 or ✈",
+        };
+        top.Add (jumpEdit);
+
+        _charMap.SelectedCodePointChanged += (sender, args) =>
+                                             {
+                                                 if (Rune.IsValid (args.CurrentValue))
+                                                 {
+                                                     jumpEdit.Text = ((Rune)args.CurrentValue).ToString ();
+                                                 }
+                                                 else
+                                                 {
+                                                     jumpEdit.Text = $"U+{args.CurrentValue:x5}";
+                                                 }
+                                             };
+
+        _errorLabel = new ()
+        {
+            X = Pos.Right (jumpEdit) + 1,
+            Y = Pos.Y (_charMap),
+            ColorScheme = Colors.ColorSchemes ["error"],
+            Text = "err",
+            Visible = false
+
+        };
+        top.Add (_errorLabel);
+
+        jumpEdit.Accepting += JumpEditOnAccept;
+
+        _categoryList = new () { 
+            X = Pos.Right (_charMap), 
+            Y = Pos.Bottom (jumpLabel), 
+            Height = Dim.Fill (),
+        };
+        _categoryList.FullRowSelect = true;
+        _categoryList.MultiSelect = false;
+
+        _categoryList.Style.ShowVerticalCellLines = false;
+        _categoryList.Style.AlwaysShowHeaders = true;
+
+        var isDescending = false;
+
+        _categoryList.Table = CreateCategoryTable (0, isDescending);
+
+        // if user clicks the mouse in TableView
+        _categoryList.MouseClick += (s, e) =>
+                                    {
+                                        _categoryList.ScreenToCell (e.Position, out int? clickedCol);
+
+                                        if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked))
+                                        {
+                                            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
+                                            string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
+                                            isDescending = !isDescending;
+
+                                            _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
+
+                                            table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
+
+                                            _categoryList.SelectedRow = table.Data
+                                                                             .Select ((item, index) => new { item, index })
+                                                                             .FirstOrDefault (x => x.item.Category == prevSelection)
+                                                                             ?.index
+                                                                        ?? -1;
+                                        }
+                                    };
+
+        int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ());
+
+        _categoryList.Style.ColumnStyles.Add (
+                                              0,
+                                              new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
+                                             );
+        _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 });
+        _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 });
+
+        _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4;
+
+        _categoryList.SelectedCellChanged += (s, args) =>
+                                             {
+                                                 EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
+                                                 _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start;
+                                                 jumpEdit.Text = $"U+{_charMap.SelectedCodePoint:x5}";
+                                             };
+
+        top.Add (_categoryList);
+
+        var menu = new MenuBar
+        {
+            Menus =
+            [
+                new (
+                     "_File",
+                     new MenuItem []
+                     {
+                         new (
+                              "_Quit",
+                              $"{Application.QuitKey}",
+                              () => Application.RequestStop ()
+                             )
+                     }
+                    ),
+                new (
+                     "_Options",
+                     new [] { CreateMenuShowWidth () }
+                    )
+            ]
+        };
+        top.Add (menu);
+
+        _charMap.SelectedCodePoint = 0;
+        _charMap.SetFocus ();
+
+        Application.Run (top);
+        top.Dispose ();
+        Application.Shutdown ();
+
+        return;
+
+        void JumpEditOnAccept (object? sender, CommandEventArgs e)
+        {
+            if (jumpEdit.Text.Length == 0)
+            {
+                return;
+            }
+
+            _errorLabel.Visible = true;
+
+            uint result = 0;
+
+            if (jumpEdit.Text.Length == 1)
+            {
+                result = (uint)jumpEdit.Text.ToRunes () [0].Value;
+            }
+            else if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
+            {
+                try
+                {
+                    result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber);
+                }
+                catch (FormatException)
+                {
+                    _errorLabel.Text = "Invalid hex value";
+
+                    return;
+                }
+            }
+            else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
+            {
+                try
+                {
+                    result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber);
+                }
+                catch (FormatException)
+                {
+                    _errorLabel.Text = "Invalid hex value";
+
+                    return;
+                }
+            }
+            else
+            {
+                try
+                {
+                    result = uint.Parse (jumpEdit.Text, NumberStyles.Integer);
+                }
+                catch (FormatException)
+                {
+                    _errorLabel.Text = "Invalid value";
+
+                    return;
+                }
+            }
+
+            if (result > RuneExtensions.MaxUnicodeCodePoint)
+            {
+                _errorLabel.Text = "Beyond maximum codepoint";
+
+                return;
+            }
+
+            _errorLabel.Visible = false;
+
+            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList!.Table;
+
+            _categoryList.SelectedRow = table.Data
+                                             .Select ((item, index) => new { item, index })
+                                             .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)
+                                             ?.index
+                                        ?? -1;
+            _categoryList.EnsureSelectedCellIsVisible ();
+
+            // Ensure the typed glyph is selected 
+            _charMap.SelectedCodePoint = (int)result;
+            _charMap.SetFocus ();
+
+            // Cancel the event to prevent ENTER from being handled elsewhere
+            e.Cancel = true;
+        }
+    }
+
+    private EnumerableTableSource<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
+    {
+        Func<UnicodeRange, object> orderBy;
+        var categorySort = string.Empty;
+        var startSort = string.Empty;
+        var endSort = string.Empty;
+
+        string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString ();
+
+        switch (sortByColumn)
+        {
+            case 0:
+                orderBy = r => r.Category;
+                categorySort = sortIndicator;
+
+                break;
+            case 1:
+                orderBy = r => r.Start;
+                startSort = sortIndicator;
+
+                break;
+            case 2:
+                orderBy = r => r.End;
+                endSort = sortIndicator;
+
+                break;
+            default:
+                throw new ArgumentException ("Invalid column number.");
+        }
+
+        IOrderedEnumerable<UnicodeRange> sortedRanges = descending
+                                                            ? UnicodeRange.Ranges.OrderByDescending (orderBy)
+                                                            : UnicodeRange.Ranges.OrderBy (orderBy);
+
+        return new (
+                    sortedRanges,
+                    new ()
+                    {
+                        { $"Category{categorySort}", s => s.Category },
+                        { $"Start{startSort}", s => $"{s.Start:x5}" },
+                        { $"End{endSort}", s => $"{s.End:x5}" }
+                    }
+                   );
+    }
+
+    private MenuItem CreateMenuShowWidth ()
+    {
+        var item = new MenuItem { Title = "_Show Glyph Width" };
+        item.CheckType |= MenuItemCheckStyle.Checked;
+        item.Checked = _charMap?.ShowGlyphWidths;
+        item.Action += () =>
+                       {
+                           if (_charMap is { })
+                           {
+                               _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked)!;
+                           }
+                       };
+
+        return item;
+    }
+
+    public override List<Key> GetDemoKeyStrokes ()
+    {
+        List<Key> keys = new ();
+
+        for (var i = 0; i < 200; i++)
+        {
+            keys.Add (Key.CursorDown);
+        }
+
+        // Category table
+        keys.Add (Key.Tab.WithShift);
+
+        // Block elements
+        keys.Add (Key.B);
+        keys.Add (Key.L);
+
+        keys.Add (Key.Tab);
+
+        for (var i = 0; i < 200; i++)
+        {
+            keys.Add (Key.CursorLeft);
+        }
+
+        return keys;
+    }
+}

+ 11 - 0
UICatalog/Scenarios/CharacterMap/README.md

@@ -0,0 +1,11 @@
+# CharacterMap Scenario Deep Dive
+
+The CharacterMap Scenario is an example of the following Terminal.Gui concepts:
+
+- **Complex and High-Performnt Drawing** - 
+- **Virtual Content Scrolling** -
+- **Advanced ScrollBar Control** -
+- **Unicode wide-glyph Rendering** -
+- **Advanced Layout** -
+- **Cursor Management** -
+- **Context Menu** - 

+ 48 - 0
UICatalog/Scenarios/CharacterMap/UcdApiClient.cs

@@ -0,0 +1,48 @@
+#nullable enable
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     A helper class for accessing the ucdapi.org API.
+/// </summary>
+public class UcdApiClient
+{
+    public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
+    private static readonly HttpClient _httpClient = new ();
+
+    public async Task<string> GetChars (string chars)
+    {
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+
+    public async Task<string> GetCharsName (string chars)
+    {
+        HttpResponseMessage response =
+            await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+
+    public async Task<string> GetCodepointDec (int dec)
+    {
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+
+    public async Task<string> GetCodepointHex (string hex)
+    {
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+}

+ 101 - 0
UICatalog/Scenarios/CharacterMap/UnicodeRange.cs

@@ -0,0 +1,101 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.Unicode;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     Represents all of the Uniicode ranges.from System.Text.Unicode.UnicodeRange plus
+///     the non-BMP ranges not included.
+/// </summary>
+public class UnicodeRange (int start, int end, string category)
+{
+    /// <summary>
+    ///     Gets the list of all ranges.
+    /// </summary>
+    public static List<UnicodeRange> Ranges => GetRanges ();
+
+    /// <summary>
+    ///     The category.
+    /// </summary>
+    public string Category { get; set; } = category;
+
+    /// <summary>
+    ///     Te codepoint at the start of the range.
+    /// </summary>
+    public int Start { get; set; } = start;
+
+    /// <summary>
+    ///     The codepoint at the end of the range.
+    /// </summary>
+    public int End { get; set; } = end;
+
+    /// <summary>
+    ///     Gets the list of all ranges..
+    /// </summary>
+    /// <returns></returns>
+    public static List<UnicodeRange> GetRanges ()
+    {
+        IEnumerable<UnicodeRange> ranges =
+            from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public)
+            let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange
+            let name = string.IsNullOrEmpty (r.Name)
+                           ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}"
+                           : r.Name
+            where name != "None" && name != "All"
+            select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name);
+
+        // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0
+        List<UnicodeRange> nonBmpRanges = new ()
+        {
+            new (
+                 0x1F130,
+                 0x1F149,
+                 "Squared Latin Capital Letters"
+                ),
+            new (
+                 0x12400,
+                 0x1240f,
+                 "Cuneiform Numbers and Punctuation"
+                ),
+            new (0x10000, 0x1007F, "Linear B Syllabary"),
+            new (0x10080, 0x100FF, "Linear B Ideograms"),
+            new (0x10100, 0x1013F, "Aegean Numbers"),
+            new (0x10300, 0x1032F, "Old Italic"),
+            new (0x10330, 0x1034F, "Gothic"),
+            new (0x10380, 0x1039F, "Ugaritic"),
+            new (0x10400, 0x1044F, "Deseret"),
+            new (0x10450, 0x1047F, "Shavian"),
+            new (0x10480, 0x104AF, "Osmanya"),
+            new (0x10800, 0x1083F, "Cypriot Syllabary"),
+            new (
+                 0x1D000,
+                 0x1D0FF,
+                 "Byzantine Musical Symbols"
+                ),
+            new (0x1D100, 0x1D1FF, "Musical Symbols"),
+            new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"),
+            new (
+                 0x1D400,
+                 0x1D7FF,
+                 "Mathematical Alphanumeric Symbols"
+                ),
+            new (0x1F600, 0x1F532, "Emojis Symbols"),
+            new (
+                 0x20000,
+                 0x2A6DF,
+                 "CJK Unified Ideographs Extension B"
+                ),
+            new (
+                 0x2F800,
+                 0x2FA1F,
+                 "CJK Compatibility Ideographs Supplement"
+                ),
+            new (0xE0000, 0xE007F, "Tags")
+        };
+
+        return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList ();
+    }
+}

+ 129 - 73
UICatalog/Scenarios/Clipping.cs

@@ -1,113 +1,169 @@
-using System.Collections.Generic;
+using System.Text;
+using System.Timers;
 using Terminal.Gui;
 
 namespace UICatalog.Scenarios;
 
-[ScenarioMetadata ("Clipping", "Used to test that things clip correctly")]
-[ScenarioCategory ("Tests")]
-[ScenarioCategory ("Drawing")]
+[ScenarioMetadata ("Clipping", "Demonstrates non-rectangular clip region support.")]
 [ScenarioCategory ("Scrolling")]
+[ScenarioCategory ("Layout")]
+[ScenarioCategory ("Arrangement")]
+[ScenarioCategory ("Tests")]
 public class Clipping : Scenario
 {
+    private int _hotkeyCount;
+
     public override void Main ()
     {
         Application.Init ();
-        var win = new Window { Title = GetQuitKeyAndName () };
 
-        var label = new Label
+        Window app = new ()
         {
-            X = 0, Y = 0, Text = "ScrollView (new Rectangle (3, 3, 50, 20)) with a 200, 100 GetContentSize ()..."
+            Title = GetQuitKeyAndName (),
+            //BorderStyle = LineStyle.None
         };
-        win.Add (label);
 
-        var scrollView = new ScrollView { X = 3, Y = 3, Width = 50, Height = 20 };
-        scrollView.ColorScheme = Colors.ColorSchemes ["Menu"];
-        // BUGBUG: set_ContentSize is supposed to be `protected`. 
-        scrollView.SetContentSize (new (200, 100));
+        app.DrawingContent += (s, e) =>
+                           {
+                               app!.FillRect (app!.Viewport, CM.Glyphs.Dot);
+                               e.Cancel = true;
+                           };
 
-        //ContentOffset = Point.Empty,
-        scrollView.AutoHideScrollBars = true;
-        scrollView.ShowVerticalScrollIndicator = true;
-        scrollView.ShowHorizontalScrollIndicator = true;
-
-        var embedded1 = new View
+        var arrangementEditor = new ArrangementEditor ()
         {
-            Title = "1",
-            X = 3,
-            Y = 3,
-            Width = Dim.Fill (3),
-            Height = Dim.Fill (3),
-            ColorScheme = Colors.ColorSchemes ["Dialog"],
-            Id = "1",
-            BorderStyle = LineStyle.Rounded,
-            Arrangement = ViewArrangement.Movable
+            X = Pos.AnchorEnd (),
+            Y = 0,
+            AutoSelectViewToEdit = true,
         };
+        app.Add (arrangementEditor);
+
+        View tiledView1 = CreateTiledView (1, 0, 0);
+
+        tiledView1.Width = 30;
 
-        var embedded2 = new View
+        ProgressBar tiledProgressBar1 = new ()
         {
-            Title = "2",
-            X = 3,
-            Y = 3,
-            Width = Dim.Fill (3),
-            Height = Dim.Fill (3),
-            ColorScheme = Colors.ColorSchemes ["Error"],
-            Id = "2",
-            BorderStyle = LineStyle.Rounded,
-            Arrangement = ViewArrangement.Movable
+            X = 0,
+            Y = Pos.AnchorEnd (),
+            Width = Dim.Fill (),
+            Id = "tiledProgressBar",
+            BidirectionalMarquee = true,
         };
-        embedded1.Add (embedded2);
+        tiledView1.Add (tiledProgressBar1);
 
-        var embedded3 = new View
+        View tiledView2 = CreateTiledView (2, 4, 2);
+
+        ProgressBar tiledProgressBar2 = new ()
         {
-            Title = "3",
-            X = 3,
-            Y = 3,
-            Width = Dim.Fill (3),
-            Height = Dim.Fill (3),
-            ColorScheme = Colors.ColorSchemes ["TopLevel"],
-            Id = "3",
-            BorderStyle = LineStyle.Rounded,
-            Arrangement = ViewArrangement.Movable
+            X = 0,
+            Y = Pos.AnchorEnd (),
+            Width = Dim.Fill (),
+            Id = "tiledProgressBar",
+            BidirectionalMarquee = true,
+            ProgressBarStyle = ProgressBarStyle.MarqueeBlocks
+            // BorderStyle = LineStyle.Rounded
         };
+        tiledView2.Add (tiledProgressBar2);
+
+        app.Add (tiledView1);
+        app.Add (tiledView2);
+
+        View tiledView3 = CreateTiledView (3, 8, 4);
+        app.Add (tiledView3);
 
-        var testButton = new Button { X = 2, Y = 2, Text = "click me" };
-        testButton.Accepting += (s, e) => { MessageBox.Query (10, 5, "Test", "test message", "Ok"); };
-        embedded3.Add (testButton);
-        embedded2.Add (embedded3);
+        // View overlappedView1 = CreateOverlappedView (1, 30, 2);
 
-        scrollView.Add (embedded1);
+        //ProgressBar progressBar = new ()
+        //{
+        //    X = Pos.AnchorEnd (),
+        //    Y = Pos.AnchorEnd (),
+        //    Width = Dim.Fill (),
+        //    Id = "progressBar",
+        //    BorderStyle = LineStyle.Rounded
+        //};
+        //overlappedView1.Add (progressBar);
 
-        win.Add (scrollView);
 
-        Application.Run (win);
-        win.Dispose ();
+        //View overlappedView2 = CreateOverlappedView (2, 32, 4);
+        //View overlappedView3 = CreateOverlappedView (3, 34, 6);
+
+        //app.Add (overlappedView1);
+        //app.Add (overlappedView2);
+        //app.Add (overlappedView3);
+
+        Timer progressTimer = new Timer (150)
+        {
+            AutoReset = true
+        };
+
+        progressTimer.Elapsed += (s, e) =>
+                                 {
+                                     tiledProgressBar1.Pulse ();
+                                     tiledProgressBar2.Pulse ();
+                                     Application.Wakeup ();
+                                 };
+
+        progressTimer.Start ();
+        Application.Run (app);
+        progressTimer.Stop ();
+        app.Dispose ();
         Application.Shutdown ();
+
+        return;
     }
 
-    public override List<Key> GetDemoKeyStrokes ()
+    private View CreateOverlappedView (int id, Pos x, Pos y)
     {
-        var keys = new List<Key> ();
-
-        for (int i = 0; i < 25; i++)
+        var overlapped = new View
         {
-            keys.Add (Key.CursorDown);
-        }
+            X = x,
+            Y = y,
+            Height = Dim.Auto (minimumContentDim: 4),
+            Width = Dim.Auto (minimumContentDim: 14),
+            Title = $"Overlapped{id} _{GetNextHotKey ()}",
+            ColorScheme = Colors.ColorSchemes ["Toplevel"],
+            Id = $"Overlapped{id}",
+            ShadowStyle = ShadowStyle.Transparent,
+            BorderStyle = LineStyle.Double,
+            CanFocus = true, // Can't drag without this? BUGBUG
+            TabStop = TabBehavior.TabGroup,
+            Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped | ViewArrangement.Resizable
+        };
+        return overlapped;
+    }
 
-        for (int i = 0; i < 25; i++)
+    private View CreateTiledView (int id, Pos x, Pos y)
+    {
+        var tiled = new View
         {
-            keys.Add (Key.CursorRight);
-        }
+            X = x,
+            Y = y,
+            Height = Dim.Auto (minimumContentDim: 8),
+            Width = Dim.Auto (minimumContentDim: 15),
+            Title = $"Tiled{id} _{GetNextHotKey ()}",
+            Id = $"Tiled{id}",
+            Text = $"Tiled{id}",
+            BorderStyle = LineStyle.Single,
+            CanFocus = true, // Can't drag without this? BUGBUG
+            TabStop = TabBehavior.TabStop,
+            Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
+            ShadowStyle = ShadowStyle.Transparent,
+        };
+        //tiled.Padding.Thickness = new (1);
+        //tiled.Padding.Diagnostics =  ViewDiagnosticFlags.Thickness;
 
-        for (int i = 0; i < 25; i++)
-        {
-            keys.Add (Key.CursorLeft);
-        }
+        //tiled.Margin.Thickness = new (1);
 
-        for (int i = 0; i < 25; i++)
+        FrameView fv = new ()
         {
-            keys.Add (Key.CursorUp);
-        }
+            Title = "FrameView",
+            Width = 15,
+            Height = 3,
+        };
+        tiled.Add (fv);
 
-        return keys;
+        return tiled;
     }
+
+    private char GetNextHotKey () { return (char)('A' + _hotkeyCount++); }
 }

+ 35 - 35
UICatalog/Scenarios/CsvEditor.cs

@@ -141,7 +141,7 @@ public class CsvEditor : Scenario
         _tableView.CellActivated += EditCurrentCell;
         _tableView.KeyDown += TableViewKeyPress;
 
-        SetupScrollBar ();
+        //SetupScrollBar ();
 
         // Run - Start the application.
         Application.Run (appWindow);
@@ -614,40 +614,40 @@ public class CsvEditor : Scenario
 
     private void SetTable (DataTable dataTable) { _tableView.Table = new DataTableSource (_currentTable = dataTable); }
 
-    private void SetupScrollBar ()
-    {
-        var scrollBar = new ScrollBarView (_tableView, true);
-
-        scrollBar.ChangedPosition += (s, e) =>
-                                     {
-                                         _tableView.RowOffset = scrollBar.Position;
-
-                                         if (_tableView.RowOffset != scrollBar.Position)
-                                         {
-                                             scrollBar.Position = _tableView.RowOffset;
-                                         }
-
-                                         _tableView.SetNeedsDraw ();
-                                     };
-        /*
-        scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
-            tableView.LeftItem = scrollBar.OtherScrollBarView.Position;
-            if (tableView.LeftItem != scrollBar.OtherScrollBarView.Position) {
-                scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
-            }
-            tableView.SetNeedsDraw ();
-        };*/
-
-        _tableView.DrawingContent += (s, e) =>
-                                  {
-                                      scrollBar.Size = _tableView.Table?.Rows ?? 0;
-                                      scrollBar.Position = _tableView.RowOffset;
-
-                                      //scrollBar.OtherScrollBarView.Size = tableView.Maxlength - 1;
-                                      //scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
-                                      scrollBar.Refresh ();
-                                  };
-    }
+    //private void SetupScrollBar ()
+    //{
+    //    var scrollBar = new ScrollBarView (_tableView, true);
+
+    //    scrollBar.ChangedPosition += (s, e) =>
+    //                                 {
+    //                                     _tableView.RowOffset = scrollBar.Position;
+
+    //                                     if (_tableView.RowOffset != scrollBar.Position)
+    //                                     {
+    //                                         scrollBar.Position = _tableView.RowOffset;
+    //                                     }
+
+    //                                     _tableView.SetNeedsDraw ();
+    //                                 };
+    //    /*
+    //    scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
+    //        tableView.LeftItem = scrollBar.OtherScrollBarView.Position;
+    //        if (tableView.LeftItem != scrollBar.OtherScrollBarView.Position) {
+    //            scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
+    //        }
+    //        tableView.SetNeedsDraw ();
+    //    };*/
+
+    //    _tableView.DrawingContent += (s, e) =>
+    //                              {
+    //                                  scrollBar.Size = _tableView.Table?.Rows ?? 0;
+    //                                  scrollBar.Position = _tableView.RowOffset;
+
+    //                                  //scrollBar.OtherScrollBarView.Size = tableView.Maxlength - 1;
+    //                                  //scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
+    //                                  scrollBar.Refresh ();
+    //                              };
+    //}
 
     private void Sort (bool asc)
     {

+ 33 - 33
UICatalog/Scenarios/Editor.cs

@@ -31,7 +31,6 @@ public class Editor : Scenario
     private MenuItem _miForceMinimumPosToZero;
     private byte [] _originalText;
     private bool _saved = true;
-    private ScrollBarView _scrollBar;
     private TabView _tabView;
     private string _textToFind;
     private string _textToReplace;
@@ -263,6 +262,7 @@ public class Editor : Scenario
             AlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast
         };
 
+        _textView.VerticalScrollBar.AutoShow = false;
         _textView.UnwrappedCursorPosition += (s, e) =>
                                              {
                                                  siCursorPosition.Title = $"Ln {e.Point.Y + 1}, Col {e.Point.X + 1}";
@@ -270,43 +270,43 @@ public class Editor : Scenario
 
         _appWindow.Add (statusBar);
 
-        _scrollBar = new (_textView, true);
+        //_scrollBar = new (_textView, true);
 
-        _scrollBar.ChangedPosition += (s, e) =>
-                                      {
-                                          _textView.TopRow = _scrollBar.Position;
+        //_scrollBar.ChangedPosition += (s, e) =>
+        //                              {
+        //                                  _textView.TopRow = _scrollBar.Position;
 
-                                          if (_textView.TopRow != _scrollBar.Position)
-                                          {
-                                              _scrollBar.Position = _textView.TopRow;
-                                          }
+        //                                  if (_textView.TopRow != _scrollBar.Position)
+        //                                  {
+        //                                      _scrollBar.Position = _textView.TopRow;
+        //                                  }
 
-                                          _textView.SetNeedsDraw ();
-                                      };
+        //                                  _textView.SetNeedsDraw ();
+        //                              };
 
-        _scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
-                                                         {
-                                                             _textView.LeftColumn = _scrollBar.OtherScrollBarView.Position;
+        //_scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
+        //                                                 {
+        //                                                     _textView.LeftColumn = _scrollBar.OtherScrollBarView.Position;
 
-                                                             if (_textView.LeftColumn != _scrollBar.OtherScrollBarView.Position)
-                                                             {
-                                                                 _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn;
-                                                             }
+        //                                                     if (_textView.LeftColumn != _scrollBar.OtherScrollBarView.Position)
+        //                                                     {
+        //                                                         _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn;
+        //                                                     }
 
-                                                             _textView.SetNeedsDraw ();
-                                                         };
+        //                                                     _textView.SetNeedsDraw ();
+        //                                                 };
 
-        _textView.DrawingContent += (s, e) =>
-                                 {
-                                     _scrollBar.Size = _textView.Lines;
-                                     _scrollBar.Position = _textView.TopRow;
-
-                                     if (_scrollBar.OtherScrollBarView != null)
-                                     {
-                                         _scrollBar.OtherScrollBarView.Size = _textView.Maxlength;
-                                         _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn;
-                                     }
-                                 };
+        //_textView.DrawingContent += (s, e) =>
+        //                         {
+        //                             _scrollBar.Size = _textView.Lines;
+        //                             _scrollBar.Position = _textView.TopRow;
+
+        //                             if (_scrollBar.OtherScrollBarView != null)
+        //                             {
+        //                                 _scrollBar.OtherScrollBarView.Size = _textView.Maxlength;
+        //                                 _scrollBar.OtherScrollBarView.Position = _textView.LeftColumn;
+        //                             }
+        //                         };
 
 
         _appWindow.Closed += (s, e) => Thread.CurrentThread.CurrentUICulture = new ("en-US");
@@ -772,7 +772,7 @@ public class Editor : Scenario
         item.Title = "Keep Content Always In Viewport";
         item.CheckType |= MenuItemCheckStyle.Checked;
         item.Checked = true;
-        item.Action += () => _scrollBar.KeepContentAlwaysInViewport = (bool)(item.Checked = !item.Checked);
+        //item.Action += () => _scrollBar.KeepContentAlwaysInViewport = (bool)(item.Checked = !item.Checked);
 
         return new [] { item };
     }
@@ -818,7 +818,7 @@ public class Editor : Scenario
 
                            if (_textView.WordWrap)
                            {
-                               _scrollBar.OtherScrollBarView.ShowScrollIndicator = false;
+                               //_scrollBar.OtherScrollBarView.ShowScrollIndicator = false;
                            }
                        };
 

+ 10 - 0
UICatalog/Scenarios/Editors/DimEditor.cs

@@ -22,6 +22,15 @@ public class DimEditor : EditorBase
     private RadioGroup? _dimRadioGroup;
     private TextField? _valueEdit;
 
+    /// <inheritdoc />
+    protected override void OnViewToEditChanged ()
+    {
+        if (ViewToEdit is { })
+        {
+            ViewToEdit.SubviewsLaidOut += (_, _) => { OnUpdateSettings (); };
+        }
+    }
+
     protected override void OnUpdateSettings ()
     {
         Enabled = ViewToEdit is not Adornment;
@@ -110,6 +119,7 @@ public class DimEditor : EditorBase
         Add (_valueEdit);
 
         Add (_dimRadioGroup);
+
     }
 
     private void OnRadioGroupOnSelectedItemChanged (object? s, SelectedItemChangedArgs selected) { DimChanged (); }

+ 34 - 18
UICatalog/Scenarios/Editors/EventLog.cs

@@ -18,11 +18,18 @@ public class EventLog : ListView
     public EventLog ()
     {
         Title = "Event Log";
-        CanFocus = false;
+        CanFocus = true;
 
         X = Pos.AnchorEnd ();
         Y = 0;
-        Width = Dim.Func (() => Math.Min (SuperView!.Viewport.Width / 3, MaxLength + GetAdornmentsThickness ().Horizontal));
+        Width = Dim.Func (() =>
+                          {
+                              if (!IsInitialized)
+                              {
+                                  return 0;
+                              }
+                              return Math.Min (SuperView!.Viewport.Width / 3, MaxLength + GetAdornmentsThickness ().Horizontal);
+                          });
         Height = Dim.Fill ();
 
         ExpandButton = new ()
@@ -31,6 +38,20 @@ public class EventLog : ListView
         };
 
         Initialized += EventLog_Initialized;
+
+        HorizontalScrollBar.AutoShow = true;
+        VerticalScrollBar.AutoShow = true;
+
+        AddCommand (Command.DeleteAll,
+                   () =>
+                   {
+                       _eventSource.Clear ();
+
+                       return true;
+                   });
+
+        KeyBindings.Add (Key.Delete, Command.DeleteAll);
+
     }
     public ExpanderButton? ExpandButton { get; }
 
@@ -55,45 +76,40 @@ public class EventLog : ListView
                 _viewToLog.Initialized += (s, args) =>
                                              {
                                                  View? sender = s as View;
-                                                 _eventSource.Add ($"Initialized: {GetIdentifyingString (sender)}");
-                                                 MoveEnd ();
+                                                 Log ($"Initialized: {GetIdentifyingString (sender)}");
                                              };
 
                 _viewToLog.MouseClick += (s, args) =>
                 {
-                    View? sender = s as View;
-                    _eventSource.Add ($"MouseClick: {args}");
-                    MoveEnd ();
+                    Log ($"MouseClick: {args}");
                 };
 
                 _viewToLog.HandlingHotKey += (s, args) =>
                                         {
-                                            View? sender = s as View;
-                                            _eventSource.Add ($"HandlingHotKey: {args.Context.Command} {args.Context.Data}");
-                                            MoveEnd ();
+                                            Log ($"HandlingHotKey: {args.Context.Command} {args.Context.Data}");
                                         };
                 _viewToLog.Selecting += (s, args) =>
                                         {
-                                            View? sender = s as View;
-                                            _eventSource.Add ($"Selecting: {args.Context.Command} {args.Context.Data}");
-                                            MoveEnd ();
+                                            Log ($"Selecting: {args.Context.Command} {args.Context.Data}");
                                         };
                 _viewToLog.Accepting += (s, args) =>
                                         {
-                                            View? sender = s as View;
-                                            _eventSource.Add ($"Accepting: {args.Context.Command} {args.Context.Data}");
-                                            MoveEnd ();
+                                            Log ($"Accepting: {args.Context.Command} {args.Context.Data}");
                                         };
             }
         }
     }
 
-    private void EventLog_Initialized (object? _, EventArgs e)
+    public void Log (string text)
     {
+        _eventSource.Add (text);
+        MoveEnd ();
+    }
 
+    private void EventLog_Initialized (object? _, EventArgs e)
+    {
         Border?.Add (ExpandButton!);
         Source = new ListWrapper<string> (_eventSource);
-
     }
     private string GetIdentifyingString (View? view)
     {

+ 6 - 1
UICatalog/Scenarios/HexEditor.cs

@@ -44,6 +44,8 @@ public class HexEditor : Scenario
         _hexView.Arrangement = ViewArrangement.Resizable;
         _hexView.Edited += _hexView_Edited;
         _hexView.PositionChanged += _hexView_PositionChanged;
+        _hexView.VerticalScrollBar.AutoShow = false;
+
         app.Add (_hexView);
 
         var menu = new MenuBar
@@ -146,6 +148,9 @@ public class HexEditor : Scenario
         };
         app.Add (_statusBar);
 
+        _hexView.VerticalScrollBar.AutoShow = true;
+        _hexView.HorizontalScrollBar.AutoShow = true;
+
         _hexView.Source = LoadFile ();
 
         Application.Run (app);
@@ -250,7 +255,7 @@ public class HexEditor : Scenario
         {
             _fileName = d.FilePaths [0];
             _hexView.Source = LoadFile ();
-            _hexView.DisplayStart = 0;
+            //_hexView.DisplayStart = 0;
         }
 
         d.Dispose ();

+ 36 - 36
UICatalog/Scenarios/ListColumns.cs

@@ -236,7 +236,7 @@ public class ListColumns : Scenario
         _listColView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{_listColView.SelectedRow},{_listColView.SelectedColumn}"; };
         _listColView.KeyDown += TableViewKeyPress;
 
-        SetupScrollBar ();
+        //SetupScrollBar ();
 
         _alternatingColorScheme = new ()
         {
@@ -324,41 +324,41 @@ public class ListColumns : Scenario
         }
     }
 
-    private void SetupScrollBar ()
-    {
-        var scrollBar = new ScrollBarView (_listColView, true); // (listColView, true, true);
-
-        scrollBar.ChangedPosition += (s, e) =>
-                                     {
-                                         _listColView.RowOffset = scrollBar.Position;
-
-                                         if (_listColView.RowOffset != scrollBar.Position)
-                                         {
-                                             scrollBar.Position = _listColView.RowOffset;
-                                         }
-
-                                         _listColView.SetNeedsDraw ();
-                                     };
-        /*
-        scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
-            listColView.ColumnOffset = scrollBar.OtherScrollBarView.Position;
-            if (listColView.ColumnOffset != scrollBar.OtherScrollBarView.Position) {
-                scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset;
-            }
-            listColView.SetNeedsDraw ();
-        };
-        */
-
-        _listColView.DrawingContent += (s, e) =>
-                                    {
-                                        scrollBar.Size = _listColView.Table?.Rows ?? 0;
-                                        scrollBar.Position = _listColView.RowOffset;
-
-                                        //scrollBar.OtherScrollBarView.Size = listColView.Table?.Columns - 1 ?? 0;
-                                        //scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset;
-                                        scrollBar.Refresh ();
-                                    };
-    }
+    //private void SetupScrollBar ()
+    //{
+    //    var scrollBar = new ScrollBarView (_listColView, true); // (listColView, true, true);
+
+    //    scrollBar.ChangedPosition += (s, e) =>
+    //                                 {
+    //                                     _listColView.RowOffset = scrollBar.Position;
+
+    //                                     if (_listColView.RowOffset != scrollBar.Position)
+    //                                     {
+    //                                         scrollBar.Position = _listColView.RowOffset;
+    //                                     }
+
+    //                                     _listColView.SetNeedsDraw ();
+    //                                 };
+    //    /*
+    //    scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
+    //        listColView.ColumnOffset = scrollBar.OtherScrollBarView.Position;
+    //        if (listColView.ColumnOffset != scrollBar.OtherScrollBarView.Position) {
+    //            scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset;
+    //        }
+    //        listColView.SetNeedsDraw ();
+    //    };
+    //    */
+
+    //    _listColView.DrawingContent += (s, e) =>
+    //                                {
+    //                                    scrollBar.Size = _listColView.Table?.Rows ?? 0;
+    //                                    scrollBar.Position = _listColView.RowOffset;
+
+    //                                    //scrollBar.OtherScrollBarView.Size = listColView.Table?.Columns - 1 ?? 0;
+    //                                    //scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset;
+    //                                    scrollBar.Refresh ();
+    //                                };
+    //}
 
     private void TableViewKeyPress (object sender, Key e)
     {

+ 55 - 72
UICatalog/Scenarios/ListViewWithSelection.cs

@@ -10,19 +10,20 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios;
 
-[ScenarioMetadata ("List View With Selection", "ListView with columns and selection")]
+[ScenarioMetadata ("List View With Selection", "ListView with custom rendering, columns, and selection")]
 [ScenarioCategory ("Controls")]
 [ScenarioCategory ("ListView")]
+[ScenarioCategory ("Scrolling")]
 public class ListViewWithSelection : Scenario
 {
-    private CheckBox _allowMarkingCB;
-    private CheckBox _allowMultipleCB;
-    private CheckBox _customRenderCB;
+    private CheckBox _allowMarkingCb;
+    private CheckBox _allowMultipleCb;
+    private CheckBox _customRenderCb;
+    private CheckBox _keep;
     private ListView _listView;
     private ObservableCollection<Scenario> _scenarios;
     private Window _appWindow;
 
-
     private ObservableCollection<string> _eventList = new ();
     private ListView _eventListView;
 
@@ -38,90 +39,56 @@ public class ListViewWithSelection : Scenario
 
         _scenarios = GetScenarios ();
 
-        _customRenderCB = new CheckBox { X = 0, Y = 0, Text = "Use custom _rendering" };
-        _appWindow.Add (_customRenderCB);
-        _customRenderCB.CheckedStateChanging += _customRenderCB_Toggle;
+        _customRenderCb = new CheckBox { X = 0, Y = 0, Text = "Custom _Rendering" };
+        _appWindow.Add (_customRenderCb);
+        _customRenderCb.CheckedStateChanging += CustomRenderCB_Toggle;
+
+        _allowMarkingCb = new CheckBox
+        {
+            X = Pos.Right (_customRenderCb) + 1,
+            Y = 0,
+            Text = "Allows_Marking",
+            AllowCheckStateNone = false
+        };
+        _appWindow.Add (_allowMarkingCb);
+        _allowMarkingCb.CheckedStateChanging += AllowsMarkingCB_Toggle;
 
-        _allowMarkingCB = new CheckBox
+        _allowMultipleCb = new CheckBox
         {
-            X = Pos.Right (_customRenderCB) + 1, Y = 0, Text = "Allow _Marking", AllowCheckStateNone = false
+            X = Pos.Right (_allowMarkingCb) + 1,
+            Y = 0,
+            Enabled = _allowMarkingCb.CheckedState == CheckState.Checked,
+            Text = "AllowsMulti_Select"
         };
-        _appWindow.Add (_allowMarkingCB);
-        _allowMarkingCB.CheckedStateChanging += AllowMarkingCB_Toggle;
+        _appWindow.Add (_allowMultipleCb);
+        _allowMultipleCb.CheckedStateChanging += AllowsMultipleSelectionCB_Toggle;
 
-        _allowMultipleCB = new CheckBox
+        _keep = new CheckBox
         {
-            X = Pos.Right (_allowMarkingCB) + 1,
+            X = Pos.Right (_allowMultipleCb) + 1,
             Y = 0,
-            Visible = _allowMarkingCB.CheckedState == CheckState.Checked,
-            Text = "Allow Multi-_Select"
+            Text = "Allow_YGreaterThanContentHeight"
         };
-        _appWindow.Add (_allowMultipleCB);
-        _allowMultipleCB.CheckedStateChanging += AllowMultipleCB_Toggle;
+        _appWindow.Add (_keep);
+        _keep.CheckedStateChanging += AllowYGreaterThanContentHeightCB_Toggle;
 
         _listView = new ListView
         {
             Title = "_ListView",
             X = 0,
-            Y = Pos.Bottom(_allowMarkingCB),
+            Y = Pos.Bottom (_allowMarkingCb),
             Width = Dim.Func (() => _listView?.MaxLength ?? 10),
             Height = Dim.Fill (),
             AllowsMarking = false,
-            AllowsMultipleSelection = false
+            AllowsMultipleSelection = false,
+            BorderStyle = LineStyle.Dotted,
+            Arrangement = ViewArrangement.Resizable
         };
-        //_listView.Border.Thickness = new Thickness (0, 1, 0, 0);
         _listView.RowRender += ListView_RowRender;
         _appWindow.Add (_listView);
 
-        var scrollBar = new ScrollBarView (_listView, true);
-
-        scrollBar.ChangedPosition += (s, e) =>
-        {
-            _listView.TopItem = scrollBar.Position;
-
-            if (_listView.TopItem != scrollBar.Position)
-            {
-                scrollBar.Position = _listView.TopItem;
-            }
-
-            _listView.SetNeedsDraw ();
-        };
-
-        scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
-        {
-            _listView.LeftItem = scrollBar.OtherScrollBarView.Position;
-
-            if (_listView.LeftItem != scrollBar.OtherScrollBarView.Position)
-            {
-                scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
-            }
-
-            _listView.SetNeedsDraw ();
-        };
-
-        _listView.DrawingContent += (s, e) =>
-        {
-            scrollBar.Size = _listView.Source.Count;
-            scrollBar.Position = _listView.TopItem;
-            scrollBar.OtherScrollBarView.Size = _listView.MaxLength;
-            scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
-            scrollBar.Refresh ();
-        };
-
         _listView.SetSource (_scenarios);
 
-        var k = "_Keep Content Always In Viewport";
-
-        var keepCheckBox = new CheckBox
-        {
-            X = Pos.Right(_allowMultipleCB) + 1,
-            Y = 0, 
-            Text = k, 
-            CheckedState = scrollBar.AutoHideScrollBars ? CheckState.Checked : CheckState.UnChecked
-        };
-        keepCheckBox.CheckedStateChanging += (s, e) => scrollBar.KeepContentAlwaysInViewport = e.NewValue == CheckState.Checked;
-        _appWindow.Add (keepCheckBox);
-
         _eventList = new ();
 
         _eventListView = new ListView
@@ -140,6 +107,8 @@ public class ListViewWithSelection : Scenario
         _listView.CollectionChanged += (s, a) => LogEvent (s as View, a, "CollectionChanged");
         _listView.Accepting += (s, a) => LogEvent (s as View, a, "Accept");
         _listView.Selecting += (s, a) => LogEvent (s as View, a, "Select");
+        _listView.VerticalScrollBar.AutoShow = true;
+        _listView.HorizontalScrollBar.AutoShow = true;
 
         bool? LogEvent (View sender, EventArgs args, string message)
         {
@@ -155,7 +124,7 @@ public class ListViewWithSelection : Scenario
         Application.Shutdown ();
     }
 
-    private void _customRenderCB_Toggle (object sender, CancelEventArgs<CheckState> stateEventArgs)
+    private void CustomRenderCB_Toggle (object sender, CancelEventArgs<CheckState> stateEventArgs)
     {
         if (stateEventArgs.CurrentValue == CheckState.Checked)
         {
@@ -169,19 +138,33 @@ public class ListViewWithSelection : Scenario
         _appWindow.SetNeedsDraw ();
     }
 
-    private void AllowMarkingCB_Toggle (object sender, [NotNull] CancelEventArgs<CheckState> stateEventArgs)
+    private void AllowsMarkingCB_Toggle (object sender, [NotNull] CancelEventArgs<CheckState> stateEventArgs)
     {
         _listView.AllowsMarking = stateEventArgs.NewValue == CheckState.Checked;
-        _allowMultipleCB.Visible = _listView.AllowsMarking;
+        _allowMultipleCb.Enabled = _listView.AllowsMarking;
         _appWindow.SetNeedsDraw ();
     }
 
-    private void AllowMultipleCB_Toggle (object sender, [NotNull] CancelEventArgs<CheckState> stateEventArgs)
+    private void AllowsMultipleSelectionCB_Toggle (object sender, [NotNull] CancelEventArgs<CheckState> stateEventArgs)
     {
         _listView.AllowsMultipleSelection = stateEventArgs.NewValue == CheckState.Checked;
         _appWindow.SetNeedsDraw ();
     }
 
+
+    private void AllowYGreaterThanContentHeightCB_Toggle (object sender, [NotNull] CancelEventArgs<CheckState> stateEventArgs)
+    {
+        if (stateEventArgs.NewValue == CheckState.Checked)
+        {
+            _listView.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight;
+        }
+        else
+        {
+            _listView.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight;
+        }
+        _appWindow.SetNeedsDraw ();
+    }
+
     private void ListView_RowRender (object sender, ListViewRowEventArgs obj)
     {
         if (obj.Row == _listView.SelectedItem)

+ 54 - 54
UICatalog/Scenarios/ListsAndCombos.cs

@@ -54,40 +54,40 @@ public class ListsAndCombos : Scenario
         listview.SelectedItemChanged += (s, e) => lbListView.Text = items [listview.SelectedItem];
         win.Add (lbListView, listview);
 
-        var scrollBar = new ScrollBarView (listview, true);
+        //var scrollBar = new ScrollBarView (listview, true);
 
-        scrollBar.ChangedPosition += (s, e) =>
-                                     {
-                                         listview.TopItem = scrollBar.Position;
+        //scrollBar.ChangedPosition += (s, e) =>
+        //                             {
+        //                                 listview.TopItem = scrollBar.Position;
 
-                                         if (listview.TopItem != scrollBar.Position)
-                                         {
-                                             scrollBar.Position = listview.TopItem;
-                                         }
+        //                                 if (listview.TopItem != scrollBar.Position)
+        //                                 {
+        //                                     scrollBar.Position = listview.TopItem;
+        //                                 }
 
-                                         listview.SetNeedsDraw ();
-                                     };
+        //                                 listview.SetNeedsDraw ();
+        //                             };
 
-        scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
-                                                        {
-                                                            listview.LeftItem = scrollBar.OtherScrollBarView.Position;
+        //scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
+        //                                                {
+        //                                                    listview.LeftItem = scrollBar.OtherScrollBarView.Position;
 
-                                                            if (listview.LeftItem != scrollBar.OtherScrollBarView.Position)
-                                                            {
-                                                                scrollBar.OtherScrollBarView.Position = listview.LeftItem;
-                                                            }
+        //                                                    if (listview.LeftItem != scrollBar.OtherScrollBarView.Position)
+        //                                                    {
+        //                                                        scrollBar.OtherScrollBarView.Position = listview.LeftItem;
+        //                                                    }
 
-                                                            listview.SetNeedsDraw ();
-                                                        };
+        //                                                    listview.SetNeedsDraw ();
+        //                                                };
 
-        listview.DrawingContent += (s, e) =>
-                                {
-                                    scrollBar.Size = listview.Source.Count - 1;
-                                    scrollBar.Position = listview.TopItem;
-                                    scrollBar.OtherScrollBarView.Size = listview.MaxLength - 1;
-                                    scrollBar.OtherScrollBarView.Position = listview.LeftItem;
-                                    scrollBar.Refresh ();
-                                };
+        //listview.DrawingContent += (s, e) =>
+        //                        {
+        //                            scrollBar.Size = listview.Source.Count - 1;
+        //                            scrollBar.Position = listview.TopItem;
+        //                            scrollBar.OtherScrollBarView.Size = listview.MaxLength - 1;
+        //                            scrollBar.OtherScrollBarView.Position = listview.LeftItem;
+        //                            scrollBar.Refresh ();
+        //                        };
 
         // ComboBox
         var lbComboBox = new Label
@@ -111,40 +111,40 @@ public class ListsAndCombos : Scenario
         comboBox.SelectedItemChanged += (s, text) => lbComboBox.Text = text.Value.ToString ();
         win.Add (lbComboBox, comboBox);
 
-        var scrollBarCbx = new ScrollBarView (comboBox.Subviews [1], true);
+        //var scrollBarCbx = new ScrollBarView (comboBox.Subviews [1], true);
 
-        scrollBarCbx.ChangedPosition += (s, e) =>
-                                        {
-                                            ((ListView)comboBox.Subviews [1]).TopItem = scrollBarCbx.Position;
+        //scrollBarCbx.ChangedPosition += (s, e) =>
+        //                                {
+        //                                    ((ListView)comboBox.Subviews [1]).TopItem = scrollBarCbx.Position;
 
-                                            if (((ListView)comboBox.Subviews [1]).TopItem != scrollBarCbx.Position)
-                                            {
-                                                scrollBarCbx.Position = ((ListView)comboBox.Subviews [1]).TopItem;
-                                            }
+        //                                    if (((ListView)comboBox.Subviews [1]).TopItem != scrollBarCbx.Position)
+        //                                    {
+        //                                        scrollBarCbx.Position = ((ListView)comboBox.Subviews [1]).TopItem;
+        //                                    }
 
-                                            comboBox.SetNeedsDraw ();
-                                        };
+        //                                    comboBox.SetNeedsDraw ();
+        //                                };
 
-        scrollBarCbx.OtherScrollBarView.ChangedPosition += (s, e) =>
-                                                           {
-                                                               ((ListView)comboBox.Subviews [1]).LeftItem = scrollBarCbx.OtherScrollBarView.Position;
+        //scrollBarCbx.OtherScrollBarView.ChangedPosition += (s, e) =>
+        //                                                   {
+        //                                                       ((ListView)comboBox.Subviews [1]).LeftItem = scrollBarCbx.OtherScrollBarView.Position;
 
-                                                               if (((ListView)comboBox.Subviews [1]).LeftItem != scrollBarCbx.OtherScrollBarView.Position)
-                                                               {
-                                                                   scrollBarCbx.OtherScrollBarView.Position = ((ListView)comboBox.Subviews [1]).LeftItem;
-                                                               }
+        //                                                       if (((ListView)comboBox.Subviews [1]).LeftItem != scrollBarCbx.OtherScrollBarView.Position)
+        //                                                       {
+        //                                                           scrollBarCbx.OtherScrollBarView.Position = ((ListView)comboBox.Subviews [1]).LeftItem;
+        //                                                       }
 
-                                                               comboBox.SetNeedsDraw ();
-                                                           };
+        //                                                       comboBox.SetNeedsDraw ();
+        //                                                   };
 
-        comboBox.DrawingContent += (s, e) =>
-                                {
-                                    scrollBarCbx.Size = comboBox.Source.Count;
-                                    scrollBarCbx.Position = ((ListView)comboBox.Subviews [1]).TopItem;
-                                    scrollBarCbx.OtherScrollBarView.Size = ((ListView)comboBox.Subviews [1]).MaxLength - 1;
-                                    scrollBarCbx.OtherScrollBarView.Position = ((ListView)comboBox.Subviews [1]).LeftItem;
-                                    scrollBarCbx.Refresh ();
-                                };
+        //comboBox.DrawingContent += (s, e) =>
+        //                        {
+        //                            scrollBarCbx.Size = comboBox.Source.Count;
+        //                            scrollBarCbx.Position = ((ListView)comboBox.Subviews [1]).TopItem;
+        //                            scrollBarCbx.OtherScrollBarView.Size = ((ListView)comboBox.Subviews [1]).MaxLength - 1;
+        //                            scrollBarCbx.OtherScrollBarView.Position = ((ListView)comboBox.Subviews [1]).LeftItem;
+        //                            scrollBarCbx.Refresh ();
+        //                        };
 
         var btnMoveUp = new Button { X = 1, Y = Pos.Bottom (lbListView), Text = "Move _Up" };
         btnMoveUp.Accepting += (s, e) => { listview.MoveUp (); };

+ 398 - 0
UICatalog/Scenarios/ScrollBarDemo.cs

@@ -0,0 +1,398 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("ScrollBar Demo", "Demonstrates ScrollBar.")]
+[ScenarioCategory ("Scrolling")]
+public class ScrollBarDemo : Scenario
+{
+    public override void Main ()
+    {
+        Application.Init ();
+
+        Window app = new ()
+        {
+            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}",
+            Arrangement = ViewArrangement.Fixed
+        };
+
+        var demoFrame = new FrameView ()
+        {
+            Title = "Demo View",
+            X = 0,
+            Width = 75,
+            Height = 25 + 4,
+            ColorScheme = Colors.ColorSchemes ["Base"],
+            Arrangement = ViewArrangement.Resizable
+        };
+        demoFrame!.Padding!.Thickness = new (1);
+        demoFrame.Padding.Diagnostics = ViewDiagnosticFlags.Ruler;
+        app.Add (demoFrame);
+
+        var scrollBar = new ScrollBar
+        {
+            X = Pos.AnchorEnd () - 5,
+            AutoShow = false,
+            ScrollableContentSize = 100,
+            Height = Dim.Fill()
+        };
+        demoFrame.Add (scrollBar);
+
+        ListView controlledList = new ()
+        {
+            X = Pos.AnchorEnd (),
+            Width = 5,
+            Height = Dim.Fill (),
+            ColorScheme = Colors.ColorSchemes ["Error"],
+        };
+
+        demoFrame.Add (controlledList);
+
+        // populate the list box with Size items of the form "{n:00000}"
+        controlledList.SetSource (new ObservableCollection<string> (Enumerable.Range (0, scrollBar.ScrollableContentSize).Select (n => $"{n:00000}")));
+
+        int GetMaxLabelWidth (int groupId)
+        {
+            return demoFrame.Subviews.Max (
+                                           v =>
+                                           {
+                                               if (v.Y.Has<PosAlign> (out var pos) && pos.GroupId == groupId)
+                                               {
+                                                   return v.Text.GetColumns ();
+                                               }
+
+                                               return 0;
+                                           });
+        }
+
+        var lblWidthHeight = new Label
+        {
+            Text = "_Width/Height:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, AlignmentModes.StartToEnd, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        demoFrame.Add (lblWidthHeight);
+
+        NumericUpDown<int> scrollWidthHeight = new ()
+        {
+            Value = 1,
+            X = Pos.Right (lblWidthHeight) + 1,
+            Y = Pos.Top (lblWidthHeight),
+        };
+        demoFrame.Add (scrollWidthHeight);
+
+        scrollWidthHeight.ValueChanging += (s, e) =>
+                                           {
+                                               if (e.NewValue < 1
+                                                   || (e.NewValue
+                                                       > (scrollBar.Orientation == Orientation.Vertical
+                                                              ? scrollBar.SuperView?.GetContentSize ().Width
+                                                              : scrollBar.SuperView?.GetContentSize ().Height)))
+                                               {
+                                                   // TODO: This must be handled in the ScrollSlider if Width and Height being virtual
+                                                   e.Cancel = true;
+
+                                                   return;
+                                               }
+
+                                               if (scrollBar.Orientation == Orientation.Vertical)
+                                               {
+                                                   scrollBar.Width = e.NewValue;
+                                               }
+                                               else
+                                               {
+                                                   scrollBar.Height = e.NewValue;
+                                               }
+                                           };
+
+
+        var lblOrientationLabel = new Label
+        {
+            Text = "_Orientation:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        demoFrame.Add (lblOrientationLabel);
+
+        var rgOrientation = new RadioGroup
+        {
+            X = Pos.Right (lblOrientationLabel) + 1,
+            Y = Pos.Top (lblOrientationLabel),
+            RadioLabels = ["Vertical", "Horizontal"],
+            Orientation = Orientation.Horizontal
+        };
+        demoFrame.Add (rgOrientation);
+
+        rgOrientation.SelectedItemChanged += (s, e) =>
+                                             {
+                                                 if (e.SelectedItem == e.PreviousSelectedItem)
+                                                 {
+                                                     return;
+                                                 }
+
+                                                 if (rgOrientation.SelectedItem == 0)
+                                                 {
+                                                     scrollBar.Orientation = Orientation.Vertical;
+                                                     scrollBar.X = Pos.AnchorEnd () - 5;
+                                                     scrollBar.Y = 0;
+                                                     scrollBar.Width = scrollWidthHeight.Value;
+                                                     scrollBar.Height = Dim.Fill ();
+                                                     controlledList.Visible = true;
+                                                 }
+                                                 else
+                                                 {
+                                                     scrollBar.Orientation = Orientation.Horizontal;
+                                                     scrollBar.X = 0;
+                                                     scrollBar.Y = Pos.AnchorEnd ();
+                                                     scrollBar.Height = scrollWidthHeight.Value;
+                                                     scrollBar.Width = Dim.Fill ();
+                                                     controlledList.Visible = false;
+
+                                                 }
+                                             };
+
+        var lblSize = new Label
+        {
+            Text = "Scrollable_ContentSize:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        demoFrame.Add (lblSize);
+
+        NumericUpDown<int> scrollContentSize = new ()
+        {
+            Value = scrollBar.ScrollableContentSize,
+            X = Pos.Right (lblSize) + 1,
+            Y = Pos.Top (lblSize)
+        };
+        demoFrame.Add (scrollContentSize);
+
+        scrollContentSize.ValueChanging += (s, e) =>
+                                    {
+                                        if (e.NewValue < 0)
+                                        {
+                                            e.Cancel = true;
+
+                                            return;
+                                        }
+
+                                        if (scrollBar.ScrollableContentSize != e.NewValue)
+                                        {
+                                            scrollBar.ScrollableContentSize = e.NewValue;
+                                            controlledList.SetSource (new ObservableCollection<string> (Enumerable.Range (0, scrollBar.ScrollableContentSize).Select (n => $"{n:00000}")));
+                                        }
+                                    };
+
+        var lblVisibleContentSize = new Label
+        {
+            Text = "_VisibleContentSize:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        demoFrame.Add (lblVisibleContentSize);
+
+        NumericUpDown<int> visibleContentSize = new ()
+        {
+            Value = scrollBar.VisibleContentSize,
+            X = Pos.Right (lblVisibleContentSize) + 1,
+            Y = Pos.Top (lblVisibleContentSize)
+        };
+        demoFrame.Add (visibleContentSize);
+
+        visibleContentSize.ValueChanging += (s, e) =>
+                                           {
+                                               if (e.NewValue < 0)
+                                               {
+                                                   e.Cancel = true;
+
+                                                   return;
+                                               }
+
+                                               if (scrollBar.VisibleContentSize != e.NewValue)
+                                               {
+                                                   scrollBar.VisibleContentSize = e.NewValue;
+                                               }
+                                           };
+
+
+        var lblPosition = new Label
+        {
+            Text = "_Position:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+
+        };
+        demoFrame.Add (lblPosition);
+
+        NumericUpDown<int> scrollPosition = new ()
+        {
+            Value = scrollBar.GetSliderPosition (),
+            X = Pos.Right (lblPosition) + 1,
+            Y = Pos.Top (lblPosition)
+        };
+        demoFrame.Add (scrollPosition);
+
+        scrollPosition.ValueChanging += (s, e) =>
+                                               {
+                                                   if (e.NewValue < 0)
+                                                   {
+                                                       e.Cancel = true;
+
+                                                       return;
+                                                   }
+
+                                                   if (scrollBar.Position != e.NewValue)
+                                                   {
+                                                       scrollBar.Position = e.NewValue;
+                                                   }
+
+                                                   if (scrollBar.Position != e.NewValue)
+                                                   {
+                                                       e.Cancel = true;
+                                                   }
+                                               };
+
+        var lblOptions = new Label
+        {
+            Text = "Options:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        demoFrame.Add (lblOptions);
+        var autoShow = new CheckBox
+        {
+            Y = Pos.Top (lblOptions),
+            X = Pos.Right (lblOptions) + 1,
+            Text = $"_AutoShow",
+            CheckedState = scrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked
+        };
+        autoShow.CheckedStateChanging += (s, e) => scrollBar.AutoShow = e.NewValue == CheckState.Checked;
+        demoFrame.Add (autoShow);
+
+        var lblSliderPosition = new Label
+        {
+            Text = "SliderPosition:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        demoFrame.Add (lblSliderPosition);
+
+        Label scrollSliderPosition = new ()
+        {
+            Text = scrollBar.GetSliderPosition ().ToString (),
+            X = Pos.Right (lblSliderPosition) + 1,
+            Y = Pos.Top (lblSliderPosition)
+        };
+        demoFrame.Add (scrollSliderPosition);
+
+        var lblScrolled = new Label
+        {
+            Text = "Scrolled:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+
+        };
+        demoFrame.Add (lblScrolled);
+        Label scrolled = new ()
+        {
+            X = Pos.Right (lblScrolled) + 1,
+            Y = Pos.Top (lblScrolled)
+        };
+        demoFrame.Add (scrolled);
+
+        var lblScrollFrame = new Label
+        {
+            Y = Pos.Bottom (lblScrolled) + 1
+        };
+        demoFrame.Add (lblScrollFrame);
+
+        var lblScrollViewport = new Label
+        {
+            Y = Pos.Bottom (lblScrollFrame)
+        };
+        demoFrame.Add (lblScrollViewport);
+
+        var lblScrollContentSize = new Label
+        {
+            Y = Pos.Bottom (lblScrollViewport)
+        };
+        demoFrame.Add (lblScrollContentSize);
+
+        scrollBar.SubviewsLaidOut += (s, e) =>
+                                     {
+                                         lblScrollFrame.Text = $"Scroll Frame: {scrollBar.Frame.ToString ()}";
+                                         lblScrollViewport.Text = $"Scroll Viewport: {scrollBar.Viewport.ToString ()}";
+                                         lblScrollContentSize.Text = $"Scroll ContentSize: {scrollBar.GetContentSize ().ToString ()}";
+                                         visibleContentSize.Value = scrollBar.VisibleContentSize;
+                                     };
+
+        EventLog eventLog = new ()
+        {
+            X = Pos.AnchorEnd (),
+            Y = 0,
+            Height = Dim.Fill (),
+            BorderStyle = LineStyle.Single,
+            ViewToLog = scrollBar
+        };
+        app.Add (eventLog);
+
+        app.Initialized += AppOnInitialized;
+
+        void AppOnInitialized (object sender, EventArgs e)
+        {
+            scrollBar.ScrollableContentSizeChanged += (s, e) =>
+                                  {
+                                      eventLog.Log ($"SizeChanged: {e.CurrentValue}");
+
+                                      if (scrollContentSize.Value != e.CurrentValue)
+                                      {
+                                          scrollContentSize.Value = e.CurrentValue;
+                                      }
+                                  };
+
+            scrollBar.SliderPositionChanged += (s, e) =>
+                                            {
+                                                eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}");
+                                                eventLog.Log ($"  Position: {scrollBar.Position}");
+                                                scrollSliderPosition.Text = e.CurrentValue.ToString ();
+                                            };
+
+            scrollBar.Scrolled += (s, e) =>
+                               {
+                                   eventLog.Log ($"Scrolled: {e.CurrentValue}");
+                                   eventLog.Log ($"  SliderPosition: {scrollBar.GetSliderPosition ()}");
+                                   scrolled.Text = e.CurrentValue.ToString ();
+                               };
+
+            scrollBar.PositionChanged += (s, e) =>
+                                             {
+                                                 eventLog.Log ($"PositionChanged: {e.CurrentValue}");
+                                                 scrollPosition.Value = e.CurrentValue;
+                                                 controlledList.Viewport = controlledList.Viewport with { Y = e.CurrentValue };
+                                             };
+
+
+            controlledList.ViewportChanged += (s, e) =>
+                                              {
+                                                  eventLog.Log ($"ViewportChanged: {e.NewViewport}");
+                                                  scrollBar.Position = e.NewViewport.Y;
+                                              };
+
+        }
+
+        Application.Run (app);
+        app.Dispose ();
+        Application.Shutdown ();
+    }
+}

+ 184 - 187
UICatalog/Scenarios/Scrolling.cs

@@ -3,217 +3,99 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios;
 
-[ScenarioMetadata ("Scrolling", "Demonstrates scrolling etc...")]
+[ScenarioMetadata ("Scrolling", "Content scrolling, IScrollBars, etc...")]
 [ScenarioCategory ("Controls")]
 [ScenarioCategory ("Scrolling")]
 [ScenarioCategory ("Tests")]
 public class Scrolling : Scenario
 {
-    private ViewDiagnosticFlags _diagnosticFlags;
-
     public override void Main ()
     {
         Application.Init ();
-        _diagnosticFlags = View.Diagnostics;
-        View.Diagnostics = ViewDiagnosticFlags.Ruler;
 
         var app = new Window
         {
-            Title = GetQuitKeyAndName (),
-
-            // Offset to stress clipping
-            X = 3,
-            Y = 3,
-            Width = Dim.Fill (3),
-            Height = Dim.Fill (3)
+            Title = GetQuitKeyAndName ()
         };
 
         var label = new Label { X = 0, Y = 0 };
         app.Add (label);
 
-        var scrollView = new ScrollView
+        var demoView = new DemoView
         {
-            Id = "scrollView",
+            Id = "demoView",
             X = 2,
             Y = Pos.Bottom (label) + 1,
             Width = 60,
-            Height = 20,
-            ColorScheme = Colors.ColorSchemes ["TopLevel"],
-
-            //ContentOffset = Point.Empty,
-            ShowVerticalScrollIndicator = true,
-            ShowHorizontalScrollIndicator = true
-        };
-        // BUGBUG: set_ContentSize is supposed to be `protected`. 
-        scrollView.SetContentSize (new (120, 40));
-        scrollView.Padding.Thickness = new (1);
-
-        label.Text = $"{scrollView}\nContentSize: {scrollView.GetContentSize ()}\nContentOffset: {scrollView.ContentOffset}";
-
-        const string rule = "0123456789";
-
-        var horizontalRuler = new Label
-        {
-            X = 0,
-            Y = 0,
-
-            Width = Dim.Fill (),
-            Height = 2,
-            ColorScheme = Colors.ColorSchemes ["Error"]
+            Height = 20
         };
-        scrollView.Add (horizontalRuler);
-
-        const string vrule = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n";
 
-        var verticalRuler = new Label
-        {
-            X = 0,
-            Y = 0,
+        label.Text =
+            $"{demoView}\nContentSize: {demoView.GetContentSize ()}\nViewport.Location: {demoView.Viewport.Location}";
 
-            Width = 1,
-            Height = Dim.Fill (),
-            ColorScheme = Colors.ColorSchemes ["Error"]
-        };
-        scrollView.Add (verticalRuler);
-
-        var pressMeButton = new Button { X = 3, Y = 3, Text = "Press me!" };
-        pressMeButton.Accepting += (s, e) => MessageBox.Query (20, 7, "MessageBox", "Neat?", "Yes", "No");
-        scrollView.Add (pressMeButton);
+        demoView.ViewportChanged += (_, _) =>
+                                    {
+                                        label.Text =
+                                            $"{demoView}\nContentSize: {demoView.GetContentSize ()}\nViewport.Location: {demoView.Viewport.Location}";
+                                    };
 
-        var aLongButton = new Button
-        {
-            X = 3,
-            Y = 4,
-
-            Width = Dim.Fill (3),
-            Text = "A very long button. Should be wide enough to demo clipping!"
-        };
-        aLongButton.Accepting += (s, e) => MessageBox.Query (20, 7, "MessageBox", "Neat?", "Yes", "No");
-        scrollView.Add (aLongButton);
-
-        scrollView.Add (
-                        new TextField
-                        {
-                            X = 3,
-                            Y = 5,
-                            Width = 50,
-                            ColorScheme = Colors.ColorSchemes ["Dialog"],
-                            Text = "This is a test of..."
-                        }
-                       );
-
-        scrollView.Add (
-                        new TextField
-                        {
-                            X = 3,
-                            Y = 10,
-                            Width = 50,
-                            ColorScheme = Colors.ColorSchemes ["Dialog"],
-                            Text = "... the emergency broadcast system."
-                        }
-                       );
-
-        scrollView.Add (
-                        new TextField
-                        {
-                            X = 3,
-                            Y = 99,
-                            Width = 50,
-                            ColorScheme = Colors.ColorSchemes ["Dialog"],
-                            Text = "Last line"
-                        }
-                       );
-
-        // Demonstrate AnchorEnd - Button is anchored to bottom/right
-        var anchorButton = new Button { Y = Pos.AnchorEnd (0) - 1, Text = "Bottom Right" };
-
-        // TODO: Use Pos.Width instead of (Right-Left) when implemented (#502)
-        anchorButton.X = Pos.AnchorEnd (0) - (Pos.Right (anchorButton) - Pos.Left (anchorButton));
-
-        anchorButton.Accepting += (s, e) =>
-                               {
-                                   // This demonstrates how to have a dynamically sized button
-                                   // Each time the button is clicked the button's text gets longer
-                                   anchorButton.Text += "!";
-
-                               };
-        scrollView.Add (anchorButton);
-
-        app.Add (scrollView);
+        app.Add (demoView);
 
+        //// NOTE: This call to EnableScrollBar is technically not needed because the reference
+        //// NOTE: to demoView.HorizontalScrollBar below will cause it to be lazy created.
+        //// NOTE: The call included in this sample to for illustration purposes.
+        //demoView.EnableScrollBar (Orientation.Horizontal);
         var hCheckBox = new CheckBox
         {
-            X = Pos.X (scrollView),
-            Y = Pos.Bottom (scrollView),
-            Text = "Horizontal Scrollbar",
-            CheckedState = scrollView.ShowHorizontalScrollIndicator ? CheckState.Checked : CheckState.UnChecked
+            X = Pos.X (demoView),
+            Y = Pos.Bottom (demoView),
+            Text = "_HorizontalScrollBar.Visible",
+            CheckedState = demoView.HorizontalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked
         };
         app.Add (hCheckBox);
+        hCheckBox.CheckedStateChanged += (sender, args) => { demoView.HorizontalScrollBar.Visible = args.CurrentValue == CheckState.Checked; };
 
+        //// NOTE: This call to EnableScrollBar is technically not needed because the reference
+        //// NOTE: to demoView.HorizontalScrollBar below will cause it to be lazy created.
+        //// NOTE: The call included in this sample to for illustration purposes.
+        //demoView.EnableScrollBar (Orientation.Vertical);
         var vCheckBox = new CheckBox
         {
             X = Pos.Right (hCheckBox) + 3,
-            Y = Pos.Bottom (scrollView),
-            Text = "Vertical Scrollbar",
-            CheckedState = scrollView.ShowVerticalScrollIndicator ? CheckState.Checked : CheckState.UnChecked
+            Y = Pos.Bottom (demoView),
+            Text = "_VerticalScrollBar.Visible",
+            CheckedState = demoView.VerticalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked
         };
         app.Add (vCheckBox);
-
-        var t = "Auto Hide Scrollbars";
+        vCheckBox.CheckedStateChanged += (sender, args) => { demoView.VerticalScrollBar.Visible = args.CurrentValue == CheckState.Checked; };
 
         var ahCheckBox = new CheckBox
         {
-            X = Pos.Left (scrollView), Y = Pos.Bottom (hCheckBox), Text = t, CheckedState = scrollView.AutoHideScrollBars ? CheckState.Checked : CheckState.UnChecked
+            X = Pos.Left (demoView),
+            Y = Pos.Bottom (hCheckBox),
+            Text = "_AutoShow (both)",
+            CheckedState = demoView.HorizontalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked
         };
-        var k = "Keep Content Always In Viewport";
-
-        var keepCheckBox = new CheckBox
-        {
-            X = Pos.Left (scrollView), Y = Pos.Bottom (ahCheckBox), Text = k, CheckedState = scrollView.AutoHideScrollBars ? CheckState.Checked : CheckState.UnChecked
-        };
-
-        hCheckBox.CheckedStateChanging += (s, e) =>
-                             {
-                                 if (ahCheckBox.CheckedState == CheckState.UnChecked)
-                                 {
-                                     scrollView.ShowHorizontalScrollIndicator = e.NewValue == CheckState.Checked;
-                                 }
-                                 else
-                                 {
-                                     hCheckBox.CheckedState = CheckState.Checked;
-                                     MessageBox.Query ("Message", "Disable Auto Hide Scrollbars first.", "Ok");
-                                 }
-                             };
-
-        vCheckBox.CheckedStateChanging += (s, e) =>
-                             {
-                                 if (ahCheckBox.CheckedState == CheckState.UnChecked)
-                                 {
-                                     scrollView.ShowVerticalScrollIndicator = e.NewValue == CheckState.Checked;
-                                 }
-                                 else
-                                 {
-                                     vCheckBox.CheckedState = CheckState.Checked;
-                                     MessageBox.Query ("Message", "Disable Auto Hide Scrollbars first.", "Ok");
-                                 }
-                             };
 
         ahCheckBox.CheckedStateChanging += (s, e) =>
-                              {
-                                  scrollView.AutoHideScrollBars = e.NewValue == CheckState.Checked;
-                                  hCheckBox.CheckedState = CheckState.Checked;
-                                  vCheckBox.CheckedState = CheckState.Checked;
-                              };
+                                           {
+                                               demoView.HorizontalScrollBar.AutoShow = e.NewValue == CheckState.Checked;
+                                               demoView.VerticalScrollBar.AutoShow = e.NewValue == CheckState.Checked;
+                                           };
         app.Add (ahCheckBox);
 
-        keepCheckBox.CheckedStateChanging += (s, e) => scrollView.KeepContentAlwaysInViewport = e.NewValue == CheckState.Checked;
-        app.Add (keepCheckBox);
+        demoView.VerticalScrollBar.VisibleChanging += (sender, args) => { vCheckBox.CheckedState = args.NewValue ? CheckState.Checked : CheckState.UnChecked; };
+
+        demoView.HorizontalScrollBar.VisibleChanging += (sender, args) =>
+                                                        {
+                                                            hCheckBox.CheckedState = args.NewValue ? CheckState.Checked : CheckState.UnChecked;
+                                                        };
 
         var count = 0;
 
         var mousePos = new Label
         {
-            X = Pos.Right (scrollView) + 1,
+            X = Pos.Right (demoView) + 1,
             Y = Pos.AnchorEnd (1),
 
             Width = 50,
@@ -223,53 +105,168 @@ public class Scrolling : Scenario
         Application.MouseEvent += (sender, a) => { mousePos.Text = $"Mouse: ({a.Position}) - {a.Flags} {count++}"; };
 
         // Add a progress bar to cause constant redraws
-        var progress = new ProgressBar { X = Pos.Right (scrollView) + 1, Y = Pos.AnchorEnd (2), Width = 50 };
+        var progress = new ProgressBar { X = Pos.Right (demoView) + 1, Y = Pos.AnchorEnd (2), Width = 50 };
 
         app.Add (progress);
 
         var pulsing = true;
 
-        bool timer ()
+        bool TimerFn ()
         {
             progress.Pulse ();
 
             return pulsing;
         }
 
-        Application.AddTimeout (TimeSpan.FromMilliseconds (300), timer);
+        Application.AddTimeout (TimeSpan.FromMilliseconds (300), TimerFn);
 
-        app.Loaded += App_Loaded;
-        app.Unloaded += app_Unloaded;
+        app.Unloaded += AppUnloaded;
 
         Application.Run (app);
-        app.Loaded -= App_Loaded;
-        app.Unloaded -= app_Unloaded;
+        app.Unloaded -= AppUnloaded;
         app.Dispose ();
         Application.Shutdown ();
 
         return;
 
-        // Local functions
-        void App_Loaded (object sender, EventArgs args)
+        void AppUnloaded (object sender, EventArgs args) { pulsing = false; }
+    }
+}
+
+public class DemoView : View
+{
+    public DemoView ()
+    {
+        base.ColorScheme = Colors.ColorSchemes ["TopLevel"];
+        CanFocus = true;
+        BorderStyle = LineStyle.Heavy;
+        Arrangement = ViewArrangement.Resizable;
+        Initialized += OnInitialized;
+        HorizontalScrollBar.AutoShow = true;
+        VerticalScrollBar.AutoShow = true;
+    }
+
+    private void OnInitialized (object sender, EventArgs e)
+    {
+        SetContentSize (new (80, 25));
+
+        var rulerView = new View
+        {
+            Height = Dim.Fill (),
+            Width = Dim.Fill ()
+        };
+        rulerView.Border!.Thickness = new (1);
+        rulerView.Border.LineStyle = LineStyle.None;
+        rulerView.Border.Diagnostics = ViewDiagnosticFlags.Ruler;
+        rulerView.Border.ColorScheme = Colors.ColorSchemes ["Error"];
+
+        Add (rulerView);
+
+        var centeredLabel = new Label ()
+        {
+            X = Pos.Center (),
+            Y = Pos.Center (),
+            TextAlignment = Alignment.Center,
+            VerticalTextAlignment = Alignment.Center,
+            Text = $"This label is centred.\nContentSize is {GetContentSize ()}"
+        };
+        Add (centeredLabel);
+
+        var pressMeButton = new Button
+        {
+            X = 1,
+            Y = 1,
+            Text = "Press me!"
+        };
+        pressMeButton.Accepting += (s, e) => MessageBox.Query (20, 7, "MessageBox", "Neat?", "Yes", "No");
+        Add (pressMeButton);
+
+        var aLongButton = new Button
+        {
+            X = Pos.Right (pressMeButton),
+            Y = Pos.Bottom (pressMeButton),
+
+            Text = "A very long button. Should be wide enough to demo clipping!"
+        };
+        aLongButton.Accepting += (s, e) => MessageBox.Query (20, 7, "MessageBox", "Neat?", "Yes", "No");
+        Add (aLongButton);
+
+        Add (
+             new TextField
+             {
+                 X = Pos.Left (pressMeButton),
+                 Y = Pos.Bottom (aLongButton) + 1,
+                 Width = 50,
+                 ColorScheme = Colors.ColorSchemes ["Dialog"],
+                 Text = "This is a test of..."
+             }
+            );
+
+        Add (
+             new TextField
+             {
+                 X = Pos.Left (pressMeButton),
+                 Y = Pos.Bottom (aLongButton) + 3,
+                 Width = 50,
+                 ColorScheme = Colors.ColorSchemes ["Dialog"],
+                 Text = "... the emergency broadcast system."
+             }
+            );
+
+        Add (
+             new TextField
+             {
+                 X = Pos.Left (pressMeButton),
+                 Y = 40,
+                 Width = 50,
+                 ColorScheme = Colors.ColorSchemes ["Error"],
+                 Text = "Last line - Beyond content area @ Y = 40"
+             }
+            );
+
+        // Demonstrate AnchorEnd - Button is anchored to bottom/right
+        var anchorButton = new Button
         {
-            horizontalRuler.Text =
-                rule.Repeat ((int)Math.Ceiling (horizontalRuler.Viewport.Width / (double)rule.Length)) [
-                                                                                                        ..horizontalRuler.Viewport.Width]
-                + "\n"
-                + "|         ".Repeat (
-                                       (int)Math.Ceiling (horizontalRuler.Viewport.Width / (double)rule.Length)
-                                      ) [
-                                         ..horizontalRuler.Viewport.Width];
-
-            verticalRuler.Text =
-                vrule.Repeat ((int)Math.Ceiling (verticalRuler.Viewport.Height * 2 / (double)rule.Length))
-                    [..(verticalRuler.Viewport.Height * 2)];
+            X = Pos.AnchorEnd (),
+            Y = Pos.AnchorEnd (),
+            Text = "Bottom Right"
+        };
+
+        anchorButton.Accepting += (s, e) =>
+                                  {
+                                      // This demonstrates how to have a dynamically sized button
+                                      // Each time the button is clicked the button's text gets longer
+                                      anchorButton.Text += "!";
+                                  };
+        Add (anchorButton);
+    }
+
+    protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
+    {
+        if (mouseEvent.Flags == MouseFlags.WheeledDown)
+        {
+            ScrollVertical (1);
+            return mouseEvent.Handled = true;
+        }
+
+        if (mouseEvent.Flags == MouseFlags.WheeledUp)
+        {
+            ScrollVertical (-1);
+            return mouseEvent.Handled = true;
         }
 
-        void app_Unloaded (object sender, EventArgs args)
+        if (mouseEvent.Flags == MouseFlags.WheeledRight)
         {
-            View.Diagnostics = _diagnosticFlags;
-            pulsing = false;
+            ScrollHorizontal (1);
+            return mouseEvent.Handled = true;
         }
+
+        if (mouseEvent.Flags == MouseFlags.WheeledLeft)
+        {
+            ScrollHorizontal (-1);
+            return mouseEvent.Handled = true;
+        }
+
+        return false;
     }
 }

+ 41 - 48
UICatalog/Scenarios/TableEditor.cs

@@ -65,8 +65,8 @@ public class TableEditor : Scenario
              "Cuneiform Numbers and Punctuation"
             ),
         new (
-             (uint)(CharMap.MaxCodePoint - 16),
-             (uint)CharMap.MaxCodePoint,
+             (uint)(UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End) - 16),
+             (uint)UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End),
              "End"
             ),
         new (0x0020, 0x007F, "Basic Latin"),
@@ -666,7 +666,7 @@ public class TableEditor : Scenario
         _tableView.CellActivated += EditCurrentCell;
         _tableView.KeyDown += TableViewKeyPress;
 
-        SetupScrollBar ();
+        //SetupScrollBar ();
 
         _redColorScheme = new ()
         {
@@ -1157,40 +1157,40 @@ public class TableEditor : Scenario
 
     private void SetTable (DataTable dataTable) { _tableView.Table = new DataTableSource (_currentTable = dataTable); }
 
-    private void SetupScrollBar ()
-    {
-        var scrollBar = new ScrollBarView (_tableView, true);
-
-        scrollBar.ChangedPosition += (s, e) =>
-                                     {
-                                         _tableView.RowOffset = scrollBar.Position;
-
-                                         if (_tableView.RowOffset != scrollBar.Position)
-                                         {
-                                             scrollBar.Position = _tableView.RowOffset;
-                                         }
-
-                                         _tableView.SetNeedsDraw ();
-                                     };
-        /*
-        scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
-            tableView.LeftItem = scrollBar.OtherScrollBarView.Position;
-            if (tableView.LeftItem != scrollBar.OtherScrollBarView.Position) {
-                scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
-            }
-            tableView.SetNeedsDraw ();
-        };*/
-
-        _tableView.DrawingContent += (s, e) =>
-                                  {
-                                      scrollBar.Size = _tableView.Table?.Rows ?? 0;
-                                      scrollBar.Position = _tableView.RowOffset;
-
-                                      //scrollBar.OtherScrollBarView.Size = tableView.Maxlength - 1;
-                                      //scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
-                                      scrollBar.Refresh ();
-                                  };
-    }
+    //private void SetupScrollBar ()
+    //{
+    //    var scrollBar = new ScrollBarView (_tableView, true);
+
+    //    scrollBar.ChangedPosition += (s, e) =>
+    //                                 {
+    //                                     _tableView.RowOffset = scrollBar.Position;
+
+    //                                     if (_tableView.RowOffset != scrollBar.Position)
+    //                                     {
+    //                                         scrollBar.Position = _tableView.RowOffset;
+    //                                     }
+
+    //                                     _tableView.SetNeedsDraw ();
+    //                                 };
+    //    /*
+    //    scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
+    //        tableView.LeftItem = scrollBar.OtherScrollBarView.Position;
+    //        if (tableView.LeftItem != scrollBar.OtherScrollBarView.Position) {
+    //            scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
+    //        }
+    //        tableView.SetNeedsDraw ();
+    //    };*/
+
+    //    _tableView.DrawingContent += (s, e) =>
+    //                              {
+    //                                  scrollBar.Size = _tableView.Table?.Rows ?? 0;
+    //                                  scrollBar.Position = _tableView.RowOffset;
+
+    //                                  //scrollBar.OtherScrollBarView.Size = tableView.Maxlength - 1;
+    //                                  //scrollBar.OtherScrollBarView.Position = tableView.LeftItem;
+    //                                  scrollBar.Refresh ();
+    //                              };
+    //}
 
     private void ShowAllColumns ()
     {
@@ -1533,17 +1533,10 @@ public class TableEditor : Scenario
                                   );
     }
 
-    private class UnicodeRange
+    public class UnicodeRange (uint start, uint end, string category)
     {
-        public readonly string Category;
-        public readonly uint End;
-        public readonly uint Start;
-
-        public UnicodeRange (uint start, uint end, string category)
-        {
-            Start = start;
-            End = end;
-            Category = category;
-        }
+        public readonly string Category = category;
+        public readonly uint End = end;
+        public readonly uint Start = start;
     }
 }

+ 43 - 41
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -182,6 +182,8 @@ public class TreeViewFileSystem : Scenario
         _treeViewFiles = new TreeView<IFileSystemInfo> { X = 0, Y = 0, Width = Dim.Percent (50), Height = Dim.Fill () };
         _treeViewFiles.DrawLine += TreeViewFiles_DrawLine;
 
+        _treeViewFiles.VerticalScrollBar.AutoShow = false;
+
         _detailsFrame = new DetailsFrame (_iconProvider)
         {
             X = Pos.Right (_treeViewFiles), Y = 0, Width = Dim.Fill (), Height = Dim.Fill ()
@@ -199,7 +201,7 @@ public class TreeViewFileSystem : Scenario
         _treeViewFiles.GoToFirst ();
         _treeViewFiles.Expand ();
 
-        SetupScrollBar ();
+        //SetupScrollBar ();
 
         _treeViewFiles.SetFocus ();
 
@@ -364,46 +366,46 @@ public class TreeViewFileSystem : Scenario
         _iconProvider.IsOpenGetter = _treeViewFiles.IsExpanded;
     }
 
-    private void SetupScrollBar ()
-    {
-        // When using scroll bar leave the last row of the control free (for over-rendering with scroll bar)
-        _treeViewFiles.Style.LeaveLastRow = true;
-
-        var scrollBar = new ScrollBarView (_treeViewFiles, true);
-
-        scrollBar.ChangedPosition += (s, e) =>
-                                     {
-                                         _treeViewFiles.ScrollOffsetVertical = scrollBar.Position;
-
-                                         if (_treeViewFiles.ScrollOffsetVertical != scrollBar.Position)
-                                         {
-                                             scrollBar.Position = _treeViewFiles.ScrollOffsetVertical;
-                                         }
-
-                                         _treeViewFiles.SetNeedsDraw ();
-                                     };
-
-        scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
-                                                        {
-                                                            _treeViewFiles.ScrollOffsetHorizontal = scrollBar.OtherScrollBarView.Position;
-
-                                                            if (_treeViewFiles.ScrollOffsetHorizontal != scrollBar.OtherScrollBarView.Position)
-                                                            {
-                                                                scrollBar.OtherScrollBarView.Position = _treeViewFiles.ScrollOffsetHorizontal;
-                                                            }
-
-                                                            _treeViewFiles.SetNeedsDraw ();
-                                                        };
-
-        _treeViewFiles.DrawingContent += (s, e) =>
-                                      {
-                                          scrollBar.Size = _treeViewFiles.ContentHeight;
-                                          scrollBar.Position = _treeViewFiles.ScrollOffsetVertical;
-                                          scrollBar.OtherScrollBarView.Size = _treeViewFiles.GetContentWidth (true);
-                                          scrollBar.OtherScrollBarView.Position = _treeViewFiles.ScrollOffsetHorizontal;
-                                          scrollBar.Refresh ();
-                                      };
-    }
+    //private void SetupScrollBar ()
+    //{
+    //    // When using scroll bar leave the last row of the control free (for over-rendering with scroll bar)
+    //    _treeViewFiles.Style.LeaveLastRow = true;
+
+    //    var scrollBar = new ScrollBarView (_treeViewFiles, true);
+
+    //    scrollBar.ChangedPosition += (s, e) =>
+    //                                 {
+    //                                     _treeViewFiles.ScrollOffsetVertical = scrollBar.Position;
+
+    //                                     if (_treeViewFiles.ScrollOffsetVertical != scrollBar.Position)
+    //                                     {
+    //                                         scrollBar.Position = _treeViewFiles.ScrollOffsetVertical;
+    //                                     }
+
+    //                                     _treeViewFiles.SetNeedsDraw ();
+    //                                 };
+
+    //    scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
+    //                                                    {
+    //                                                        _treeViewFiles.ScrollOffsetHorizontal = scrollBar.OtherScrollBarView.Position;
+
+    //                                                        if (_treeViewFiles.ScrollOffsetHorizontal != scrollBar.OtherScrollBarView.Position)
+    //                                                        {
+    //                                                            scrollBar.OtherScrollBarView.Position = _treeViewFiles.ScrollOffsetHorizontal;
+    //                                                        }
+
+    //                                                        _treeViewFiles.SetNeedsDraw ();
+    //                                                    };
+
+    //    _treeViewFiles.DrawingContent += (s, e) =>
+    //                                  {
+    //                                      scrollBar.Size = _treeViewFiles.ContentHeight;
+    //                                      scrollBar.Position = _treeViewFiles.ScrollOffsetVertical;
+    //                                      scrollBar.OtherScrollBarView.Size = _treeViewFiles.GetContentWidth (true);
+    //                                      scrollBar.OtherScrollBarView.Position = _treeViewFiles.ScrollOffsetHorizontal;
+    //                                      scrollBar.Refresh ();
+    //                                  };
+    //}
 
     private void ShowColoredExpandableSymbols ()
     {

+ 142 - 81
UICatalog/Scenarios/ContentScrolling.cs → UICatalog/Scenarios/ViewportSettings.cs

@@ -5,14 +5,13 @@ using Terminal.Gui;
 
 namespace UICatalog.Scenarios;
 
-[ScenarioMetadata ("Content Scrolling", "Demonstrates using View.Viewport and View.GetContentSize () to scroll content.")]
+[ScenarioMetadata ("ViewportSettings", "Demonstrates manipulating Viewport, ViewportSettings, and ContentSize to scroll content.")]
 [ScenarioCategory ("Layout")]
 [ScenarioCategory ("Drawing")]
 [ScenarioCategory ("Scrolling")]
-public class ContentScrolling : Scenario
+[ScenarioCategory ("Adornments")]
+public class ViewportSettings : Scenario
 {
-    private ViewDiagnosticFlags _diagnosticFlags;
-
     public class ScrollingDemoView : FrameView
     {
         public ScrollingDemoView ()
@@ -20,17 +19,17 @@ public class ContentScrolling : Scenario
             Id = "ScrollingDemoView";
             Width = Dim.Fill ();
             Height = Dim.Fill ();
-            ColorScheme = Colors.ColorSchemes ["Base"];
+            base.ColorScheme = Colors.ColorSchemes ["Base"];
 
-            Text =
+            base.Text =
                 "Text (ScrollingDemoView.Text). This is long text.\nThe second line.\n3\n4\n5th line\nLine 6. This is a longer line that should wrap automatically.";
             CanFocus = true;
             BorderStyle = LineStyle.Rounded;
-            Arrangement = ViewArrangement.Fixed;
+            Arrangement = ViewArrangement.Resizable;
 
             SetContentSize (new (60, 40));
-            ViewportSettings |= ViewportSettings.ClearContentOnly;
-            ViewportSettings |= ViewportSettings.ClipContentOnly;
+            ViewportSettings |= Terminal.Gui.ViewportSettings.ClearContentOnly;
+            ViewportSettings |= Terminal.Gui.ViewportSettings.ClipContentOnly;
 
             // Things this view knows how to do
             AddCommand (Command.ScrollDown, () => ScrollVertical (1));
@@ -47,7 +46,8 @@ public class ContentScrolling : Scenario
 
             // Add a status label to the border that shows Viewport and ContentSize values. Bit of a hack.
             // TODO: Move to Padding with controls
-            Border.Add (new Label { X = 20 });
+            Border?.Add (new Label { X = 20 });
+
             ViewportChanged += VirtualDemoView_LayoutComplete;
 
             MouseEvent += VirtualDemoView_MouseEvent;
@@ -84,7 +84,7 @@ public class ContentScrolling : Scenario
 
         private void VirtualDemoView_LayoutComplete (object sender, DrawEventArgs drawEventArgs)
         {
-            Label frameLabel = Padding.Subviews.OfType<Label> ().FirstOrDefault ();
+            Label frameLabel = Padding?.Subviews.OfType<Label> ().FirstOrDefault ();
 
             if (frameLabel is { })
             {
@@ -97,8 +97,6 @@ public class ContentScrolling : Scenario
     {
         Application.Init ();
 
-        _diagnosticFlags = View.Diagnostics;
-
         Window app = new ()
         {
             Title = GetQuitKeyAndName (),
@@ -121,11 +119,11 @@ public class ContentScrolling : Scenario
             Width = Dim.Fill (),
             Height = Dim.Fill ()
         };
+
         app.Add (view);
 
         // Add Scroll Setting UI to Padding
-        view.Padding.Thickness = new (0, 5, 0, 0);
-        view.Padding.ColorScheme = Colors.ColorSchemes ["Error"];
+        view.Padding!.Thickness = view.Padding.Thickness with { Top = view.Padding.Thickness.Top + 6 };
         view.Padding.CanFocus = true;
 
         Label frameLabel = new ()
@@ -142,20 +140,7 @@ public class ContentScrolling : Scenario
             Y = Pos.Bottom (frameLabel),
             CanFocus = true
         };
-        cbAllowNegativeX.CheckedState = view.ViewportSettings.HasFlag (ViewportSettings.AllowNegativeX) ? CheckState.Checked : CheckState.UnChecked;
-        cbAllowNegativeX.CheckedStateChanging += AllowNegativeX_Toggle;
-
-        void AllowNegativeX_Toggle (object sender, CancelEventArgs<CheckState> e)
-        {
-            if (e.NewValue == CheckState.Checked)
-            {
-                view.ViewportSettings |= ViewportSettings.AllowNegativeX;
-            }
-            else
-            {
-                view.ViewportSettings &= ~ViewportSettings.AllowNegativeX;
-            }
-        }
+        cbAllowNegativeX.CheckedState = view.ViewportSettings.HasFlag  (Terminal.Gui.ViewportSettings.AllowNegativeX) ? CheckState.Checked : CheckState.UnChecked;
 
         view.Padding.Add (cbAllowNegativeX);
 
@@ -166,20 +151,7 @@ public class ContentScrolling : Scenario
             Y = Pos.Bottom (frameLabel),
             CanFocus = true,
         };
-        cbAllowNegativeY.CheckedState = view.ViewportSettings.HasFlag (ViewportSettings.AllowNegativeY) ? CheckState.Checked : CheckState.UnChecked;
-        cbAllowNegativeY.CheckedStateChanging += AllowNegativeY_Toggle;
-
-        void AllowNegativeY_Toggle (object sender, CancelEventArgs<CheckState> e)
-        {
-            if (e.NewValue == CheckState.Checked)
-            {
-                view.ViewportSettings |= ViewportSettings.AllowNegativeY;
-            }
-            else
-            {
-                view.ViewportSettings &= ~ViewportSettings.AllowNegativeY;
-            }
-        }
+        cbAllowNegativeY.CheckedState = view.ViewportSettings.HasFlag  (Terminal.Gui.ViewportSettings.AllowNegativeY) ? CheckState.Checked : CheckState.UnChecked;
 
         view.Padding.Add (cbAllowNegativeY);
 
@@ -189,22 +161,33 @@ public class ContentScrolling : Scenario
             Y = Pos.Bottom (cbAllowNegativeX),
             CanFocus = true
         };
-        cbAllowXGreaterThanContentWidth.CheckedState = view.ViewportSettings.HasFlag (ViewportSettings.AllowXGreaterThanContentWidth) ? CheckState.Checked : CheckState.UnChecked;
-        cbAllowXGreaterThanContentWidth.CheckedStateChanging += AllowXGreaterThanContentWidth_Toggle;
+        cbAllowXGreaterThanContentWidth.CheckedState = view.ViewportSettings.HasFlag  (Terminal.Gui.ViewportSettings.AllowXGreaterThanContentWidth) ? CheckState.Checked : CheckState.UnChecked;
 
-        void AllowXGreaterThanContentWidth_Toggle (object sender, CancelEventArgs<CheckState> e)
+        view.Padding.Add (cbAllowXGreaterThanContentWidth);
+
+        void AllowNegativeXToggle (object sender, CancelEventArgs<CheckState> e)
         {
             if (e.NewValue == CheckState.Checked)
             {
-                view.ViewportSettings |= ViewportSettings.AllowXGreaterThanContentWidth;
+                view.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowNegativeX;
             }
             else
             {
-                view.ViewportSettings &= ~ViewportSettings.AllowXGreaterThanContentWidth;
+                view.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowNegativeX;
             }
         }
 
-        view.Padding.Add (cbAllowXGreaterThanContentWidth);
+        void AllowXGreaterThanContentWidthToggle (object sender, CancelEventArgs<CheckState> e)
+        {
+            if (e.NewValue == CheckState.Checked)
+            {
+                view.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowXGreaterThanContentWidth;
+            }
+            else
+            {
+                view.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowXGreaterThanContentWidth;
+            }
+        }
 
         var cbAllowYGreaterThanContentHeight = new CheckBox
         {
@@ -213,26 +196,37 @@ public class ContentScrolling : Scenario
             Y = Pos.Bottom (cbAllowNegativeX),
             CanFocus = true
         };
-        cbAllowYGreaterThanContentHeight.CheckedState = view.ViewportSettings.HasFlag (ViewportSettings.AllowYGreaterThanContentHeight) ? CheckState.Checked : CheckState.UnChecked;
-        cbAllowYGreaterThanContentHeight.CheckedStateChanging += AllowYGreaterThanContentHeight_Toggle;
+        cbAllowYGreaterThanContentHeight.CheckedState = view.ViewportSettings.HasFlag  (Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight) ? CheckState.Checked : CheckState.UnChecked;
+
+        view.Padding.Add (cbAllowYGreaterThanContentHeight);
 
-        void AllowYGreaterThanContentHeight_Toggle (object sender, CancelEventArgs<CheckState> e)
+        void AllowNegativeYToggle (object sender, CancelEventArgs<CheckState> e)
         {
             if (e.NewValue == CheckState.Checked)
             {
-                view.ViewportSettings |= ViewportSettings.AllowYGreaterThanContentHeight;
+                view.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowNegativeY;
             }
             else
             {
-                view.ViewportSettings &= ~ViewportSettings.AllowYGreaterThanContentHeight;
+                view.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowNegativeY;
             }
         }
 
-        view.Padding.Add (cbAllowYGreaterThanContentHeight);
+        void AllowYGreaterThanContentHeightToggle (object sender, CancelEventArgs<CheckState> e)
+        {
+            if (e.NewValue == CheckState.Checked)
+            {
+                view.ViewportSettings |= Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight;
+            }
+            else
+            {
+                view.ViewportSettings &= ~Terminal.Gui.ViewportSettings.AllowYGreaterThanContentHeight;
+            }
+        }
 
         var labelContentSize = new Label
         {
-            Title = "_ContentSize:",
+            Title = "ContentSi_ze:",
             Y = Pos.Bottom (cbAllowYGreaterThanContentHeight)
         };
 
@@ -243,9 +237,9 @@ public class ContentScrolling : Scenario
             Y = Pos.Top (labelContentSize),
             CanFocus = true
         };
-        contentSizeWidth.ValueChanging += ContentSizeWidth_ValueChanged;
+        contentSizeWidth.ValueChanging += ContentSizeWidthValueChanged;
 
-        void ContentSizeWidth_ValueChanged (object sender, CancelEventArgs<int> e)
+        void ContentSizeWidthValueChanged (object sender, CancelEventArgs<int> e)
         {
             if (e.NewValue < 0)
             {
@@ -271,9 +265,9 @@ public class ContentScrolling : Scenario
             Y = Pos.Top (labelContentSize),
             CanFocus = true
         };
-        contentSizeHeight.ValueChanging += ContentSizeHeight_ValueChanged;
+        contentSizeHeight.ValueChanging += ContentSizeHeightValueChanged;
 
-        void ContentSizeHeight_ValueChanged (object sender, CancelEventArgs<int> e)
+        void ContentSizeHeightValueChanged (object sender, CancelEventArgs<int> e)
         {
             if (e.NewValue < 0)
             {
@@ -287,54 +281,123 @@ public class ContentScrolling : Scenario
 
         var cbClearContentOnly = new CheckBox
         {
-            Title = "ClearContentOnly",
+            Title = "C_learContentOnly",
             X = Pos.Right (contentSizeHeight) + 1,
             Y = Pos.Top (labelContentSize),
             CanFocus = true
         };
-        cbClearContentOnly.CheckedState = view.ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly) ? CheckState.Checked : CheckState.UnChecked;
-        cbClearContentOnly.CheckedStateChanging += ClearContentOnly_Toggle;
+        cbClearContentOnly.CheckedState = view.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.ClearContentOnly) ? CheckState.Checked : CheckState.UnChecked;
+        cbClearContentOnly.CheckedStateChanging += ClearContentOnlyToggle;
 
-        void ClearContentOnly_Toggle (object sender, CancelEventArgs<CheckState> e)
+        void ClearContentOnlyToggle (object sender, CancelEventArgs<CheckState> e)
         {
             if (e.NewValue == CheckState.Checked)
             {
-                view.ViewportSettings |= ViewportSettings.ClearContentOnly;
+                view.ViewportSettings |= Terminal.Gui.ViewportSettings.ClearContentOnly;
             }
             else
             {
-                view.ViewportSettings &= ~ViewportSettings.ClearContentOnly;
+                view.ViewportSettings &= ~Terminal.Gui.ViewportSettings.ClearContentOnly;
             }
         }
 
         var cbClipContentOnly = new CheckBox
         {
-            Title = "ClipContentOnly",
+            Title = "_ClipContentOnly",
             X = Pos.Right (cbClearContentOnly) + 1,
             Y = Pos.Top (labelContentSize),
             CanFocus = true
         };
-        cbClipContentOnly.CheckedState = view.ViewportSettings.HasFlag (ViewportSettings.ClipContentOnly) ? CheckState.Checked : CheckState.UnChecked;
-        cbClipContentOnly.CheckedStateChanging += ClipContentOnlyOnly_Toggle;
+        cbClipContentOnly.CheckedState = view.ViewportSettings.HasFlag (Terminal.Gui.ViewportSettings.ClipContentOnly) ? CheckState.Checked : CheckState.UnChecked;
+        cbClipContentOnly.CheckedStateChanging += ClipContentOnlyOnlyToggle;
 
-        void ClipContentOnlyOnly_Toggle (object sender, CancelEventArgs<CheckState> e)
+        void ClipContentOnlyOnlyToggle (object sender, CancelEventArgs<CheckState> e)
         {
             if (e.NewValue == CheckState.Checked)
             {
-                view.ViewportSettings |= ViewportSettings.ClipContentOnly;
+                view.ViewportSettings |= Terminal.Gui.ViewportSettings.ClipContentOnly;
             }
             else
             {
-                view.ViewportSettings &= ~ViewportSettings.ClipContentOnly;
+                view.ViewportSettings &= ~Terminal.Gui.ViewportSettings.ClipContentOnly;
             }
         }
 
-        view.Padding.Add (labelContentSize, contentSizeWidth, labelComma, contentSizeHeight, cbClearContentOnly, cbClipContentOnly);
+        var cbVerticalScrollBar = new CheckBox
+        {
+            Title = "_VerticalScrollBar.Visible",
+            X = 0,
+            Y = Pos.Bottom (labelContentSize),
+            CanFocus = false
+        };
+        cbVerticalScrollBar.CheckedState = view.VerticalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked;
+        cbVerticalScrollBar.CheckedStateChanging += VerticalScrollBarToggle;
+
+        void VerticalScrollBarToggle (object sender, CancelEventArgs<CheckState> e)
+        {
+            view.VerticalScrollBar.Visible = e.NewValue == CheckState.Checked;
+        }
+
+        var cbHorizontalScrollBar = new CheckBox
+        {
+            Title = "_HorizontalScrollBar.Visible",
+            X = Pos.Right (cbVerticalScrollBar) + 1,
+            Y = Pos.Bottom (labelContentSize),
+            CanFocus = false,
+        };
+        cbHorizontalScrollBar.CheckedState = view.HorizontalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked;
+        cbHorizontalScrollBar.CheckedStateChanging += HorizontalScrollBarToggle;
+
+        void HorizontalScrollBarToggle (object sender, CancelEventArgs<CheckState> e)
+        {
+            view.HorizontalScrollBar.Visible = e.NewValue == CheckState.Checked;
+        }
+
+        view.VerticalScrollBar.AutoShow = true;
+        var cbAutoShowVerticalScrollBar = new CheckBox
+        {
+            Title = "VerticalScrollBar._AutoShow",
+            X = Pos.Right (cbHorizontalScrollBar) + 1,
+            Y = Pos.Bottom (labelContentSize),
+            CanFocus = false,
+            CheckedState = view.VerticalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked
+        };
+        cbAutoShowVerticalScrollBar.CheckedStateChanging += AutoShowVerticalScrollBarToggle;
+
+        void AutoShowVerticalScrollBarToggle (object sender, CancelEventArgs<CheckState> e)
+        {
+            view.VerticalScrollBar.AutoShow = e.NewValue == CheckState.Checked;
+        }
+
+        view.HorizontalScrollBar.AutoShow = true;
+        var cbAutoShowHorizontalScrollBar = new CheckBox
+        {
+            Title = "HorizontalScrollBar.A_utoShow ",
+            X = Pos.Right (cbAutoShowVerticalScrollBar) + 1,
+            Y = Pos.Bottom (labelContentSize),
+            CanFocus = false,
+            CheckedState = view.HorizontalScrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked
+        };
+        cbAutoShowHorizontalScrollBar.CheckedStateChanging += AutoShowHorizontalScrollBarToggle;
+
+        void AutoShowHorizontalScrollBarToggle (object sender, CancelEventArgs<CheckState> e)
+        {
+            view.HorizontalScrollBar.AutoShow = e.NewValue == CheckState.Checked;
+        }
+
+        cbAllowNegativeX.CheckedStateChanging += AllowNegativeXToggle;
+        cbAllowNegativeY.CheckedStateChanging += AllowNegativeYToggle;
+
+        cbAllowXGreaterThanContentWidth.CheckedStateChanging += AllowXGreaterThanContentWidthToggle;
+        cbAllowYGreaterThanContentHeight.CheckedStateChanging += AllowYGreaterThanContentHeightToggle;
+
+
+        view.Padding.Add (labelContentSize, contentSizeWidth, labelComma, contentSizeHeight, cbClearContentOnly, cbClipContentOnly, cbVerticalScrollBar, cbHorizontalScrollBar, cbAutoShowVerticalScrollBar, cbAutoShowHorizontalScrollBar);
 
         // Add demo views to show that things work correctly
-        var textField = new TextField { X = 20, Y = 7, Width = 15, Text = "Test TextField" };
+        var textField = new TextField { X = 20, Y = 7, Width = 15, Text = "Test Te_xtField" };
 
-        var colorPicker = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (), Y = 10 };
+        var colorPicker = new ColorPicker16 { Title = "_BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (), Y = 10 };
         colorPicker.BorderStyle = LineStyle.RoundedDotted;
 
         colorPicker.ColorChanged += (s, e) =>
@@ -352,13 +415,13 @@ public class ContentScrolling : Scenario
         {
             X = Pos.Center (),
             Y = 10,
-            Title = "TextView",
+            Title = "TextVie_w",
             Text = "I have a 3 row top border.\nMy border inherits from the SuperView.\nI have 3 lines of text with room for 2.",
             AllowsTab = false,
             Width = 30,
             Height = 6 // TODO: Use Dim.Auto
         };
-        textView.Border.Thickness = new (1, 3, 1, 1);
+        textView.Border!.Thickness = new (1, 3, 1, 1);
 
         var charMap = new CharMap
         {
@@ -377,10 +440,10 @@ public class ContentScrolling : Scenario
         };
         buttonAnchored.Accepting += (sender, args) => MessageBox.Query ("Hi", $"You pressed {((Button)sender)?.Text}", "_Ok");
 
-        view.Margin.Data = "Margin";
+        view.Margin!.Data = "Margin";
         view.Margin.Thickness = new (0);
 
-        view.Border.Data = "Border";
+        view.Border!.Data = "Border";
         view.Border.Thickness = new (3);
 
         view.Padding.Data = "Padding";
@@ -414,8 +477,6 @@ public class ContentScrolling : Scenario
 
         editor.Initialized += (s, e) => { editor.ViewToEdit = view; };
 
-        app.Closed += (s, e) => View.Diagnostics = _diagnosticFlags;
-
         editor.AutoSelectViewToEdit = true;
         editor.AutoSelectSuperView = view;
         editor.AutoSelectAdornments = false;

+ 26 - 26
UICatalog/Scenarios/Wizards.cs

@@ -311,32 +311,32 @@ public class Wizards : Scenario
                                                                  };
                                            fourthStep.Add (hideHelpBtn);
                                            fourthStep.NextButtonText = "_Go To Last Step";
-                                           var scrollBar = new ScrollBarView (someText, true);
-
-                                           scrollBar.ChangedPosition += (s, e) =>
-                                                                        {
-                                                                            someText.TopRow = scrollBar.Position;
-
-                                                                            if (someText.TopRow != scrollBar.Position)
-                                                                            {
-                                                                                scrollBar.Position = someText.TopRow;
-                                                                            }
-
-                                                                            someText.SetNeedsDraw ();
-                                                                        };
-
-                                           someText.DrawingContent += (s, e) =>
-                                                                   {
-                                                                       scrollBar.Size = someText.Lines;
-                                                                       scrollBar.Position = someText.TopRow;
-
-                                                                       if (scrollBar.OtherScrollBarView != null)
-                                                                       {
-                                                                           scrollBar.OtherScrollBarView.Size = someText.Maxlength;
-                                                                           scrollBar.OtherScrollBarView.Position = someText.LeftColumn;
-                                                                       }
-                                                                   };
-                                           fourthStep.Add (scrollBar);
+                                           //var scrollBar = new ScrollBarView (someText, true);
+
+                                           //scrollBar.ChangedPosition += (s, e) =>
+                                           //                             {
+                                           //                                 someText.TopRow = scrollBar.Position;
+
+                                           //                                 if (someText.TopRow != scrollBar.Position)
+                                           //                                 {
+                                           //                                     scrollBar.Position = someText.TopRow;
+                                           //                                 }
+
+                                           //                                 someText.SetNeedsDraw ();
+                                           //                             };
+
+                                           //someText.DrawingContent += (s, e) =>
+                                           //                        {
+                                           //                            scrollBar.Size = someText.Lines;
+                                           //                            scrollBar.Position = someText.TopRow;
+
+                                           //                            if (scrollBar.OtherScrollBarView != null)
+                                           //                            {
+                                           //                                scrollBar.OtherScrollBarView.Size = someText.Maxlength;
+                                           //                                scrollBar.OtherScrollBarView.Position = someText.LeftColumn;
+                                           //                            }
+                                           //                        };
+                                           //fourthStep.Add (scrollBar);
 
                                            // Add last step
                                            var lastStep = new WizardStep { Title = "The last step" };

+ 10 - 2
UICatalog/UICatalog.cs

@@ -18,6 +18,8 @@ using System.Runtime.InteropServices;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
 using Terminal.Gui;
 using UICatalog.Scenarios;
 using static Terminal.Gui.ConfigurationManager;
@@ -439,6 +441,7 @@ public class UICatalogApp
 
         scenario.Dispose ();
 
+
         // TODO: Throw if shutdown was not called already
         Application.Shutdown ();
 
@@ -607,12 +610,12 @@ public class UICatalogApp
         // Validate there are no outstanding Responder-based instances 
         // after a scenario was selected to run. This proves the main UI Catalog
         // 'app' closed cleanly.
-        foreach (Responder? inst in Responder.Instances)
+        foreach (View? inst in View.Instances)
         {
             Debug.Assert (inst.WasDisposed);
         }
 
-        Responder.Instances.Clear ();
+        View.Instances.Clear ();
 
         // Validate there are no outstanding Application.RunState-based instances 
         // after a scenario was selected to run. This proves the main UI Catalog
@@ -799,6 +802,9 @@ public class UICatalogApp
             CategoryList.OpenSelectedItem += (s, a) => { ScenarioList!.SetFocus (); };
             CategoryList.SelectedItemChanged += CategoryView_SelectedChanged;
 
+            // This enables the scrollbar by causing lazy instantiation to happen
+            CategoryList.VerticalScrollBar.AutoShow = true;
+
             // Create the scenario list. The contents of the scenario list changes whenever the
             // Category list selection changes (to show just the scenarios that belong to the selected
             // category).
@@ -821,6 +827,8 @@ public class UICatalogApp
                 BorderStyle = CategoryList.BorderStyle,
                 SuperViewRendersLineCanvas = true
             };
+            //ScenarioList.VerticalScrollBar.AutoHide = false;
+            //ScenarioList.HorizontalScrollBar.AutoHide = false;
 
             // TableView provides many options for table headers. For simplicity we turn all 
             // of these off. By enabling FullRowSelect and turning off headers, TableView looks just

+ 3 - 3
UnitTests/Application/ApplicationTests.cs

@@ -13,7 +13,7 @@ public class ApplicationTests
         ConfigurationManager.Locations = ConfigurationManager.ConfigLocations.None;
 
 #if DEBUG_IDISPOSABLE
-        Responder.Instances.Clear ();
+        View.Instances.Clear ();
         RunState.Instances.Clear ();
 #endif
     }
@@ -66,7 +66,7 @@ public class ApplicationTests
         Assert.True (shutdown);
 
 #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
+        Assert.Empty (View.Instances);
 #endif
         lock (_timeoutLock)
         {
@@ -393,7 +393,7 @@ public class ApplicationTests
         // Validate there are no outstanding Responder-based instances 
         // after a scenario was selected to run. This proves the main UI Catalog
         // 'app' closed cleanly.
-        Assert.Empty (Responder.Instances);
+        Assert.Empty (View.Instances);
 #endif
     }
 

+ 2 - 2
UnitTests/Application/KeyboardTests.cs

@@ -11,7 +11,7 @@ public class KeyboardTests
     {
         _output = output;
 #if DEBUG_IDISPOSABLE
-        Responder.Instances.Clear ();
+        View.Instances.Clear ();
         RunState.Instances.Clear ();
 #endif
     }
@@ -671,7 +671,7 @@ public class KeyboardTests
         Assert.True (shutdown);
 
 #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
+        Assert.Empty (View.Instances);
 #endif
         lock (_timeoutLock)
         {

+ 42 - 42
UnitTests/Application/Mouse/ApplicationMouseTests.cs

@@ -13,7 +13,7 @@ public class ApplicationMouseTests
     {
         _output = output;
 #if DEBUG_IDISPOSABLE
-        Responder.Instances.Clear ();
+        View.Instances.Clear ();
         RunState.Instances.Clear ();
 #endif
     }
@@ -238,67 +238,67 @@ public class ApplicationMouseTests
 
     #region mouse grab tests
 
-    [Fact]
+    [Fact (Skip = "Rebuild to use ScrollBar")]
     [AutoInitShutdown]
     public void MouseGrabView_WithNullMouseEventView ()
     {
-        var tf = new TextField { Width = 10 };
-        var sv = new ScrollView { Width = Dim.Fill (), Height = Dim.Fill () };
-        sv.SetContentSize (new (100, 100));
+        //var tf = new TextField { Width = 10 };
+        //var sv = new ScrollView { Width = Dim.Fill (), Height = Dim.Fill () };
+        //sv.SetContentSize (new (100, 100));
 
-        sv.Add (tf);
-        var top = new Toplevel ();
-        top.Add (sv);
+        //sv.Add (tf);
+        //var top = new Toplevel ();
+        //top.Add (sv);
 
-        int iterations = -1;
+        //int iterations = -1;
 
-        Application.Iteration += (s, a) =>
-                                 {
-                                     iterations++;
+        //Application.Iteration += (s, a) =>
+        //                         {
+        //                             iterations++;
 
-                                     if (iterations == 0)
-                                     {
-                                         Assert.True (tf.HasFocus);
-                                         Assert.Null (Application.MouseGrabView);
+        //                             if (iterations == 0)
+        //                             {
+        //                                 Assert.True (tf.HasFocus);
+        //                                 Assert.Null (Application.MouseGrabView);
 
-                                         Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition });
+        //                                 Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition });
 
-                                         Assert.Equal (sv, Application.MouseGrabView);
+        //                                 Assert.Equal (sv, Application.MouseGrabView);
 
-                                         MessageBox.Query ("Title", "Test", "Ok");
+        //                                 MessageBox.Query ("Title", "Test", "Ok");
 
-                                         Assert.Null (Application.MouseGrabView);
-                                     }
-                                     else if (iterations == 1)
-                                     {
-                                         // Application.MouseGrabView is null because
-                                         // another toplevel (Dialog) was opened
-                                         Assert.Null (Application.MouseGrabView);
+        //                                 Assert.Null (Application.MouseGrabView);
+        //                             }
+        //                             else if (iterations == 1)
+        //                             {
+        //                                 // Application.MouseGrabView is null because
+        //                                 // another toplevel (Dialog) was opened
+        //                                 Assert.Null (Application.MouseGrabView);
 
-                                         Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition });
+        //                                 Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.ReportMousePosition });
 
-                                         Assert.Null (Application.MouseGrabView);
+        //                                 Assert.Null (Application.MouseGrabView);
 
-                                         Application.RaiseMouseEvent (new () { ScreenPosition = new (40, 12), Flags = MouseFlags.ReportMousePosition });
+        //                                 Application.RaiseMouseEvent (new () { ScreenPosition = new (40, 12), Flags = MouseFlags.ReportMousePosition });
 
-                                         Assert.Null (Application.MouseGrabView);
+        //                                 Assert.Null (Application.MouseGrabView);
 
-                                         Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed });
+        //                                 Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 0), Flags = MouseFlags.Button1Pressed });
 
-                                         Assert.Null (Application.MouseGrabView);
+        //                                 Assert.Null (Application.MouseGrabView);
 
-                                         Application.RequestStop ();
-                                     }
-                                     else if (iterations == 2)
-                                     {
-                                         Assert.Null (Application.MouseGrabView);
+        //                                 Application.RequestStop ();
+        //                             }
+        //                             else if (iterations == 2)
+        //                             {
+        //                                 Assert.Null (Application.MouseGrabView);
 
-                                         Application.RequestStop ();
-                                     }
-                                 };
+        //                                 Application.RequestStop ();
+        //                             }
+        //                         };
 
-        Application.Run (top);
-        top.Dispose ();
+        //Application.Run (top);
+        //top.Dispose ();
     }
 
     [Fact]

+ 1 - 1
UnitTests/Application/RunStateTests.cs

@@ -8,7 +8,7 @@ public class RunStateTests
     public RunStateTests ()
     {
 #if DEBUG_IDISPOSABLE
-        Responder.Instances.Clear ();
+        View.Instances.Clear ();
         RunState.Instances.Clear ();
 #endif
     }

+ 1 - 270
UnitTests/Input/ResponderTests.cs

@@ -4,200 +4,7 @@ namespace Terminal.Gui.InputTests;
 
 public class ResponderTests
 {
-    // Generic lifetime (IDisposable) tests
-    [Fact]
-    [TestRespondersDisposed]
-    public void Dispose_Works ()
-    {
-        var r = new Responder ();
-#if DEBUG_IDISPOSABLE
-        Assert.Single (Responder.Instances);
-#endif
-
-        r.Dispose ();
-#if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
-#endif
-    }
-
-    [Fact]
-    public void Disposing_Event_Notify_All_Subscribers_On_The_First_Container ()
-    {
-    #if DEBUG_IDISPOSABLE
-        // Only clear before because need to test after assert
-        Responder.Instances.Clear ();
-    #endif
-
-        var container1 = new View { Id = "Container1" };
-        var count = 0;
-
-        var view = new View { Id = "View" };
-        view.Disposing += View_Disposing;
-        container1.Add (view);
-        Assert.Equal (container1, view.SuperView);
-
-        void View_Disposing (object sender, EventArgs e)
-        {
-            count++;
-            Assert.Equal (view, sender);
-            container1.Remove ((View)sender);
-        }
-
-        Assert.Single (container1.Subviews);
-
-        var container2 = new View { Id = "Container2" };
-
-        container2.Add (view);
-        Assert.Equal (container2, view.SuperView);
-        Assert.Equal (container1.Subviews.Count, container2.Subviews.Count);
-        container2.Dispose ();
-
-        Assert.Empty (container1.Subviews);
-        Assert.Empty (container2.Subviews);
-        Assert.Equal (1, count);
-        Assert.Null (view.SuperView);
-
-        container1.Dispose ();
-
-    #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
-    #endif
-    }
-
-    [Fact]
-    public void Disposing_Event_Notify_All_Subscribers_On_The_Second_Container ()
-    {
-    #if DEBUG_IDISPOSABLE
-        // Only clear before because need to test after assert
-        Responder.Instances.Clear ();
-    #endif
-
-        var container1 = new View { Id = "Container1" };
-
-        var view = new View { Id = "View" };
-        container1.Add (view);
-        Assert.Equal (container1, view.SuperView);
-        Assert.Single (container1.Subviews);
-
-        var container2 = new View { Id = "Container2" };
-        var count = 0;
-
-        view.Disposing += View_Disposing;
-        container2.Add (view);
-        Assert.Equal (container2, view.SuperView);
-
-        void View_Disposing (object sender, EventArgs e)
-        {
-            count++;
-            Assert.Equal (view, sender);
-            container2.Remove ((View)sender);
-        }
-
-        Assert.Equal (container1.Subviews.Count, container2.Subviews.Count);
-        container1.Dispose ();
-
-        Assert.Empty (container1.Subviews);
-        Assert.Empty (container2.Subviews);
-        Assert.Equal (1, count);
-        Assert.Null (view.SuperView);
-
-        container2.Dispose ();
-
-    #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
-    #endif
-    }
-
-    [Fact]
-    [TestRespondersDisposed]
-    public void IsOverridden_False_IfNotOverridden ()
-    {
-        // MouseEvent IS defined on Responder but NOT overridden
-        Assert.False (Responder.IsOverridden (new Responder (), "OnMouseEvent"));
-
-        // MouseEvent is defined on Responder and NOT overrident on View
-        Assert.False (
-                      Responder.IsOverridden (
-                                              new View { Text = "View does not override OnMouseEvent" },
-                                              "OnMouseEvent"
-                                             )
-                     );
-
-        Assert.False (
-                      Responder.IsOverridden (
-                                              new DerivedView { Text = "DerivedView does not override OnMouseEvent" },
-                                              "OnMouseEvent"
-                                             )
-                     );
-
-        // MouseEvent is NOT defined on DerivedView 
-        Assert.False (
-                      Responder.IsOverridden (
-                                              new DerivedView { Text = "DerivedView does not override OnMouseEvent" },
-                                              "OnMouseEvent"
-                                             )
-                     );
-
-        // OnKeyDown is defined on View and NOT overrident on Button
-        Assert.False (
-                      Responder.IsOverridden (
-                                              new Button { Text = "Button does not override OnKeyDown" },
-                                              "OnKeyDown"
-                                             )
-                     );
-
-#if DEBUG_IDISPOSABLE
-
-        // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above.
-        Responder.Instances.Clear ();
-        Assert.Empty (Responder.Instances);
-#endif
-    }
-
-    [Fact]
-    [TestRespondersDisposed]
-    public void IsOverridden_True_IfOverridden ()
-    {
-        // MouseEvent is defined on Responder IS overriden on ScrollBarView (but not View)
-        Assert.True (
-                     Responder.IsOverridden (
-                                             new ScrollBarView { Text = "ScrollBarView overrides OnMouseEvent" },
-                                             "OnMouseEvent"
-                                            )
-                    );
-
-        // OnKeyDown is defined on View
-        Assert.False (Responder.IsOverridden (new View { Text = "View overrides OnKeyDown" }, "OnKeyDown"));
-
-        // OnKeyDown is defined on DerivedView
-        Assert.True (
-                     Responder.IsOverridden (
-                                             new DerivedView { Text = "DerivedView overrides OnKeyDown" },
-                                             "OnKeyDown"
-                                            )
-                    );
-
-        // ScrollBarView overrides both MouseEvent (from Responder) and Redraw (from View)
-        Assert.True (
-                     Responder.IsOverridden (
-                                             new ScrollBarView { Text = "ScrollBarView overrides OnMouseEvent" },
-                                             "OnMouseEvent"
-                                            )
-                    );
-
-        Assert.True (
-                     Responder.IsOverridden (
-                                             new ScrollBarView { Text = "ScrollBarView overrides OnDrawingContent" },
-                                             "OnDrawingContent"
-                                            )
-                    );
-#if DEBUG_IDISPOSABLE
-
-        // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above.
-        Responder.Instances.Clear ();
-        Assert.Empty (Responder.Instances);
-#endif
-    }
+   
 
     [Fact]
     public void KeyPressed_Handled_True_Cancels_KeyPress ()
@@ -215,82 +22,6 @@ public class ResponderTests
         r.Dispose ();
     }
 
-    [Fact]
-    [TestRespondersDisposed]
-    public void New_Initializes ()
-    {
-        var r = new Responder ();
-        Assert.NotNull (r);
-        Assert.Equal ("Terminal.Gui.Responder", r.ToString ());
-        r.Dispose ();
-    }
-
-    [Fact]
-    [TestRespondersDisposed]
-    public void New_Methods_Return_False ()
-    {
-        var r = new View ();
-
-        //Assert.False (r.OnKeyDown (new KeyEventArgs () { Key = Key.Unknown }));
-        Assert.False (r.NewKeyDownEvent (new Key { KeyCode = KeyCode.Null }));
-        Assert.False (r.NewKeyDownEvent (new Key { KeyCode = KeyCode.Null }));
-        Assert.False (r.NewMouseEvent (new MouseEventArgs { Flags = MouseFlags.AllEvents }));
-
-        var v = new View ();
-        //Assert.False (r.OnEnter (v));
-        v.Dispose ();
-
-        v = new View ();
-        //Assert.False (r.OnLeave (v));
-        v.Dispose ();
-
-        r.Dispose ();
-    }
-
-    [Fact]
-    public void Responder_Not_Notifying_Dispose ()
-    {
-        // Only clear before because need to test after assert
-    #if DEBUG_IDISPOSABLE
-        Responder.Instances.Clear ();
-    #endif
-        var container1 = new View { Id = "Container1" };
-
-        var view = new View { Id = "View" };
-        container1.Add (view);
-        Assert.Equal (container1, view.SuperView);
-
-        Assert.Single (container1.Subviews);
-
-        var container2 = new View { Id = "Container2" };
-
-        container2.Add (view);
-        Assert.Equal (container2, view.SuperView);
-        Assert.Equal (container1.Subviews.Count, container2.Subviews.Count);
-        container1.Dispose ();
-
-        Assert.Empty (container1.Subviews);
-        Assert.NotEmpty (container2.Subviews);
-        Assert.Single (container2.Subviews);
-        Assert.Null (view.SuperView);
-
-        // Trying access disposed properties
-    #if DEBUG_IDISPOSABLE
-        Assert.True (container2.Subviews [0].WasDisposed);
-    #endif
-        Assert.False (container2.Subviews [0].CanFocus);
-        Assert.Null (container2.Subviews [0].Margin);
-        Assert.Null (container2.Subviews [0].Border);
-        Assert.Null (container2.Subviews [0].Padding);
-        Assert.Null (view.SuperView);
-
-        container2.Dispose ();
-
-    #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
-    #endif
-    }
-
     public class DerivedView : View
     {
         protected override bool OnKeyDown (Key keyEvent) { return true; }

+ 15 - 10
UnitTests/TestHelpers.cs

@@ -86,13 +86,13 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
 
                 Application.Shutdown ();
 #if DEBUG_IDISPOSABLE
-                if (Responder.Instances.Count == 0)
+                if (View.Instances.Count == 0)
                 {
-                    Assert.Empty (Responder.Instances);
+                    Assert.Empty (View.Instances);
                 }
                 else
                 {
-                    Responder.Instances.Clear ();
+                    View.Instances.Clear ();
                 }
 #endif
             }
@@ -103,7 +103,7 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
             //finally
             {
 #if DEBUG_IDISPOSABLE
-                Responder.Instances.Clear ();
+                View.Instances.Clear ();
                 Application.ResetState (true);
 #endif
             }
@@ -126,13 +126,13 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute
 #if DEBUG_IDISPOSABLE
 
             // Clear out any lingering Responder instances from previous tests
-            if (Responder.Instances.Count == 0)
+            if (View.Instances.Count == 0)
             {
-                Assert.Empty (Responder.Instances);
+                Assert.Empty (View.Instances);
             }
             else
             {
-                Responder.Instances.Clear ();
+                View.Instances.Clear ();
             }
 #endif
             Application.Init ((ConsoleDriver)Activator.CreateInstance (_driverType));
@@ -160,7 +160,7 @@ public class TestRespondersDisposed : BeforeAfterTestAttribute
         base.After (methodUnderTest);
 
 #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
+        Assert.Empty (View.Instances);
 #endif
     }
 
@@ -172,8 +172,8 @@ public class TestRespondersDisposed : BeforeAfterTestAttribute
 #if DEBUG_IDISPOSABLE
 
         // Clear out any lingering Responder instances from previous tests
-        Responder.Instances.Clear ();
-        Assert.Empty (Responder.Instances);
+        View.Instances.Clear ();
+        Assert.Empty (View.Instances);
 #endif
     }
 }
@@ -197,6 +197,11 @@ public class SetupFakeDriverAttribute : BeforeAfterTestAttribute
         // Turn off diagnostic flags in case some test left them on
         View.Diagnostics = ViewDiagnosticFlags.Off;
 
+        if (Application.Driver is { })
+        {
+            ((FakeDriver)Application.Driver).End ();
+        }
+
         Application.Driver = null;
         base.After (methodUnderTest);
     }

+ 3 - 3
UnitTests/UICatalog/ScenarioTests.cs

@@ -10,7 +10,7 @@ public class ScenarioTests : TestsAllViews
     public ScenarioTests (ITestOutputHelper output)
     {
 #if DEBUG_IDISPOSABLE
-        Responder.Instances.Clear ();
+        View.Instances.Clear ();
 #endif
         _output = output;
     }
@@ -68,7 +68,7 @@ public class ScenarioTests : TestsAllViews
         Assert.True (shutdown);
 
 #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
+        Assert.Empty (View.Instances);
 #endif
 
         lock (_timeoutLock)
@@ -836,7 +836,7 @@ public class ScenarioTests : TestsAllViews
         ConfigurationManager.Reset ();
 
 #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
+        Assert.Empty (View.Instances);
 #endif
     }
 

+ 3 - 5
UnitTests/View/Draw/DrawTests.cs

@@ -10,7 +10,7 @@ public class DrawTests (ITestOutputHelper _output)
 
     [Fact]
     [SetupFakeDriver]
-    public void Move_Is_Constrained_To_Viewport ()
+    public void Move_Is_Not_Constrained_To_Viewport ()
     {
         var view = new View
         {
@@ -20,16 +20,14 @@ public class DrawTests (ITestOutputHelper _output)
         };
         view.Margin!.Thickness = new (1);
 
-        // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen)
-
         view.Move (0, 0);
         Assert.Equal (new (2, 2), new Point (Application.Driver!.Col, Application.Driver!.Row));
 
         view.Move (-1, -1);
-        Assert.Equal (new (2, 2), new Point (Application.Driver!.Col, Application.Driver!.Row));
+        Assert.Equal (new (1, 1), new Point (Application.Driver!.Col, Application.Driver!.Row));
 
         view.Move (1, 1);
-        Assert.Equal (new (2, 2), new Point (Application.Driver!.Col, Application.Driver!.Row));
+        Assert.Equal (new (3, 3), new Point (Application.Driver!.Col, Application.Driver!.Row));
     }
 
     [Fact]

+ 2 - 2
UnitTests/View/Layout/Dim.Tests.cs

@@ -470,8 +470,8 @@ public class DimTests
 #if DEBUG_IDISPOSABLE
 
         // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above.
-        Responder.Instances.Clear ();
-        Assert.Empty (Responder.Instances);
+        View.Instances.Clear ();
+        Assert.Empty (View.Instances);
 #endif
     }
 

+ 51 - 0
UnitTests/View/Layout/FrameTests.cs

@@ -259,4 +259,55 @@ public class FrameTests (ITestOutputHelper output)
         Assert.Equal (Dim.Absolute (40), v.Height);
         v.Dispose ();
     }
+
+    private class TestFrameEventsView : View
+    {
+        public int OnFrameChangedCallCount { get; private set; }
+        public int FrameChangedEventCallCount { get; private set; }
+
+        public TestFrameEventsView ()
+        {
+            FrameChanged += (sender, args) => FrameChangedEventCallCount++;
+        }
+
+        protected override void OnFrameChanged (in Rectangle frame)
+        {
+            OnFrameChangedCallCount++;
+            base.OnFrameChanged (frame);
+        }
+    }
+
+    [Fact]
+    public void OnFrameChanged_Called_When_Frame_Changes ()
+    {
+        // Arrange
+        var view = new TestFrameEventsView ();
+        var initialFrame = new Rectangle (0, 0, 10, 10);
+        var newFrame = new Rectangle (0, 0, 20, 20);
+        view.Frame = initialFrame;
+        Assert.Equal (1, view.OnFrameChangedCallCount);
+
+        // Act
+        view.Frame = newFrame;
+
+        // Assert
+        Assert.Equal (2, view.OnFrameChangedCallCount);
+    }
+
+    [Fact]
+    public void FrameChanged_Event_Raised_When_Frame_Changes ()
+    {
+        // Arrange
+        var view = new TestFrameEventsView ();
+        var initialFrame = new Rectangle (0, 0, 10, 10);
+        var newFrame = new Rectangle (0, 0, 20, 20);
+        view.Frame = initialFrame;
+        Assert.Equal (1, view.FrameChangedEventCallCount);
+
+        // Act
+        view.Frame = newFrame;
+
+        // Assert
+        Assert.Equal (2, view.FrameChangedEventCallCount);
+    }
 }

+ 1 - 1
UnitTests/View/Layout/Pos.ViewTests.cs

@@ -249,7 +249,7 @@ public class PosViewTests (ITestOutputHelper output)
 #if DEBUG_IDISPOSABLE
 
         // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above.
-        Responder.Instances.Clear ();
+        View.Instances.Clear ();
 #endif
     }
 

+ 0 - 2
UnitTests/View/Layout/SetLayoutTests.cs

@@ -812,6 +812,4 @@ public class SetLayoutTests (ITestOutputHelper output)
         Assert.Equal (19, v2.Frame.Height);
         t.Dispose ();
     }
-
-
 }

+ 87 - 0
UnitTests/View/Layout/ViewportTests.cs

@@ -387,6 +387,93 @@ public class ViewportTests (ITestOutputHelper output)
         Assert.NotEqual (view.Viewport.Size, view.GetContentSize ());
     }
 
+    private class TestViewportEventsView : View
+    {
+        public int OnViewportChangedCallCount { get; private set; }
+        public int ViewportChangedEventCallCount { get; private set; }
+
+        public TestViewportEventsView ()
+        {
+            ViewportChanged += (sender, args) => ViewportChangedEventCallCount++;
+        }
+
+        protected override void OnViewportChanged (DrawEventArgs e)
+        {
+            OnViewportChangedCallCount++;
+            base.OnViewportChanged (e);
+        }
+    }
+
+    [Fact]
+    public void OnViewportChanged_Called_When_Viewport_Changes ()
+    {
+        // Arrange
+        var view = new TestViewportEventsView ();
+        var initialViewport = new Rectangle (0, 0, 10, 10);
+        var newViewport = new Rectangle (0, 0, 20, 20);
+        Assert.Equal (0, view.OnViewportChangedCallCount);
+        view.Viewport = initialViewport;
+        Assert.Equal (1, view.OnViewportChangedCallCount);
+
+        // Act
+        view.Viewport = newViewport;
+
+        // Assert
+        Assert.Equal (2, view.OnViewportChangedCallCount);
+    }
+
+    [Fact]
+    public void ViewportChanged_Event_Raised_When_Viewport_Changes ()
+    {
+        // Arrange
+        var view = new TestViewportEventsView ();
+        var initialViewport = new Rectangle (0, 0, 10, 10);
+        var newViewport = new Rectangle (0, 0, 20, 20);
+        view.Viewport = initialViewport;
+        Assert.Equal (1, view.ViewportChangedEventCallCount);
+
+        // Act
+        view.Viewport = newViewport;
+
+        // Assert
+        Assert.Equal (2, view.ViewportChangedEventCallCount);
+    }
+
+    [Fact]
+    public void OnViewportChanged_Called_When_Frame_Changes ()
+    {
+        // Arrange
+        var view = new TestViewportEventsView ();
+        var initialFrame = new Rectangle (0, 0, 10, 10);
+        var newFrame = new Rectangle (0, 0, 20, 20);
+        Assert.Equal (0, view.OnViewportChangedCallCount);
+        view.Frame = initialFrame;
+        Assert.Equal (1, view.OnViewportChangedCallCount);
+
+        // Act
+        view.Frame = newFrame;
+
+        // Assert
+        Assert.Equal (2, view.OnViewportChangedCallCount);
+    }
+
+    [Fact]
+    public void ViewportChanged_Event_Raised_When_Frame_Changes ()
+    {
+        // Arrange
+        var view = new TestViewportEventsView ();
+        var initialFrame = new Rectangle (0, 0, 10, 10);
+        var newFrame = new Rectangle (0, 0, 20, 20);
+        view.Frame = initialFrame;
+        Assert.Equal (1, view.ViewportChangedEventCallCount);
+
+        // Act
+        view.Frame = newFrame;
+
+        // Assert
+        Assert.Equal (2, view.ViewportChangedEventCallCount);
+    }
+
     //[Theory]
     //[InlineData (0, 0, true)]
     //[InlineData (-1, 0, true)]

+ 18 - 18
UnitTests/View/Orientation/OrientationHelperTests.cs

@@ -10,18 +10,18 @@ public class OrientationHelperTests
         // Arrange
         Mock<IOrientation> mockIOrientation = new Mock<IOrientation> ();
         var orientationHelper = new OrientationHelper (mockIOrientation.Object);
-        var changingEventInvoked = false;
-        var changedEventInvoked = false;
+        var changingEventInvoked = 0;
+        var changedEventInvoked = 0;
 
-        orientationHelper.OrientationChanging += (sender, e) => { changingEventInvoked = true; };
-        orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked = true; };
+        orientationHelper.OrientationChanging += (sender, e) => { changingEventInvoked++; };
+        orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked++; };
 
         // Act
         orientationHelper.Orientation = Orientation.Vertical;
 
         // Assert
-        Assert.True (changingEventInvoked, "OrientationChanging event was not invoked.");
-        Assert.True (changedEventInvoked, "OrientationChanged event was not invoked.");
+        Assert.Equal (1, changingEventInvoked);
+        Assert.Equal(1, changedEventInvoked);
     }
 
     [Fact]
@@ -29,15 +29,15 @@ public class OrientationHelperTests
     {
         // Arrange
         Mock<IOrientation> mockIOrientation = new Mock<IOrientation> ();
-        var onChangingOverrideCalled = false;
-        var onChangedOverrideCalled = false;
+        var onChangingOverrideCalled = 0;
+        var onChangedOverrideCalled = 0;
 
         mockIOrientation.Setup (x => x.OnOrientationChanging (It.IsAny<Orientation> (), It.IsAny<Orientation> ()))
-                        .Callback (() => onChangingOverrideCalled = true)
+                        .Callback (() => onChangingOverrideCalled++)
                         .Returns (false); // Ensure it doesn't cancel the change
 
         mockIOrientation.Setup (x => x.OnOrientationChanged (It.IsAny<Orientation> ()))
-                        .Callback (() => onChangedOverrideCalled = true);
+                        .Callback (() => onChangedOverrideCalled++);
 
         var orientationHelper = new OrientationHelper (mockIOrientation.Object);
 
@@ -45,8 +45,8 @@ public class OrientationHelperTests
         orientationHelper.Orientation = Orientation.Vertical;
 
         // Assert
-        Assert.True (onChangingOverrideCalled, "OnOrientationChanging override was not called.");
-        Assert.True (onChangedOverrideCalled, "OnOrientationChanged override was not called.");
+        Assert.Equal (1, onChangingOverrideCalled);
+        Assert.Equal (1, onChangedOverrideCalled);
     }
 
     [Fact]
@@ -56,18 +56,18 @@ public class OrientationHelperTests
         Mock<IOrientation> mockIOrientation = new Mock<IOrientation> ();
         var orientationHelper = new OrientationHelper (mockIOrientation.Object);
         orientationHelper.Orientation = Orientation.Horizontal; // Set initial orientation
-        var changingEventInvoked = false;
-        var changedEventInvoked = false;
+        var changingEventInvoked = 0;
+        var changedEventInvoked = 0;
 
-        orientationHelper.OrientationChanging += (sender, e) => { changingEventInvoked = true; };
-        orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked = true; };
+        orientationHelper.OrientationChanging += (sender, e) => { changingEventInvoked++; };
+        orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked++; };
 
         // Act
         orientationHelper.Orientation = Orientation.Horizontal; // Set to the same value
 
         // Assert
-        Assert.False (changingEventInvoked, "OrientationChanging event was invoked.");
-        Assert.False (changedEventInvoked, "OrientationChanged event was invoked.");
+        Assert.Equal (0, changingEventInvoked);
+        Assert.Equal (0, changedEventInvoked);
     }
 
     [Fact]

+ 151 - 2
UnitTests/View/ViewTests.cs

@@ -6,6 +6,155 @@ namespace Terminal.Gui.ViewTests;
 
 public class ViewTests (ITestOutputHelper output)
 {
+    // Generic lifetime (IDisposable) tests
+    [Fact]
+    [TestRespondersDisposed]
+    public void Dispose_Works ()
+    {
+        var r = new View ();
+#if DEBUG_IDISPOSABL
+        Assert.Equals (3, View.Instances.Count);
+#endif
+
+        r.Dispose ();
+#if DEBUG_IDISPOSABLE
+        Assert.Empty (View.Instances);
+#endif
+    }
+
+    [Fact]
+    public void Disposing_Event_Notify_All_Subscribers_On_The_First_Container ()
+    {
+#if DEBUG_IDISPOSABLE
+        // Only clear before because need to test after assert
+        View.Instances.Clear ();
+#endif
+
+        var container1 = new View { Id = "Container1" };
+        var count = 0;
+
+        var view = new View { Id = "View" };
+        view.Disposing += View_Disposing;
+        container1.Add (view);
+        Assert.Equal (container1, view.SuperView);
+
+        void View_Disposing (object sender, EventArgs e)
+        {
+            count++;
+            Assert.Equal (view, sender);
+            container1.Remove ((View)sender);
+        }
+
+        Assert.Single (container1.Subviews);
+
+        var container2 = new View { Id = "Container2" };
+
+        container2.Add (view);
+        Assert.Equal (container2, view.SuperView);
+        Assert.Equal (container1.Subviews.Count, container2.Subviews.Count);
+        container2.Dispose ();
+
+        Assert.Empty (container1.Subviews);
+        Assert.Empty (container2.Subviews);
+        Assert.Equal (1, count);
+        Assert.Null (view.SuperView);
+
+        container1.Dispose ();
+
+#if DEBUG_IDISPOSABLE
+        Assert.Empty (View.Instances);
+#endif
+    }
+
+    [Fact]
+    public void Disposing_Event_Notify_All_Subscribers_On_The_Second_Container ()
+    {
+#if DEBUG_IDISPOSABLE
+        // Only clear before because need to test after assert
+        View.Instances.Clear ();
+#endif
+
+        var container1 = new View { Id = "Container1" };
+
+        var view = new View { Id = "View" };
+        container1.Add (view);
+        Assert.Equal (container1, view.SuperView);
+        Assert.Single (container1.Subviews);
+
+        var container2 = new View { Id = "Container2" };
+        var count = 0;
+
+        view.Disposing += View_Disposing;
+        container2.Add (view);
+        Assert.Equal (container2, view.SuperView);
+
+        void View_Disposing (object sender, EventArgs e)
+        {
+            count++;
+            Assert.Equal (view, sender);
+            container2.Remove ((View)sender);
+        }
+
+        Assert.Equal (container1.Subviews.Count, container2.Subviews.Count);
+        container1.Dispose ();
+
+        Assert.Empty (container1.Subviews);
+        Assert.Empty (container2.Subviews);
+        Assert.Equal (1, count);
+        Assert.Null (view.SuperView);
+
+        container2.Dispose ();
+
+#if DEBUG_IDISPOSABLE
+        Assert.Empty (View.Instances);
+#endif
+    }
+
+
+    [Fact]
+    public void Not_Notifying_Dispose ()
+    {
+        // Only clear before because need to test after assert
+#if DEBUG_IDISPOSABLE
+        View.Instances.Clear ();
+#endif
+        var container1 = new View { Id = "Container1" };
+
+        var view = new View { Id = "View" };
+        container1.Add (view);
+        Assert.Equal (container1, view.SuperView);
+
+        Assert.Single (container1.Subviews);
+
+        var container2 = new View { Id = "Container2" };
+
+        container2.Add (view);
+        Assert.Equal (container2, view.SuperView);
+        Assert.Equal (container1.Subviews.Count, container2.Subviews.Count);
+        container1.Dispose ();
+
+        Assert.Empty (container1.Subviews);
+        Assert.NotEmpty (container2.Subviews);
+        Assert.Single (container2.Subviews);
+        Assert.Null (view.SuperView);
+
+        // Trying access disposed properties
+#if DEBUG_IDISPOSABLE
+        Assert.True (container2.Subviews [0].WasDisposed);
+#endif
+        Assert.False (container2.Subviews [0].CanFocus);
+        Assert.Null (container2.Subviews [0].Margin);
+        Assert.Null (container2.Subviews [0].Border);
+        Assert.Null (container2.Subviews [0].Padding);
+        Assert.Null (view.SuperView);
+
+        container2.Dispose ();
+
+#if DEBUG_IDISPOSABLE
+        Assert.Empty (View.Instances);
+#endif
+    }
+
     [Fact]
     [AutoInitShutdown]
     public void Clear_Viewport_Can_Use_Driver_AddRune_Or_AddStr_Methods ()
@@ -428,7 +577,7 @@ At 0,0
         Assert.NotNull (view.Padding);
 
 #if DEBUG_IDISPOSABLE
-        Assert.Equal (4, Responder.Instances.Count);
+        Assert.Equal (4, View.Instances.Count);
 #endif
 
         view.Dispose ();
@@ -948,7 +1097,7 @@ At 0,0
         super.Dispose ();
 
 #if DEBUG_IDISPOSABLE
-        Assert.Empty (Responder.Instances);
+        Assert.Empty (View.Instances);
 #endif
 
         // Default Constructor

+ 12 - 38
UnitTests/Views/HexViewTests.cs

@@ -156,24 +156,24 @@ public class HexViewTests
         Assert.Equal (63, hv.Source!.Length);
         Assert.Equal (20, hv.BytesPerLine);
 
-        Assert.Equal (new (0, 0), hv.Position);
+        Assert.Equal (new (0, 0), hv.GetPosition (hv.Address));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.Tab));
-        Assert.Equal (new (0, 0), hv.Position);
+        Assert.Equal (new (0, 0), hv.GetPosition (hv.Address));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl));
-        Assert.Equal (hv.BytesPerLine - 1, hv.Position.X);
+        Assert.Equal (hv.BytesPerLine - 1, hv.GetPosition (hv.Address).X);
 
         Assert.True (Application.RaiseKeyDownEvent (Key.Home));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight));
-        Assert.Equal (new (1, 0), hv.Position);
+        Assert.Equal (new (1, 0), hv.GetPosition (hv.Address));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown));
-        Assert.Equal (new (1, 1), hv.Position);
+        Assert.Equal (new (1, 1), hv.GetPosition (hv.Address));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.End));
-        Assert.Equal (new (3, 3), hv.Position);
+        Assert.Equal (new (3, 3), hv.GetPosition (hv.Address));
 
         Assert.Equal (hv.Source!.Length, hv.Address);
         Application.Top.Dispose ();
@@ -194,23 +194,23 @@ public class HexViewTests
         Assert.Equal (126, hv.Source!.Length);
         Assert.Equal (20, hv.BytesPerLine);
 
-        Assert.Equal (new (0, 0), hv.Position);
+        Assert.Equal (new (0, 0), hv.GetPosition (hv.Address));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.Tab));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight.WithCtrl));
-        Assert.Equal (hv.BytesPerLine - 1, hv.Position.X);
+        Assert.Equal (hv.BytesPerLine - 1, hv.GetPosition (hv.Address).X);
 
         Assert.True (Application.RaiseKeyDownEvent (Key.Home));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight));
-        Assert.Equal (new (1, 0), hv.Position);
+        Assert.Equal (new (1, 0), hv.GetPosition (hv.Address));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown));
-        Assert.Equal (new (1, 1), hv.Position);
+        Assert.Equal (new (1, 1), hv.GetPosition (hv.Address));
 
         Assert.True (Application.RaiseKeyDownEvent (Key.End));
-        Assert.Equal (new (6, 6), hv.Position);
+        Assert.Equal (new (6, 6), hv.GetPosition (hv.Address));
 
         Assert.Equal (hv.Source!.Length, hv.Address);
         Application.Top.Dispose ();
@@ -236,27 +236,6 @@ public class HexViewTests
         Assert.Empty (hv.Edits);
     }
 
-    [Fact]
-    public void DisplayStart_Source ()
-    {
-        var hv = new HexView (LoadStream (null, out _, true)) { Width = 20, Height = 20 };
-
-        // Needed because HexView relies on LayoutComplete to calc sizes
-        hv.LayoutSubviews ();
-
-        Assert.Equal (0, hv.DisplayStart);
-
-        Assert.True (hv.NewKeyDownEvent (Key.PageDown));
-        Assert.Equal (4 * hv.Frame.Height, hv.DisplayStart);
-        Assert.Equal (hv.Source!.Length, hv.Source.Position);
-
-        Assert.True (hv.NewKeyDownEvent (Key.End));
-
-        // already on last page and so the DisplayStart is the same as before
-        Assert.Equal (4 * hv.Frame.Height, hv.DisplayStart);
-        Assert.Equal (hv.Source.Length, hv.Source.Position);
-    }
-
     [Fact]
     public void Edited_Event ()
     {
@@ -365,7 +344,7 @@ public class HexViewTests
     }
 
     [Fact]
-    public void Source_Sets_DisplayStart_And_Position_To_Zero_If_Greater_Than_Source_Length ()
+    public void Source_Sets_Address_To_Zero_If_Greater_Than_Source_Length ()
     {
         var hv = new HexView (LoadStream (null, out _)) { Width = 10, Height = 5 };
         Application.Top = new Toplevel ();
@@ -374,28 +353,23 @@ public class HexViewTests
         Application.Top.Layout ();
 
         Assert.True (hv.NewKeyDownEvent (Key.End));
-        Assert.Equal (MEM_STRING_LENGTH - 1, hv.DisplayStart);
         Assert.Equal (MEM_STRING_LENGTH, hv.Address);
 
         hv.Source = new MemoryStream ();
         Application.Top.Layout ();
-        Assert.Equal (0, hv.DisplayStart);
         Assert.Equal (0, hv.Address);
 
         hv.Source = LoadStream (null, out _);
         hv.Width = Dim.Fill ();
         hv.Height = Dim.Fill ();
         Application.Top.Layout ();
-        Assert.Equal (0, hv.DisplayStart);
         Assert.Equal (0, hv.Address);
 
         Assert.True (hv.NewKeyDownEvent (Key.End));
-        Assert.Equal (0, hv.DisplayStart);
         Assert.Equal (MEM_STRING_LENGTH, hv.Address);
 
         hv.Source = new MemoryStream ();
         Application.Top.Layout ();
-        Assert.Equal (0, hv.DisplayStart);
         Assert.Equal (0, hv.Address);
 
         Application.Top.Dispose ();

+ 2 - 2
UnitTests/Views/ListViewTests.cs

@@ -790,10 +790,10 @@ Item 6",
         var lv = new ListView
         {
             X = 1,
-            Width = 10,
-            Height = 5,
             Source = new ListWrapper<string> (source)
         };
+        lv.Height = lv.Source.Count;
+        lv.Width = lv.MaxLength;
         var top = new Toplevel ();
         top.Add (lv);
         Application.Begin (top);

+ 943 - 0
UnitTests/Views/ScrollBarTests.cs

@@ -0,0 +1,943 @@
+using Xunit.Abstractions;
+using static Unix.Terminal.Delegates;
+
+namespace Terminal.Gui.ViewsTests;
+
+public class ScrollBarTests (ITestOutputHelper output)
+{
+    [Fact]
+    public void Constructor_Defaults ()
+    {
+        var scrollBar = new ScrollBar ();
+        Assert.False (scrollBar.CanFocus);
+        Assert.Equal (Orientation.Vertical, scrollBar.Orientation);
+        Assert.Equal (0, scrollBar.ScrollableContentSize);
+        Assert.Equal (0, scrollBar.VisibleContentSize);
+        Assert.Equal (0, scrollBar.GetSliderPosition ());
+        Assert.Equal (0, scrollBar.Position);
+        Assert.False (scrollBar.AutoShow);
+    }
+
+    #region AutoHide
+    [Fact]
+    [AutoInitShutdown]
+    public void AutoHide_False_Is_Default_CorrectlyHidesAndShows ()
+    {
+        var super = new Toplevel ()
+        {
+            Id = "super",
+            Width = 1,
+            Height = 20
+        };
+
+        var scrollBar = new ScrollBar
+        {
+        };
+        super.Add (scrollBar);
+        Assert.False (scrollBar.AutoShow);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.AutoShow = true;
+        Assert.True (scrollBar.AutoShow);
+        Assert.True (scrollBar.Visible);
+
+        RunState rs = Application.Begin (super);
+
+        // Should Show
+        scrollBar.ScrollableContentSize = 21;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        // Should Hide
+        scrollBar.ScrollableContentSize = 10;
+        Assert.False (scrollBar.Visible);
+
+        super.Dispose ();
+    }
+
+    [Fact]
+    [AutoInitShutdown]
+    public void AutoHide_False_CorrectlyHidesAndShows ()
+    {
+        var super = new Toplevel ()
+        {
+            Id = "super",
+            Width = 1,
+            Height = 20
+        };
+
+        var scrollBar = new ScrollBar
+        {
+            ScrollableContentSize = 20,
+            AutoShow = false
+        };
+        super.Add (scrollBar);
+        Assert.False (scrollBar.AutoShow);
+        Assert.True (scrollBar.Visible);
+
+        RunState rs = Application.Begin (super);
+
+        // Should Hide if AutoSize = true, but should not hide if AutoSize = false
+        scrollBar.ScrollableContentSize = 10;
+        Assert.True (scrollBar.Visible);
+
+        super.Dispose ();
+    }
+
+    [Fact]
+    [AutoInitShutdown]
+    public void AutoHide_True_Changing_ScrollableContentSize_CorrectlyHidesAndShows ()
+    {
+        var super = new Toplevel ()
+        {
+            Id = "super",
+            Width = 1,
+            Height = 20
+        };
+
+        var scrollBar = new ScrollBar
+        {
+            ScrollableContentSize = 20,
+        };
+        super.Add (scrollBar);
+        Assert.False (scrollBar.AutoShow);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.AutoShow = true;
+
+        RunState rs = Application.Begin (super);
+
+        Assert.False (scrollBar.Visible);
+        Assert.Equal (1, scrollBar.Frame.Width);
+        Assert.Equal (20, scrollBar.Frame.Height);
+
+        scrollBar.ScrollableContentSize = 10;
+        Application.RunIteration (ref rs);
+        Assert.False (scrollBar.Visible);
+
+        scrollBar.ScrollableContentSize = 30;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.AutoShow = false;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.ScrollableContentSize = 10;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        super.Dispose ();
+    }
+
+    [Fact]
+    [AutoInitShutdown]
+    public void AutoHide_Change_VisibleContentSize_CorrectlyHidesAndShows ()
+    {
+        var super = new Toplevel ()
+        {
+            Id = "super",
+            Width = 1,
+            Height = 20
+        };
+
+        var scrollBar = new ScrollBar
+        {
+            ScrollableContentSize = 20,
+            VisibleContentSize = 20
+        };
+        super.Add (scrollBar);
+        Assert.False (scrollBar.AutoShow);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.AutoShow = true;
+
+        RunState rs = Application.Begin (super);
+
+        Assert.Equal (Orientation.Vertical, scrollBar.Orientation);
+        Assert.Equal (20, scrollBar.VisibleContentSize);
+        Assert.False (scrollBar.Visible);
+
+        scrollBar.VisibleContentSize = 10;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.VisibleContentSize = 30;
+        Application.RunIteration (ref rs);
+        Assert.False (scrollBar.Visible);
+
+        scrollBar.VisibleContentSize = 10;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.VisibleContentSize = 21;
+        Application.RunIteration (ref rs);
+        Assert.False (scrollBar.Visible);
+
+        scrollBar.AutoShow = false;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        scrollBar.VisibleContentSize = 10;
+        Application.RunIteration (ref rs);
+        Assert.True (scrollBar.Visible);
+
+        super.Dispose ();
+    }
+
+    #endregion AutoHide
+
+    #region Orientation
+    [Fact]
+    public void OnOrientationChanged_Keeps_Size ()
+    {
+        var scroll = new ScrollBar ();
+        scroll.Layout ();
+        scroll.ScrollableContentSize = 1;
+
+        scroll.Orientation = Orientation.Horizontal;
+        Assert.Equal (1, scroll.ScrollableContentSize);
+    }
+
+    [Fact]
+    public void OnOrientationChanged_Sets_Position_To_0 ()
+    {
+        View super = new View ()
+        {
+            Id = "super",
+            Width = 10,
+            Height = 10
+        };
+        var scrollBar = new ScrollBar ()
+        {
+        };
+        super.Add (scrollBar);
+        scrollBar.Layout ();
+        scrollBar.Position = 1;
+        scrollBar.Orientation = Orientation.Horizontal;
+
+        Assert.Equal (0, scrollBar.GetSliderPosition ());
+    }
+
+    #endregion Orientation
+
+
+    #region Size
+
+    // TODO: Add tests.
+
+    #endregion Size
+
+    #region Position
+    [Fact]
+    public void Position_Event_Cancelables ()
+    {
+        var changingCount = 0;
+        var changedCount = 0;
+        var scrollBar = new ScrollBar { };
+        scrollBar.ScrollableContentSize = 5;
+        scrollBar.Frame = new Rectangle (0, 0, 1, 4); // Needs to be at least 4 for slider to move
+
+        scrollBar.PositionChanging += (s, e) =>
+                                            {
+                                                if (changingCount == 0)
+                                                {
+                                                    e.Cancel = true;
+                                                }
+
+                                                changingCount++;
+                                            };
+        scrollBar.PositionChanged += (s, e) => changedCount++;
+
+        scrollBar.Position = 1;
+        Assert.Equal (0, scrollBar.Position);
+        Assert.Equal (1, changingCount);
+        Assert.Equal (0, changedCount);
+
+        scrollBar.Position = 1;
+        Assert.Equal (1, scrollBar.Position);
+        Assert.Equal (2, changingCount);
+        Assert.Equal (1, changedCount);
+    }
+    #endregion Position
+
+
+    [Fact]
+    public void ScrollableContentSize_Cannot_Be_Negative ()
+    {
+        var scrollBar = new ScrollBar { Height = 10, ScrollableContentSize = -1 };
+        Assert.Equal (0, scrollBar.ScrollableContentSize);
+        scrollBar.ScrollableContentSize = -10;
+        Assert.Equal (0, scrollBar.ScrollableContentSize);
+    }
+
+    [Fact]
+    public void ScrollableContentSizeChanged_Event ()
+    {
+        var count = 0;
+        var scrollBar = new ScrollBar ();
+        scrollBar.ScrollableContentSizeChanged += (s, e) => count++;
+
+        scrollBar.ScrollableContentSize = 10;
+        Assert.Equal (10, scrollBar.ScrollableContentSize);
+        Assert.Equal (1, count);
+    }
+
+    [Theory]
+    [SetupFakeDriver]
+
+    #region Draw
+
+
+    #region Horizontal
+
+    #region Super 10 - ScrollBar 8
+    [InlineData (
+                    10,
+                    1,
+                    10,
+                    -1,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄████████►│
+└──────────┘")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    -1,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄████░░░░►│
+└──────────┘")]
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    0,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄████░░░░►│
+└──────────┘")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    1,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄████░░░░►│
+└──────────┘")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    2,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░████░░░►│
+└──────────┘
+")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    3,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░████░░░►│
+└──────────┘
+")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    4,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░████░░►│
+└──────────┘
+")]
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    5,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░████░░►│
+└──────────┘
+")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    6,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░████░░►│
+└──────────┘
+")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    7,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░░████░►│
+└──────────┘
+")]
+
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    8,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░░████░►│
+└──────────┘
+")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    9,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░░░████►│
+└──────────┘
+")]
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    10,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░░░████►│
+└──────────┘
+")]
+
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    19,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░░░████►│
+└──────────┘
+")]
+
+
+    [InlineData (
+                    10,
+                    1,
+                    20,
+                    20,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│◄░░░░████►│
+└──────────┘
+")]
+    #endregion  Super 10 - ScrollBar 8
+
+    #region  Super 12 - ScrollBar 10
+    [InlineData (
+                    12,
+                    1,
+                    10,
+                    -1,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄██████████►│
+└────────────┘")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    -1,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄██████░░░░►│
+└────────────┘")]
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    0,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄██████░░░░►│
+└────────────┘")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    1,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄██████░░░░►│
+└────────────┘")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    2,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░██████░░░►│
+└────────────┘
+")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    3,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░██████░░►│
+└────────────┘
+")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    4,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░██████░░►│
+└────────────┘
+")]
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    5,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░██████░░►│
+└────────────┘
+")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    6,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░░██████░►│
+└────────────┘
+")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    7,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░░░██████►│
+└────────────┘
+")]
+
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    8,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░░░██████►│
+└────────────┘
+")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    9,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░░░██████►│
+└────────────┘
+")]
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    10,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░░░██████►│
+└────────────┘
+")]
+
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    19,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░░░██████►│
+└────────────┘
+")]
+
+
+    [InlineData (
+                    12,
+                    1,
+                    20,
+                    20,
+                    Orientation.Horizontal,
+                    @"
+┌────────────┐
+│◄░░░░██████►│
+└────────────┘
+")]
+    #endregion Super 12 - ScrollBar 10
+    [InlineData (
+                    10,
+                    3,
+                    20,
+                    2,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│ ░████░░░ │
+│◄░████░░░►│
+│ ░████░░░ │
+└──────────┘
+")]
+    #endregion Horizontal
+
+    #region Vertical
+
+    [InlineData (
+                    1,
+                    10,
+                    10,
+                    -1,
+                    Orientation.Vertical,
+                    @"
+┌─┐
+│▲│
+│█│
+│█│
+│█│
+│█│
+│█│
+│█│
+│█│
+│█│
+│▼│
+└─┘")]
+
+    [InlineData (
+                    1,
+                    10,
+                    10,
+                    5,
+                    Orientation.Vertical,
+                    @"
+┌─┐
+│▲│
+│█│
+│█│
+│█│
+│█│
+│█│
+│█│
+│█│
+│█│
+│▼│
+└─┘")]
+
+    [InlineData (
+                    1,
+                    10,
+                    20,
+                    5,
+                    Orientation.Vertical,
+                    @"
+┌─┐
+│▲│
+│░│
+│░│
+│█│
+│█│
+│█│
+│█│
+│░│
+│░│
+│▼│
+└─┘")]
+
+    [InlineData (
+                    1,
+                    12,
+                    20,
+                    5,
+                    Orientation.Vertical,
+                    @"
+┌─┐
+│▲│
+│░│
+│░│
+│█│
+│█│
+│█│
+│█│
+│█│
+│█│
+│░│
+│░│
+│▼│
+└─┘")]
+
+    [InlineData (
+                    3,
+                    10,
+                    20,
+                    2,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│ ▲ │
+│░░░│
+│███│
+│███│
+│███│
+│███│
+│░░░│
+│░░░│
+│░░░│
+│ ▼ │
+└───┘
+")]
+    #endregion Vertical
+
+
+    public void Draws_Correctly_Default_Settings (int width, int height, int contentSize, int contentPosition, Orientation orientation, string expected)
+    {
+        var super = new Window
+        {
+            Id = "super",
+            Width = width + 2,
+            Height = height + 2,
+        };
+
+        var scrollBar = new ScrollBar
+        {
+            AutoShow = false,
+            Orientation = orientation,
+        };
+
+        if (orientation == Orientation.Vertical)
+        {
+            super.SetContentSize (new (width, contentSize));
+            scrollBar.Width = width;
+        }
+        else
+        {
+            super.SetContentSize (new (contentSize, height));
+            scrollBar.Height = height;
+        }
+        super.Add (scrollBar);
+
+        scrollBar.Position = contentPosition;
+
+        super.Layout ();
+        super.Draw ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+    }
+    #endregion Draw
+
+    #region Mouse
+
+
+
+    [Theory]
+    [CombinatorialData]
+    [AutoInitShutdown]
+    public void Mouse_Click_DecrementButton_Decrements ([CombinatorialRange (1, 3, 1)] int increment, Orientation orientation)
+    {
+        var top = new Toplevel ()
+        {
+            Id = "top",
+            Width = 10,
+            Height = 10
+        };
+        var scrollBar = new ScrollBar
+        {
+            Id = "scrollBar",
+            Orientation = orientation,
+            ScrollableContentSize = 20,
+            Increment = increment
+        };
+
+        top.Add (scrollBar);
+        RunState rs = Application.Begin (top);
+
+        // Scroll to end
+        scrollBar.Position = 19;
+        Assert.Equal (10, scrollBar.Position);
+        Application.RunIteration (ref rs);
+
+        Assert.Equal (4, scrollBar.GetSliderPosition ());
+        Assert.Equal (10, scrollBar.Position);
+        int initialPos = scrollBar.Position;
+
+        Point btnPoint = orientation == Orientation.Vertical
+                             ? new (0, 0)
+                             : new (0, 0);
+
+        Application.RaiseMouseEvent (new ()
+        {
+            ScreenPosition = btnPoint,
+            Flags = MouseFlags.Button1Clicked
+        });
+
+        Application.RunIteration (ref rs);
+
+        Assert.Equal (initialPos - increment, scrollBar.Position);
+
+        Application.ResetState (true);
+    }
+
+
+    [Theory]
+    [CombinatorialData]
+    [AutoInitShutdown]
+    public void Mouse_Click_IncrementButton_Increments ([CombinatorialRange (1, 3, 1)] int increment, Orientation orientation)
+    {
+        var top = new Toplevel ()
+        {
+            Id = "top",
+            Width = 10,
+            Height = 10
+        };
+        var scrollBar = new ScrollBar
+        {
+            Id = "scrollBar",
+            Orientation = orientation,
+            ScrollableContentSize = 20,
+            Increment = increment
+        };
+
+        top.Add (scrollBar);
+        RunState rs = Application.Begin (top);
+
+        // Scroll to top
+        scrollBar.Position = 0;
+        Application.RunIteration (ref rs);
+
+        Assert.Equal (0, scrollBar.GetSliderPosition ());
+        Assert.Equal (0, scrollBar.Position);
+        int initialPos = scrollBar.Position;
+
+        Point btnPoint = orientation == Orientation.Vertical
+                             ? new (scrollBar.Frame.X, scrollBar.Frame.Height - 1)
+                             : new (scrollBar.Frame.Width - 1, scrollBar.Frame.Y);
+
+        Application.RaiseMouseEvent (new ()
+        {
+            ScreenPosition = btnPoint,
+            Flags = MouseFlags.Button1Clicked
+        });
+        Application.RunIteration (ref rs);
+
+        Assert.Equal (initialPos + increment, scrollBar.Position);
+
+        Application.ResetState (true);
+    }
+    #endregion Mouse
+
+
+
+    [Fact (Skip = "Disabled - Will put this feature in View")]
+    [AutoInitShutdown]
+    public void KeepContentInAllViewport_True_False ()
+    {
+        var view = new View { Width = Dim.Fill (), Height = Dim.Fill () };
+        view.Padding.Thickness = new (0, 0, 2, 0);
+        view.SetContentSize (new (view.Viewport.Width, 30));
+        var scrollBar = new ScrollBar { Width = 2, Height = Dim.Fill (), ScrollableContentSize = view.GetContentSize ().Height };
+        scrollBar.SliderPositionChanged += (_, e) => view.Viewport = view.Viewport with { Y = e.CurrentValue };
+        view.Padding.Add (scrollBar);
+        var top = new Toplevel ();
+        top.Add (view);
+        Application.Begin (top);
+
+        //Assert.False (scrollBar.KeepContentInAllViewport);
+        //scrollBar.KeepContentInAllViewport = true;
+        Assert.Equal (80, view.Padding.Viewport.Width);
+        Assert.Equal (25, view.Padding.Viewport.Height);
+        Assert.Equal (2, scrollBar.Viewport.Width);
+        Assert.Equal (25, scrollBar.Viewport.Height);
+        Assert.Equal (30, scrollBar.ScrollableContentSize);
+
+        //scrollBar.KeepContentInAllViewport = false;
+        scrollBar.Position = 50;
+        Assert.Equal (scrollBar.GetSliderPosition (), scrollBar.ScrollableContentSize - 1);
+        Assert.Equal (scrollBar.GetSliderPosition (), view.Viewport.Y);
+        Assert.Equal (29, scrollBar.GetSliderPosition ());
+        Assert.Equal (29, view.Viewport.Y);
+
+        top.Dispose ();
+    }
+
+}

+ 0 - 1390
UnitTests/Views/ScrollBarViewTests.cs

@@ -1,1390 +0,0 @@
-using System.Collections.ObjectModel;
-using System.Reflection;
-using Xunit.Abstractions;
-
-namespace Terminal.Gui.ViewsTests;
-
-public class ScrollBarViewTests
-{
-    private static HostView _hostView;
-    private readonly ITestOutputHelper _output;
-    private bool _added;
-    private ScrollBarView _scrollBar;
-    public ScrollBarViewTests (ITestOutputHelper output) { _output = output; }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void AutoHideScrollBars_Check ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        Application.Begin (_hostView.SuperView as Toplevel);
-
-        AddHandlers ();
-
-        _hostView.Draw ();
-        Assert.True (_scrollBar.ShowScrollIndicator);
-        Assert.True (_scrollBar.Visible);
-        Assert.Equal ("Absolute(1)", _scrollBar.Width.ToString ());
-        Assert.Equal (1, _scrollBar.Viewport.Width);
-
-        Assert.Equal (
-                      $"Combine(View(Height,HostView(){_hostView.Frame})-Absolute(1))",
-                      _scrollBar.Height.ToString ()
-                     );
-        Assert.Equal (24, _scrollBar.Viewport.Height);
-        Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (_scrollBar.OtherScrollBarView.Visible);
-
-        Assert.Equal (
-                      $"Combine(View(Width,HostView(){_hostView.Frame})-Absolute(1))",
-                      _scrollBar.OtherScrollBarView.Width.ToString ()
-                     );
-        Assert.Equal (79, _scrollBar.OtherScrollBarView.Viewport.Width);
-        Assert.Equal ("Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
-        Assert.Equal (1, _scrollBar.OtherScrollBarView.Viewport.Height);
-
-        _hostView.Lines = 10;
-        _hostView.SetNeedsDraw ();
-        _hostView.Draw ();
-        Assert.False (_scrollBar.ShowScrollIndicator);
-        Assert.False (_scrollBar.Visible);
-        Assert.Equal ("Absolute(1)", _scrollBar.Width.ToString ());
-        Assert.Equal (1, _scrollBar.Viewport.Width);
-
-        Assert.Equal (
-                      $"Combine(View(Height,HostView(){_hostView.Frame})-Absolute(1))",
-                      _scrollBar.Height.ToString ()
-                     );
-        Assert.Equal (24, _scrollBar.Viewport.Height);
-        Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (_scrollBar.OtherScrollBarView.Visible);
-
-        Assert.Equal (
-                      $"View(Width,HostView(){_hostView.Frame})",
-                      _scrollBar.OtherScrollBarView.Width.ToString ()
-                     );
-        Assert.Equal (80, _scrollBar.OtherScrollBarView.Viewport.Width);
-        Assert.Equal ("Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
-        Assert.Equal (1, _scrollBar.OtherScrollBarView.Viewport.Height);
-
-        _hostView.Cols = 60;
-        _hostView.SetNeedsDraw ();
-        _hostView.Draw ();
-        Assert.False (_scrollBar.ShowScrollIndicator);
-        Assert.False (_scrollBar.Visible);
-        Assert.Equal ("Absolute(1)", _scrollBar.Width.ToString ());
-        Assert.Equal (1, _scrollBar.Viewport.Width);
-
-        Assert.Equal (
-                      $"Combine(View(Height,HostView(){_hostView.Frame})-Absolute(1))",
-                      _scrollBar.Height.ToString ()
-                     );
-        Assert.Equal (24, _scrollBar.Viewport.Height);
-        Assert.False (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.False (_scrollBar.OtherScrollBarView.Visible);
-
-        Assert.Equal (
-                      $"View(Width,HostView(){_hostView.Frame})",
-                      _scrollBar.OtherScrollBarView.Width.ToString ()
-                     );
-        Assert.Equal (80, _scrollBar.OtherScrollBarView.Viewport.Width);
-        Assert.Equal ("Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
-        Assert.Equal (1, _scrollBar.OtherScrollBarView.Viewport.Height);
-
-        _hostView.Lines = 40;
-        _hostView.SetNeedsDraw ();
-        _hostView.Draw ();
-        Assert.True (_scrollBar.ShowScrollIndicator);
-        Assert.True (_scrollBar.Visible);
-        Assert.Equal ("Absolute(1)", _scrollBar.Width.ToString ());
-        Assert.Equal (1, _scrollBar.Viewport.Width);
-
-        Assert.Equal (
-                      $"View(Height,HostView(){_hostView.Frame})",
-                      _scrollBar.Height.ToString ()
-                     );
-        Assert.Equal (25, _scrollBar.Viewport.Height);
-        Assert.False (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.False (_scrollBar.OtherScrollBarView.Visible);
-
-        Assert.Equal (
-                      $"View(Width,HostView(){_hostView.Frame})",
-                      _scrollBar.OtherScrollBarView.Width.ToString ()
-                     );
-        Assert.Equal (80, _scrollBar.OtherScrollBarView.Viewport.Width);
-        Assert.Equal ("Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
-        Assert.Equal (1, _scrollBar.OtherScrollBarView.Viewport.Height);
-
-        _hostView.Cols = 120;
-        _hostView.SetNeedsDraw ();
-        _hostView.Draw ();
-        Assert.True (_scrollBar.ShowScrollIndicator);
-        Assert.True (_scrollBar.Visible);
-        Assert.Equal ("Absolute(1)", _scrollBar.Width.ToString ());
-        Assert.Equal (1, _scrollBar.Viewport.Width);
-
-        Assert.Equal (
-                      $"Combine(View(Height,HostView(){_hostView.Frame})-Absolute(1))",
-                      _scrollBar.Height.ToString ()
-                     );
-        Assert.Equal (24, _scrollBar.Viewport.Height);
-        Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (_scrollBar.OtherScrollBarView.Visible);
-
-        Assert.Equal (
-                      $"Combine(View(Width,HostView(){_hostView.Frame})-Absolute(1))",
-                      _scrollBar.OtherScrollBarView.Width.ToString ()
-                     );
-        Assert.Equal (79, _scrollBar.OtherScrollBarView.Viewport.Width);
-        Assert.Equal ("Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
-        Assert.Equal (1, _scrollBar.OtherScrollBarView.Viewport.Height);
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Both_Default_Draws_Correctly ()
-    {
-        var width = 3;
-        var height = 40;
-
-        var super = new Window { Id = "super", Width = Dim.Fill (), Height = Dim.Fill () };
-        var top = new Toplevel ();
-        top.Add (super);
-
-        var horiz = new ScrollBarView
-        {
-            Id = "horiz",
-            Size = width * 2,
-
-            // BUGBUG: ScrollBarView should work if Host is null
-            Host = super,
-            ShowScrollIndicator = true,
-            IsVertical = true
-        };
-        super.Add (horiz);
-
-        var vert = new ScrollBarView
-        {
-            Id = "vert",
-            Size = height * 2,
-
-            // BUGBUG: ScrollBarView should work if Host is null
-            Host = super,
-            ShowScrollIndicator = true,
-            IsVertical = true
-        };
-        super.Add (vert);
-
-        Application.Begin (top);
-        ((FakeDriver)Application.Driver!).SetBufferSize (width, height);
-        Application.LayoutAndDraw ();
-
-        var expected = @"
-┌─┐
-│▲│
-│┬│
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│┴│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│▼│
-└─┘";
-        _ = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
-        top.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void ChangedPosition_Negative_Value ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        Application.Begin (_hostView.SuperView as Toplevel);
-        Application.LayoutAndDraw ();
-
-        AddHandlers ();
-
-        _scrollBar.Position = -20;
-        Assert.Equal (0, _scrollBar.Position);
-        Assert.Equal (_scrollBar.Position, _hostView.Top);
-
-        _scrollBar.OtherScrollBarView.Position = -50;
-        Assert.Equal (0, _scrollBar.OtherScrollBarView.Position);
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void ChangedPosition_Scrolling ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        Application.Begin (_hostView.SuperView as Toplevel);
-        Application.LayoutAndDraw ();
-
-        AddHandlers ();
-
-        for (var i = 0; i < _scrollBar.Size; i++)
-        {
-            _scrollBar.Position += 1;
-            Assert.Equal (_scrollBar.Position, _hostView.Top);
-        }
-
-        for (int i = _scrollBar.Size - 1; i >= 0; i--)
-        {
-            _scrollBar.Position -= 1;
-            Assert.Equal (_scrollBar.Position, _hostView.Top);
-        }
-
-        for (var i = 0; i < _scrollBar.OtherScrollBarView.Size; i++)
-        {
-            _scrollBar.OtherScrollBarView.Position += i;
-            Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        }
-
-        for (int i = _scrollBar.OtherScrollBarView.Size - 1; i >= 0; i--)
-        {
-            _scrollBar.OtherScrollBarView.Position -= 1;
-            Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        }
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void ChangedPosition_Update_The_Hosted_View ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        Application.Begin (_hostView.SuperView as Toplevel);
-        Application.LayoutAndDraw ();
-
-        AddHandlers ();
-
-        _scrollBar.Position = 2;
-        Assert.Equal (_scrollBar.Position, _hostView.Top);
-
-        _scrollBar.OtherScrollBarView.Position = 5;
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Visible_Gets_Sets ()
-    {
-        var text =
-            "This is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test";
-        var label = new Label { Text = text };
-        var top = new Toplevel ();
-        top.Add (label);
-
-        var sbv = new ScrollBarView (label, true, false) { Size = 100 };
-        RunState rs = Application.Begin (top);
-        Application.RunIteration (ref rs);
-
-        Assert.True (sbv.Visible);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a tes▲
-This is a tes┬
-This is a tes┴
-This is a tes░
-This is a tes░
-This is a tes▼
-",
-                                                      _output
-                                                     );
-
-        sbv.Visible = false;
-        Assert.False (sbv.Visible);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-",
-                                                      _output
-                                                     );
-
-        sbv.Visible = true;
-        Assert.True (sbv.Visible);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a tes▲
-This is a tes┬
-This is a tes┴
-This is a tes░
-This is a tes░
-This is a tes▼
-",
-                                                      _output
-                                                     );
-
-        sbv.Visible = false;
-        Assert.False (sbv.Visible);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-",
-                                                      _output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact (Skip = "#3798 broke - Fix with #3498")]
-    public void
-        Constructor_ShowBothScrollIndicator_False_And_IsVertical_False_Refresh_Does_Not_Throws_An_Object_Null_Exception ()
-    {
-        var exception = Record.Exception (
-                                          () =>
-                                          {
-                                              Application.Init (new FakeDriver ());
-
-                                              Toplevel top = new ();
-
-                                              var win = new Window { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () };
-
-                                              ObservableCollection<string> source = [];
-
-                                              for (var i = 0; i < 50; i++)
-                                              {
-                                                  var text = $"item {i} - ";
-
-                                                  for (var j = 0; j < 160; j++)
-                                                  {
-                                                      var col = j.ToString ();
-                                                      text += col.Length == 1 ? col [0] : col [1];
-                                                  }
-
-                                                  source.Add (text);
-                                              }
-
-                                              var listView = new ListView
-                                              {
-                                                  X = 0,
-                                                  Y = 0,
-                                                  Width = Dim.Fill (),
-                                                  Height = Dim.Fill (),
-                                                  Source = new ListWrapper<string> (source)
-                                              };
-                                              win.Add (listView);
-
-                                              var newScrollBarView = new ScrollBarView (listView, false, false) { KeepContentAlwaysInViewport = true };
-                                              win.Add (newScrollBarView);
-
-                                              newScrollBarView.ChangedPosition += (s, e) =>
-                                                                                  {
-                                                                                      listView.LeftItem = newScrollBarView.Position;
-
-                                                                                      if (listView.LeftItem != newScrollBarView.Position)
-                                                                                      {
-                                                                                          newScrollBarView.Position = listView.LeftItem;
-                                                                                      }
-
-                                                                                      Assert.Equal (newScrollBarView.Position, listView.LeftItem);
-                                                                                      listView.SetNeedsDraw ();
-                                                                                      Application.LayoutAndDraw ();
-
-                                                                                  };
-
-                                              listView.DrawingContent += (s, e) =>
-                                                                      {
-                                                                          newScrollBarView.Size = listView.MaxLength;
-                                                                          Assert.Equal (newScrollBarView.Size, listView.MaxLength);
-                                                                          newScrollBarView.Position = listView.LeftItem;
-                                                                          Assert.Equal (newScrollBarView.Position, listView.LeftItem);
-                                                                          newScrollBarView.Refresh ();
-                                                                      };
-
-                                              top.Ready += (s, e) =>
-                                                           {
-                                                               newScrollBarView.Position = 100;
-
-                                                               Assert.Equal (
-                                                                             newScrollBarView.Position,
-                                                                             newScrollBarView.Size
-                                                                             - listView.LeftItem
-                                                                             + (listView.LeftItem - listView.Viewport.Width));
-                                                               Assert.Equal (newScrollBarView.Position, listView.LeftItem);
-
-                                                               Assert.Equal (92, newScrollBarView.Position);
-                                                               Assert.Equal (92, listView.LeftItem);
-                                                               Application.RequestStop ();
-                                                           };
-
-                                              top.Add (win);
-
-                                              Application.Run (top);
-
-                                              top.Dispose ();
-                                              Application.Shutdown ();
-
-                                          });
-
-        Assert.Null (exception);
-    }
-
-    [Fact (Skip = "#3798 broke - Fix with #3498")]
-    public void
-        Constructor_ShowBothScrollIndicator_False_And_IsVertical_True_Refresh_Does_Not_Throws_An_Object_Null_Exception ()
-    {
-        Exception exception = Record.Exception (
-                                                () =>
-                                                {
-                                                    Application.Init (new FakeDriver ());
-                                                    Toplevel top = new ();
-                                                    var win = new Window { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () };
-                                                    ObservableCollection<string> source = [];
-
-                                                    for (var i = 0; i < 50; i++)
-                                                    {
-                                                        source.Add ($"item {i}");
-                                                    }
-
-                                                    var listView = new ListView
-                                                    {
-                                                        X = 0,
-                                                        Y = 0,
-                                                        Width = Dim.Fill (),
-                                                        Height = Dim.Fill (),
-                                                        Source = new ListWrapper<string> (source)
-                                                    };
-                                                    win.Add (listView);
-                                                    var newScrollBarView = new ScrollBarView (listView, true, false) { KeepContentAlwaysInViewport = true };
-                                                    win.Add (newScrollBarView);
-
-                                                    newScrollBarView.ChangedPosition += (s, e) =>
-                                                                                        {
-                                                                                            listView.TopItem = newScrollBarView.Position;
-
-                                                                                            if (listView.TopItem != newScrollBarView.Position)
-                                                                                            {
-                                                                                                newScrollBarView.Position = listView.TopItem;
-                                                                                            }
-
-                                                                                            Assert.Equal (newScrollBarView.Position, listView.TopItem);
-                                                                                            listView.SetNeedsDraw ();
-                                                                                            Application.LayoutAndDraw ();
-
-                                                                                        };
-
-                                                    listView.DrawingContent += (s, e) =>
-                                                                            {
-                                                                                newScrollBarView.Size = listView.Source.Count;
-                                                                                Assert.Equal (newScrollBarView.Size, listView.Source.Count);
-                                                                                newScrollBarView.Position = listView.TopItem;
-                                                                                Assert.Equal (newScrollBarView.Position, listView.TopItem);
-                                                                                newScrollBarView.Refresh ();
-                                                                            };
-
-                                                    top.Ready += (s, e) =>
-                                                                 {
-                                                                     newScrollBarView.Position = 45;
-
-                                                                     Assert.Equal (
-                                                                                   newScrollBarView.Position,
-                                                                                   newScrollBarView.Size
-                                                                                   - listView.TopItem
-                                                                                   + (listView.TopItem - listView.Viewport.Height)
-                                                                                  );
-                                                                     Assert.Equal (newScrollBarView.Position, listView.TopItem);
-                                                                     Assert.Equal (27, newScrollBarView.Position);
-                                                                     Assert.Equal (27, listView.TopItem);
-                                                                     Application.RequestStop ();
-                                                                 };
-                                                    top.Add (win);
-                                                    Application.Run (top);
-                                                    top.Dispose ();
-                                                    Application.Shutdown ();
-                                                }
-                                               );
-
-        Assert.Null (exception);
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void ContentBottomRightCorner_Not_Redraw_If_Both_Size_Equal_To_Zero ()
-    {
-        var text =
-            "This is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test";
-        var label = new Label { Text = text };
-        var top = new Toplevel ();
-        top.Add (label);
-
-        var sbv = new ScrollBarView (label, true) { Size = 100 };
-        sbv.OtherScrollBarView.Size = 100;
-        RunState rs = Application.Begin (top);
-
-        Application.RunIteration (ref rs);
-        Assert.Equal (100, sbv.Size);
-        Assert.Equal (100, sbv.OtherScrollBarView.Size);
-        Assert.True (sbv.ShowScrollIndicator);
-        Assert.True (sbv.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (sbv.Visible);
-        Assert.True (sbv.OtherScrollBarView.Visible);
-
-        View contentBottomRightCorner =
-            label.SuperView.Subviews.First (v => v is ScrollBarView.ContentBottomRightCorner);
-        Assert.True (contentBottomRightCorner is ScrollBarView.ContentBottomRightCorner);
-        Assert.True (contentBottomRightCorner.Visible);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a tes▲
-This is a tes┬
-This is a tes┴
-This is a tes░
-This is a tes▼
-◄├─┤░░░░░░░░► 
-",
-                                                      _output
-                                                     );
-
-        sbv.Size = 0;
-        sbv.OtherScrollBarView.Size = 0;
-        Assert.Equal (0, sbv.Size);
-        Assert.Equal (0, sbv.OtherScrollBarView.Size);
-        Assert.False (sbv.ShowScrollIndicator);
-        Assert.False (sbv.OtherScrollBarView.ShowScrollIndicator);
-        Assert.False (sbv.Visible);
-        Assert.False (sbv.OtherScrollBarView.Visible);
-        top.Draw ();
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-",
-                                                      _output
-                                                     );
-
-        sbv.Size = 50;
-        sbv.OtherScrollBarView.Size = 50;
-        Assert.Equal (50, sbv.Size);
-        Assert.Equal (50, sbv.OtherScrollBarView.Size);
-        Assert.True (sbv.ShowScrollIndicator);
-        Assert.True (sbv.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (sbv.Visible);
-        Assert.True (sbv.OtherScrollBarView.Visible);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a tes▲
-This is a tes┬
-This is a tes┴
-This is a tes░
-This is a tes▼
-◄├──┤░░░░░░░► 
-",
-                                                      _output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void ContentBottomRightCorner_Not_Redraw_If_One_Size_Equal_To_Zero ()
-    {
-        var text =
-            "This is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test";
-        var label = new Label { Text = text };
-        var top = new Toplevel ();
-        top.Add (label);
-
-        var sbv = new ScrollBarView (label, true, false) { Size = 100 };
-        Application.Begin (top);
-        Application.LayoutAndDraw ();
-
-        Assert.Equal (100, sbv.Size);
-        Assert.Null (sbv.OtherScrollBarView);
-        Assert.True (sbv.ShowScrollIndicator);
-        Assert.True (sbv.Visible);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a tes▲
-This is a tes┬
-This is a tes┴
-This is a tes░
-This is a tes░
-This is a tes▼
-",
-                                                      _output
-                                                     );
-
-        sbv.Size = 0;
-        Assert.Equal (0, sbv.Size);
-        Assert.False (sbv.ShowScrollIndicator);
-        Assert.False (sbv.Visible);
-        top.Draw ();
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-This is a test
-",
-                                                      _output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void DrawContent_Update_The_ScrollBarView_Position ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        Application.Begin (_hostView.SuperView as Toplevel);
-        Application.LayoutAndDraw ();
-
-        AddHandlers ();
-
-        _hostView.Top = 3;
-        _hostView.SetNeedsDraw ();
-        _hostView.Draw ();
-        Assert.Equal (_scrollBar.Position, _hostView.Top);
-
-        _hostView.Left = 6;
-        _hostView.SetNeedsDraw ();
-        _hostView.Draw ();
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Horizontal_Default_Draws_Correctly ()
-    {
-        var width = 40;
-        var height = 3;
-
-        var super = new Window { Id = "super", Width = Dim.Fill (), Height = Dim.Fill () };
-        var top = new Toplevel ();
-        top.Add (super);
-
-        var sbv = new ScrollBarView { Id = "sbv", Size = width * 2, ShowScrollIndicator = true };
-        super.Add (sbv);
-        Application.Begin (top);
-        ((FakeDriver)Application.Driver!).SetBufferSize (width, height);
-
-        var expected = @"
-┌──────────────────────────────────────┐
-│◄├────────────────┤░░░░░░░░░░░░░░░░░░►│
-└──────────────────────────────────────┘";
-        _ = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
-        top.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void Hosting_A_Null_SuperView_View_To_A_ScrollBarView_Throws_ArgumentNullException ()
-    {
-        Assert.Throws<ArgumentNullException> (
-                                              "The host SuperView parameter can't be null.",
-                                              () => new ScrollBarView (new View (), true)
-                                             );
-
-        Assert.Throws<ArgumentNullException> (
-                                              "The host SuperView parameter can't be null.",
-                                              () => new ScrollBarView (new View (), false)
-                                             );
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void Hosting_A_Null_View_To_A_ScrollBarView_Throws_ArgumentNullException ()
-    {
-        Assert.Throws<ArgumentNullException> (
-                                              "The host parameter can't be null.",
-                                              () => new ScrollBarView (null, true)
-                                             );
-
-        Assert.Throws<ArgumentNullException> (
-                                              "The host parameter can't be null.",
-                                              () => new ScrollBarView (null, false)
-                                             );
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void Hosting_A_View_To_A_ScrollBarView ()
-    {
-        RemoveHandlers ();
-
-        _scrollBar = new ScrollBarView (_hostView, true);
-        RunState rs = Application.Begin (_hostView.SuperView as Toplevel);
-
-        Assert.True (_scrollBar.IsVertical);
-        Assert.False (_scrollBar.OtherScrollBarView.IsVertical);
-
-        Assert.Equal (_scrollBar.Position, _hostView.Top);
-        Assert.NotEqual (_scrollBar.Size, _hostView.Lines);
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        Assert.NotEqual (_scrollBar.OtherScrollBarView.Size, _hostView.Cols);
-
-        AddHandlers ();
-        Application.RunIteration (ref rs);
-
-        Assert.Equal (_scrollBar.Position, _hostView.Top);
-        Assert.Equal (_scrollBar.Size, _hostView.Lines);
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        Assert.Equal (_scrollBar.OtherScrollBarView.Size, _hostView.Cols);
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Hosting_ShowBothScrollIndicator_Invisible ()
-    {
-        var textView = new TextView
-        {
-            Width = Dim.Fill (),
-            Height = Dim.Fill (),
-            Text =
-                "This is the help text for the Second Step.\n\nPress the button to see a message box.\n\nEnter name too."
-        };
-        var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () };
-        win.Add (textView);
-
-        var scrollBar = new ScrollBarView (textView, true);
-
-        scrollBar.ChangedPosition += (s, e) =>
-                                     {
-                                         textView.TopRow = scrollBar.Position;
-
-                                         if (textView.TopRow != scrollBar.Position)
-                                         {
-                                             scrollBar.Position = textView.TopRow;
-                                         }
-
-                                         textView.SetNeedsDraw ();
-                                     };
-
-        scrollBar.OtherScrollBarView.ChangedPosition += (s, e) =>
-                                                        {
-                                                            textView.LeftColumn = scrollBar.OtherScrollBarView.Position;
-
-                                                            if (textView.LeftColumn != scrollBar.OtherScrollBarView.Position)
-                                                            {
-                                                                scrollBar.OtherScrollBarView.Position = textView.LeftColumn;
-                                                            }
-
-                                                            textView.SetNeedsDraw ();
-                                                        };
-
-        textView.SubviewsLaidOut += (s, e) =>
-                                   {
-                                       scrollBar.Size = textView.Lines;
-                                       scrollBar.Position = textView.TopRow;
-
-                                       if (scrollBar.OtherScrollBarView != null)
-                                       {
-                                           scrollBar.OtherScrollBarView.Size = textView.Maxlength;
-                                           scrollBar.OtherScrollBarView.Position = textView.LeftColumn;
-                                       }
-                                   };
-        var top = new Toplevel ();
-        top.Add (win);
-
-        RunState rs = Application.Begin (top);
-        ((FakeDriver)Application.Driver!).SetBufferSize (45, 20);
-
-        Assert.True (scrollBar.AutoHideScrollBars);
-        Assert.False (scrollBar.ShowScrollIndicator);
-        Assert.False (scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.Equal (5, textView.Lines);
-        Assert.Equal (42, textView.Maxlength);
-        Assert.Equal (0, textView.LeftColumn);
-        Assert.Equal (0, scrollBar.Position);
-        Assert.Equal (0, scrollBar.OtherScrollBarView.Position);
-
-        var expected = @"
-┌───────────────────────────────────────────┐
-│This is the help text for the Second Step. │
-│                                           │
-│Press the button to see a message box.     │
-│                                           │
-│Enter name too.                            │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-│                                           │
-└───────────────────────────────────────────┘
-";
-
-        Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
-        Assert.Equal (new Rectangle (0, 0, 45, 20), pos);
-
-        textView.WordWrap = true;
-        ((FakeDriver)Application.Driver!).SetBufferSize (26, 20);
-        Application.RunIteration (ref rs);
-
-        Assert.True (textView.WordWrap);
-        Assert.True (scrollBar.AutoHideScrollBars);
-        Assert.Equal (7, textView.Lines);
-        Assert.Equal (22, textView.Maxlength);
-        Assert.Equal (0, textView.LeftColumn);
-        Assert.Equal (0, scrollBar.Position);
-        Assert.Equal (0, scrollBar.OtherScrollBarView.Position);
-
-        expected = @"
-┌────────────────────────┐
-│This is the help text   │
-│for the Second Step.    │
-│                        │
-│Press the button to     │
-│see a message box.      │
-│                        │
-│Enter name too.         │
-│                        │
-│                        │
-│                        │
-│                        │
-│                        │
-│                        │
-│                        │
-│                        │
-│                        │
-│                        │
-│                        │
-└────────────────────────┘
-";
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
-        Assert.Equal (new Rectangle (0, 0, 26, 20), pos);
-
-        ((FakeDriver)Application.Driver!).SetBufferSize (10, 10);
-        Application.RunIteration (ref rs);
-
-        Assert.True (textView.WordWrap);
-        Assert.True (scrollBar.AutoHideScrollBars);
-        Assert.Equal (20, textView.Lines);
-        Assert.Equal (7, textView.Maxlength);
-        Assert.Equal (0, textView.LeftColumn);
-        Assert.Equal (0, scrollBar.Position);
-        Assert.Equal (0, scrollBar.OtherScrollBarView.Position);
-        Assert.True (scrollBar.ShowScrollIndicator);
-
-        expected = @"
-┌────────┐
-│This   ▲│
-│is the ┬│
-│help   ││
-│text   ┴│
-│for    ░│
-│the    ░│
-│Second ░│
-│Step.  ▼│
-└────────┘
-";
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
-        Assert.Equal (new Rectangle (0, 0, 10, 10), pos);
-        top.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void Hosting_Two_Horizontal_ScrollBarView_Throws_ArgumentException ()
-    {
-        var top = new Toplevel ();
-        var host = new View ();
-        top.Add (host);
-        var v = new ScrollBarView (host, false);
-        var h = new ScrollBarView (host, false);
-
-        Assert.Throws<ArgumentException> (() => v.OtherScrollBarView = h);
-        Assert.Throws<ArgumentException> (() => h.OtherScrollBarView = v);
-        top.Dispose ();
-
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void Hosting_Two_Vertical_ScrollBarView_Throws_ArgumentException ()
-    {
-        var top = new Toplevel ();
-        var host = new View ();
-        top.Add (host);
-        var v = new ScrollBarView (host, true);
-        var h = new ScrollBarView (host, true);
-
-        Assert.Throws<ArgumentException> (() => v.OtherScrollBarView = h);
-        Assert.Throws<ArgumentException> (() => h.OtherScrollBarView = v);
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Internal_Tests ()
-    {
-        Toplevel top = new ();
-        top.Layout ();
-        Assert.Equal (new Rectangle (0, 0, 80, 25), top.Viewport);
-        var view = new View { Width = Dim.Fill (), Height = Dim.Fill () };
-        top.Add (view);
-        var sbv = new ScrollBarView (view, true);
-        top.Add (sbv);
-        Assert.Equal (view, sbv.Host);
-        sbv.Size = 40;
-        sbv.Position = 0;
-        sbv.OtherScrollBarView.Size = 100;
-        sbv.OtherScrollBarView.Position = 0;
-        top.Layout ();
-
-        // Host bounds is not empty.
-        Assert.True (sbv.CanScroll (10, out int max, sbv.IsVertical));
-        Assert.Equal (10, max);
-        Assert.True (sbv.OtherScrollBarView.CanScroll (10, out max, sbv.OtherScrollBarView.IsVertical));
-        Assert.Equal (10, max);
-
-        Application.Begin (top);
-
-        // They are visible so they are drawn.
-        Assert.True (sbv.Visible);
-        Assert.True (sbv.OtherScrollBarView.Visible);
-        top.LayoutSubviews ();
-
-        // Now the host bounds is not empty.
-        Assert.True (sbv.CanScroll (10, out max, sbv.IsVertical));
-        Assert.Equal (10, max);
-        Assert.True (sbv.OtherScrollBarView.CanScroll (10, out max, sbv.OtherScrollBarView.IsVertical));
-        Assert.Equal (10, max);
-        Assert.True (sbv.CanScroll (50, out max, sbv.IsVertical));
-        Assert.Equal (40, sbv.Size);
-        Assert.Equal (16, max); // 16+25=41
-        Assert.True (sbv.OtherScrollBarView.CanScroll (150, out max, sbv.OtherScrollBarView.IsVertical));
-        Assert.Equal (100, sbv.OtherScrollBarView.Size);
-        Assert.Equal (21, max); // 21+80=101
-        Assert.True (sbv.Visible);
-        Assert.True (sbv.OtherScrollBarView.Visible);
-        sbv.KeepContentAlwaysInViewport = false;
-        sbv.OtherScrollBarView.KeepContentAlwaysInViewport = false;
-        Assert.True (sbv.CanScroll (50, out max, sbv.IsVertical));
-        Assert.Equal (39, max);
-        Assert.True (sbv.OtherScrollBarView.CanScroll (150, out max, sbv.OtherScrollBarView.IsVertical));
-        Assert.Equal (99, max);
-        Assert.True (sbv.Visible);
-        Assert.True (sbv.OtherScrollBarView.Visible);
-        top.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void KeepContentAlwaysInViewport_False ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        RunState rs = Application.Begin (_hostView.SuperView as Toplevel);
-
-        AddHandlers ();
-        Application.RunIteration (ref rs);
-
-        _scrollBar.KeepContentAlwaysInViewport = false;
-        _scrollBar.Position = 50;
-        Assert.Equal (_scrollBar.Position, _scrollBar.Size - 1);
-        Assert.Equal (_scrollBar.Position, _hostView.Top);
-        Assert.Equal (29, _scrollBar.Position);
-        Assert.Equal (29, _hostView.Top);
-
-        _scrollBar.OtherScrollBarView.Position = 150;
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _scrollBar.OtherScrollBarView.Size - 1);
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        Assert.Equal (99, _scrollBar.OtherScrollBarView.Position);
-        Assert.Equal (99, _hostView.Left);
-        _hostView.SuperView.Dispose ();
-
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void KeepContentAlwaysInViewport_True ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        RunState rs = Application.Begin (_hostView.SuperView as Toplevel);
-
-        AddHandlers ();
-        Application.RunIteration (ref rs);
-
-        Assert.Equal (80, _hostView.Viewport.Width);
-        Assert.Equal (25, _hostView.Viewport.Height);
-        Assert.Equal (79, _scrollBar.OtherScrollBarView.Viewport.Width);
-        Assert.Equal (24, _scrollBar.Viewport.Height);
-        Assert.Equal (30, _scrollBar.Size);
-        Assert.Equal (100, _scrollBar.OtherScrollBarView.Size);
-        Assert.True (_scrollBar.ShowScrollIndicator);
-        Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (_scrollBar.Visible);
-        Assert.True (_scrollBar.OtherScrollBarView.Visible);
-
-        _scrollBar.Position = 50;
-        Assert.Equal (_scrollBar.Position, _scrollBar.Size - _scrollBar.Viewport.Height);
-        Assert.Equal (_scrollBar.Position, _hostView.Top);
-        Assert.Equal (6, _scrollBar.Position);
-        Assert.Equal (6, _hostView.Top);
-        Assert.True (_scrollBar.ShowScrollIndicator);
-        Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (_scrollBar.Visible);
-        Assert.True (_scrollBar.OtherScrollBarView.Visible);
-
-        _scrollBar.OtherScrollBarView.Position = 150;
-
-        Assert.Equal (
-                      _scrollBar.OtherScrollBarView.Position,
-                      _scrollBar.OtherScrollBarView.Size - _scrollBar.OtherScrollBarView.Viewport.Width
-                     );
-        Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
-        Assert.Equal (21, _scrollBar.OtherScrollBarView.Position);
-        Assert.Equal (21, _hostView.Left);
-        Assert.True (_scrollBar.ShowScrollIndicator);
-        Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        Assert.True (_scrollBar.Visible);
-        Assert.True (_scrollBar.OtherScrollBarView.Visible);
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void OtherScrollBarView_Not_Null ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        Application.Begin (_hostView.SuperView as Toplevel);
-
-        AddHandlers ();
-
-        Assert.NotNull (_scrollBar.OtherScrollBarView);
-        Assert.NotEqual (_scrollBar, _scrollBar.OtherScrollBarView);
-        Assert.Equal (_scrollBar.OtherScrollBarView.OtherScrollBarView, _scrollBar);
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void Scrolling_With_Default_Constructor_Do_Not_Scroll ()
-    {
-        var sbv = new ScrollBarView { Position = 1 };
-        Assert.Equal (1, sbv.Position);
-        Assert.NotEqual (0, sbv.Position);
-        sbv.Dispose ();
-    }
-
-    [Fact]
-    [ScrollBarAutoInitShutdown]
-    public void ShowScrollIndicator_Check ()
-    {
-        _scrollBar = new ScrollBarView (_hostView, true);
-        RunState rs = Application.Begin (_hostView.SuperView as Toplevel);
-
-        AddHandlers ();
-        Application.RunIteration (ref rs);
-
-        Assert.True (_scrollBar.ShowScrollIndicator);
-        Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
-        _hostView.SuperView.Dispose ();
-    }
-
-    [Fact (Skip = "#3798 broke - Fix with #3498")]
-    [AutoInitShutdown]
-    public void ShowScrollIndicator_False_Must_Also_Set_Visible_To_False_To_Not_Respond_To_Events ()
-    {
-        // Override CM
-        Window.DefaultBorderStyle = LineStyle.Single;
-        Dialog.DefaultButtonAlignment = Alignment.Center;
-        Dialog.DefaultBorderStyle = LineStyle.Single;
-        Dialog.DefaultShadow = ShadowStyle.None;
-        Button.DefaultShadow = ShadowStyle.None;
-
-        var clicked = false;
-        var text = "This is a test\nThis is a test\nThis is a test\nThis is a test\nThis is a test";
-        var label = new Label { Width = 14, Height = 5, Text = text };
-        var btn = new Button { X = 14, Text = "Click Me!" };
-        btn.Accepting += (s, e) => clicked = true;
-        var top = new Toplevel ();
-        top.Add (label, btn);
-
-        var sbv = new ScrollBarView (label, true, false) { Size = 5 };
-        Application.Begin (top);
-        Application.LayoutAndDraw ();
-
-        Assert.Equal (5, sbv.Size);
-        Assert.Null (sbv.OtherScrollBarView);
-        Assert.False (sbv.ShowScrollIndicator);
-        Assert.False (sbv.Visible);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @$"
-This is a test{CM.Glyphs.LeftBracket} Click Me! {CM.Glyphs.RightBracket}
-This is a test             
-This is a test             
-This is a test             
-This is a test             ",
-                                                      _output
-                                                     );
-
-        Application.RaiseMouseEvent (new MouseEventArgs { Position = new (15, 0), Flags = MouseFlags.Button1Clicked });
-
-        Assert.Null (Application.MouseGrabView);
-        Assert.True (clicked);
-
-        clicked = false;
-
-        sbv.Visible = true;
-        Assert.Equal (5, sbv.Size);
-        Assert.False (sbv.ShowScrollIndicator);
-        Assert.True (sbv.Visible);
-        Application.LayoutAndDraw ();
-        Assert.False (sbv.Visible);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @$"
-This is a test{CM.Glyphs.LeftBracket} Click Me! {CM.Glyphs.RightBracket}
-This is a test             
-This is a test             
-This is a test             
-This is a test             ",
-                                                      _output
-                                                     );
-
-        Application.RaiseMouseEvent (new MouseEventArgs { Position = new (15, 0), Flags = MouseFlags.Button1Clicked });
-
-        Assert.Null (Application.MouseGrabView);
-        Assert.True (clicked);
-        Assert.Equal (5, sbv.Size);
-        Assert.False (sbv.ShowScrollIndicator);
-        Assert.False (sbv.Visible);
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Vertical_Default_Draws_Correctly ()
-    {
-        var width = 3;
-        var height = 40;
-
-        var super = new Window { Id = "super", Width = Dim.Fill (), Height = Dim.Fill () };
-        var top = new Toplevel ();
-        top.Add (super);
-
-        var sbv = new ScrollBarView
-        {
-            Id = "sbv",
-            Size = height * 2,
-
-            // BUGBUG: ScrollBarView should work if Host is null
-            Host = super,
-            ShowScrollIndicator = true,
-            IsVertical = true
-        };
-
-        super.Add (sbv);
-        Application.Begin (top);
-        ((FakeDriver)Application.Driver!).SetBufferSize (width, height);
-
-        var expected = @"
-┌─┐
-│▲│
-│┬│
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│││
-│┴│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│░│
-│▼│
-└─┘";
-        _ = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
-        top.Dispose ();
-    }
-
-    private void _hostView_DrawContent (object sender, DrawEventArgs e)
-    {
-        _scrollBar.Size = _hostView.Lines;
-        _scrollBar.Position = _hostView.Top;
-        _scrollBar.OtherScrollBarView.Size = _hostView.Cols;
-        _scrollBar.OtherScrollBarView.Position = _hostView.Left;
-        _scrollBar.Refresh ();
-    }
-
-    private void _scrollBar_ChangedPosition (object sender, EventArgs e)
-    {
-        _hostView.Top = _scrollBar.Position;
-
-        if (_hostView.Top != _scrollBar.Position)
-        {
-            _scrollBar.Position = _hostView.Top;
-        }
-
-        _hostView.SetNeedsDraw ();
-    }
-
-    private void _scrollBar_OtherScrollBarView_ChangedPosition (object sender, EventArgs e)
-    {
-        _hostView.Left = _scrollBar.OtherScrollBarView.Position;
-
-        if (_hostView.Left != _scrollBar.OtherScrollBarView.Position)
-        {
-            _scrollBar.OtherScrollBarView.Position = _hostView.Left;
-        }
-
-        _hostView.SetNeedsDraw ();
-    }
-
-    private void AddHandlers ()
-    {
-        if (!_added)
-        {
-            _hostView.DrawingContent += _hostView_DrawContent;
-            _scrollBar.ChangedPosition += _scrollBar_ChangedPosition;
-            _scrollBar.OtherScrollBarView.ChangedPosition += _scrollBar_OtherScrollBarView_ChangedPosition;
-        }
-
-        _added = true;
-    }
-
-    private void RemoveHandlers ()
-    {
-        if (_added)
-        {
-            _hostView.DrawingContent -= _hostView_DrawContent;
-            _scrollBar.ChangedPosition -= _scrollBar_ChangedPosition;
-            _scrollBar.OtherScrollBarView.ChangedPosition -= _scrollBar_OtherScrollBarView_ChangedPosition;
-        }
-
-        _added = false;
-    }
-
-    public class HostView : View
-    {
-        public int Cols { get; set; }
-        public int Left { get; set; }
-        public int Lines { get; set; }
-        public int Top { get; set; }
-    }
-
-    // This class enables test functions annotated with the [InitShutdown] attribute
-    // to have a function called before the test function is called and after.
-    // 
-    // This is necessary because a) Application is a singleton and Init/Shutdown must be called
-    // as a pair, and b) all unit test functions should be atomic.
-    [AttributeUsage (AttributeTargets.Class | AttributeTargets.Method)]
-    public class ScrollBarAutoInitShutdownAttribute : AutoInitShutdownAttribute
-    {
-        public override void After (MethodInfo methodUnderTest)
-        {
-            _hostView = null;
-            base.After (methodUnderTest);
-        }
-
-        public override void Before (MethodInfo methodUnderTest)
-        {
-            base.Before (methodUnderTest);
-
-            _hostView = new HostView
-            {
-                Width = Dim.Fill (),
-                Height = Dim.Fill (),
-                Top = 0,
-                Lines = 30,
-                Left = 0,
-                Cols = 100
-            };
-
-            var top = new Toplevel ();
-            top.Add (_hostView);
-        }
-    }
-}

+ 1010 - 0
UnitTests/Views/ScrollSliderTests.cs

@@ -0,0 +1,1010 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Microsoft.VisualStudio.TestPlatform.Utilities;
+using Xunit.Abstractions;
+using static Unix.Terminal.Delegates;
+
+namespace Terminal.Gui.ViewsTests;
+
+public class ScrollSliderTests (ITestOutputHelper output)
+{
+
+    [Fact]
+    public void Constructor_Initializes_Correctly ()
+    {
+        var scrollSlider = new ScrollSlider ();
+        Assert.False (scrollSlider.CanFocus);
+        Assert.Equal (Orientation.Vertical, scrollSlider.Orientation);
+        Assert.Equal (TextDirection.TopBottom_LeftRight, scrollSlider.TextDirection);
+        Assert.Equal (Alignment.Center, scrollSlider.TextAlignment);
+        Assert.Equal (Alignment.Center, scrollSlider.VerticalTextAlignment);
+        scrollSlider.Layout ();
+        Assert.Equal (0, scrollSlider.Frame.X);
+        Assert.Equal (0, scrollSlider.Frame.Y);
+        Assert.Equal (1, scrollSlider.Size);
+        Assert.Equal (2048, scrollSlider.VisibleContentSize);
+    }
+
+    [Fact]
+    public void Add_To_SuperView_Initializes_Correctly ()
+    {
+        View super = new View ()
+        {
+            Id = "super",
+            Width = 10,
+            Height = 10,
+            CanFocus = true
+        };
+        var scrollSlider = new ScrollSlider ();
+        super.Add (scrollSlider);
+
+        Assert.False (scrollSlider.CanFocus);
+        Assert.Equal (Orientation.Vertical, scrollSlider.Orientation);
+        Assert.Equal (TextDirection.TopBottom_LeftRight, scrollSlider.TextDirection);
+        Assert.Equal (Alignment.Center, scrollSlider.TextAlignment);
+        Assert.Equal (Alignment.Center, scrollSlider.VerticalTextAlignment);
+        scrollSlider.Layout ();
+        Assert.Equal (0, scrollSlider.Frame.X);
+        Assert.Equal (0, scrollSlider.Frame.Y);
+        Assert.Equal (1, scrollSlider.Size);
+        Assert.Equal (10, scrollSlider.VisibleContentSize);
+    }
+
+    //[Fact]
+    //public void OnOrientationChanged_Sets_Size_To_1 ()
+    //{
+    //    var scrollSlider = new ScrollSlider ();
+    //    scrollSlider.Orientation = Orientation.Horizontal;
+    //    Assert.Equal (1, scrollSlider.Size);
+    //}
+
+    [Fact]
+    public void OnOrientationChanged_Sets_Position_To_0 ()
+    {
+        View super = new View ()
+        {
+            Id = "super",
+            Width = 10,
+            Height = 10
+        };
+        var scrollSlider = new ScrollSlider ()
+        {
+        };
+        super.Add (scrollSlider);
+        scrollSlider.Layout ();
+        scrollSlider.Position = 1;
+        scrollSlider.Orientation = Orientation.Horizontal;
+
+        Assert.Equal (0, scrollSlider.Position);
+    }
+
+    [Fact]
+    public void OnOrientationChanged_Updates_TextDirection_And_TextAlignment ()
+    {
+        var scrollSlider = new ScrollSlider ();
+        scrollSlider.Orientation = Orientation.Horizontal;
+        Assert.Equal (TextDirection.LeftRight_TopBottom, scrollSlider.TextDirection);
+        Assert.Equal (Alignment.Center, scrollSlider.TextAlignment);
+        Assert.Equal (Alignment.Center, scrollSlider.VerticalTextAlignment);
+    }
+
+    [Theory]
+    [CombinatorialData]
+    public void Size_Clamps_To_SuperView_Viewport ([CombinatorialRange (-1, 6, 1)] int sliderSize, Orientation orientation)
+    {
+        var super = new View
+        {
+            Id = "super",
+            Width = 5,
+            Height = 5
+        };
+
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+        };
+        super.Add (scrollSlider);
+        scrollSlider.Layout ();
+
+        scrollSlider.Size = sliderSize;
+        scrollSlider.Layout ();
+
+        Assert.True (scrollSlider.Size > 0);
+
+        Assert.True (scrollSlider.Size <= 5);
+    }
+
+
+    [Theory]
+    [CombinatorialData]
+    public void Size_Clamps_To_VisibleContentSizes ([CombinatorialRange (1, 6, 1)] int dimension, [CombinatorialRange (-1, 6, 1)] int sliderSize, Orientation orientation)
+    {
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            VisibleContentSize = dimension,
+            Size = sliderSize,
+        };
+        scrollSlider.Layout ();
+
+        Assert.True (scrollSlider.Size > 0);
+
+        Assert.True (scrollSlider.Size <= dimension);
+    }
+
+
+    [Theory]
+    [CombinatorialData]
+
+    public void CalculateSize_ScrollBounds_0_Returns_1 ([CombinatorialRange (-1, 5, 1)] int visibleContentSize, [CombinatorialRange (-1, 5, 1)] int scrollableContentSize)
+    {
+        // Arrange
+
+        // Act
+        var sliderSize = ScrollSlider.CalculateSize (scrollableContentSize, visibleContentSize, 0);
+
+        // Assert
+        Assert.Equal (1, sliderSize);
+    }
+
+    [Theory]
+    [CombinatorialData]
+
+    public void CalculateSize_ScrollableContentSize_0_Returns_1 ([CombinatorialRange (-1, 5, 1)] int visibleContentSize, [CombinatorialRange (-1, 5, 1)] int sliderBounds)
+    {
+        // Arrange
+
+        // Act
+        var sliderSize = ScrollSlider.CalculateSize (0, visibleContentSize, sliderBounds);
+
+        // Assert
+        Assert.Equal (1, sliderSize);
+    }
+
+
+    //[Theory]
+    //[CombinatorialData]
+
+    //public void CalculateSize_VisibleContentSize_0_Returns_0 ([CombinatorialRange (-1, 5, 1)] int scrollableContentSize, [CombinatorialRange (-1, 5, 1)] int sliderBounds)
+    //{
+    //    // Arrange
+
+    //    // Act
+    //    var sliderSize = ScrollSlider.CalculateSize (scrollableContentSize, 0, sliderBounds);
+
+    //    // Assert
+    //    Assert.Equal (0, sliderSize);
+    //}
+
+
+    [Theory]
+    [InlineData (0, 1, 1, 1)]
+    [InlineData (1, 1, 1, 1)]
+    [InlineData (1, 2, 1, 1)]
+    [InlineData (0, 5, 5, 5)]
+    [InlineData (1, 5, 5, 1)]
+    [InlineData (2, 5, 5, 2)]
+    [InlineData (3, 5, 5, 3)]
+    [InlineData (4, 5, 5, 4)]
+    [InlineData (5, 5, 5, 5)]
+    [InlineData (6, 5, 5, 5)]
+    public void CalculateSize_Calculates_Correctly (int visibleContentSize, int scrollableContentSize, int scrollBounds, int expectedSliderSize)
+    {
+        // Arrange
+
+        // Act
+        var sliderSize = ScrollSlider.CalculateSize (scrollableContentSize, visibleContentSize, scrollBounds);
+
+        // Assert
+        Assert.Equal (expectedSliderSize, sliderSize);
+    }
+
+    [Fact]
+    public void VisibleContentSize_Not_Set_Uses_SuperView ()
+    {
+        View super = new ()
+        {
+            Id = "super",
+            Height = 5,
+            Width = 5,
+        };
+        var scrollSlider = new ScrollSlider
+        {
+        };
+
+        super.Add (scrollSlider);
+        super.Layout ();
+        Assert.Equal (5, scrollSlider.VisibleContentSize);
+    }
+
+    [Fact]
+    public void VisibleContentSize_Set_Overrides_SuperView ()
+    {
+        View super = new ()
+        {
+            Id = "super",
+            Height = 5,
+            Width = 5,
+        };
+        var scrollSlider = new ScrollSlider
+        {
+            VisibleContentSize = 10,
+        };
+
+        super.Add (scrollSlider);
+        super.Layout ();
+        Assert.Equal (10, scrollSlider.VisibleContentSize);
+
+        super.Height = 3;
+        super.Layout ();
+        Assert.Equal (10, scrollSlider.VisibleContentSize);
+
+        super.Height = 7;
+        super.Layout ();
+        Assert.Equal (10, scrollSlider.VisibleContentSize);
+
+    }
+
+    [Theory]
+    [CombinatorialData]
+    public void VisibleContentSizes_Clamps_0_To_Dimension ([CombinatorialRange (0, 10, 1)] int dimension, Orientation orientation)
+    {
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            VisibleContentSize = dimension,
+        };
+
+        Assert.InRange (scrollSlider.VisibleContentSize, 1, 10);
+
+        View super = new ()
+        {
+            Id = "super",
+            Height = dimension,
+            Width = dimension,
+        };
+
+        scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+        };
+        super.Add (scrollSlider);
+        super.Layout ();
+
+        Assert.InRange (scrollSlider.VisibleContentSize, 1, 10);
+
+        scrollSlider.VisibleContentSize = dimension;
+
+        Assert.InRange (scrollSlider.VisibleContentSize, 1, 10);
+    }
+
+    [Theory]
+    //// 0123456789
+    ////  ---------
+    //// ◄█►
+    //[InlineData (3, 3, 0, 1, 0)]
+    //[InlineData (3, 3, 1, 1, 0)]
+    //[InlineData (3, 3, 2, 1, 0)]
+
+    //// 0123456789
+    ////  ---------
+    //// ◄██►
+    //[InlineData (4, 4, 0, 2, 0)]
+    //[InlineData (4, 4, 1, 2, 0)]
+    //[InlineData (4, 4, 2, 2, 0)]
+    //[InlineData (4, 4, 3, 2, 0)]
+    //[InlineData (4, 4, 4, 2, 0)]
+
+    // 012345
+    // ^----
+    // █░
+    [InlineData (2, 5, 0, 0)]
+    // -^---
+    // █░
+    [InlineData (2, 5, 1, 0)]
+    // --^--
+    // ░█
+    [InlineData (2, 5, 2, 1)]
+    // ---^-
+    // ░█
+    [InlineData (2, 5, 3, 1)]
+    // ----^
+    // ░█
+    [InlineData (2, 5, 4, 1)]
+
+    // 012345
+    // ^----
+    // █░░
+    [InlineData (3, 5, 0, 0)]
+    // -^---
+    // ░█░
+    [InlineData (3, 5, 1, 1)]
+    // --^--
+    // ░░█
+    [InlineData (3, 5, 2, 2)]
+    // ---^-
+    // ░░█
+    [InlineData (3, 5, 3, 2)]
+    // ----^
+    // ░░█
+    [InlineData (3, 5, 4, 2)]
+
+
+    // 0123456789
+    // ^-----
+    // █░░
+    [InlineData (3, 6, 0, 0)]
+    // -^----
+    // █░░
+    [InlineData (3, 6, 1, 1)]
+    // --^---
+    // ░█░
+    [InlineData (3, 6, 2, 1)]
+    // ---^--
+    // ░░█
+    [InlineData (3, 6, 3, 2)]
+    // ----^-
+    // ░░█
+    [InlineData (3, 6, 4, 2)]
+
+    // -----^
+    // ░░█
+    [InlineData (3, 6, 5, 2)]
+
+    // 012345
+    // ^----
+    // ███░
+    [InlineData (4, 5, 0, 0)]
+    // -^---
+    // ░███
+    [InlineData (4, 5, 1, 1)]
+    // --^--
+    // ░███
+    [InlineData (4, 5, 2, 1)]
+    // ---^-
+    // ░███
+    [InlineData (4, 5, 3, 1)]
+    // ----^
+    // ░███
+    [InlineData (4, 5, 4, 1)]
+
+    //// 01234
+    //// ^---------
+    //// ◄█░░►
+    //[InlineData (5, 10, 0, 1, 0)]
+    //// -^--------
+    //// ◄█░░►
+    //[InlineData (5, 10, 1, 1, 0)]
+    //// --^-------
+    //// ◄█░░►
+    //[InlineData (5, 10, 2, 1, 0)]
+    //// ---^------
+    //// ◄█░░►
+    //[InlineData (5, 10, 3, 1, 0)]
+    //// ----^----
+    //// ◄░█░►
+    //[InlineData (5, 10, 4, 1, 1)]
+    //// -----^---
+    //// ◄░█░►
+    //[InlineData (5, 10, 5, 1, 1)]
+    //// ------^--
+    //// ◄░░█►
+    //[InlineData (5, 10, 6, 1, 2)]
+    //// ------^--
+    //// ◄░░█►
+    //[InlineData (5, 10, 7, 1, 2)]
+    //// -------^-
+    //// ◄░░█►
+    //[InlineData (5, 10, 8, 1, 2)]
+    //// --------^
+    //// ◄░░█►
+    //[InlineData (5, 10, 9, 1, 2)]
+
+    // 0123456789
+    // ████░░░░
+    // ^-----------------
+    // 012345678901234567890123456789
+    // ░████░░░
+    // ----^-------------
+    // 012345678901234567890123456789
+    // ░░████░░
+    // --------^---------
+    // 012345678901234567890123456789
+    // ░░░████░
+    // ------------^-----
+    // 012345678901234567890123456789
+    // ░░░░████
+    // ----------------^--
+
+
+
+    // 0123456789
+    // ███░░░░░
+    // ^-----------------
+
+    // 012345678901234567890123456789
+    // ░░███░░░
+    // --------^---------
+    // 012345678901234567890123456789
+    // ░░░███░░
+    // ------------^-----
+    // 012345678901234567890123456789
+    // ░░░░███░
+    // ----------------^--
+    // 012345678901234567890123456789
+    // ░░░░░███
+    // ----------------^--
+
+
+    [InlineData (8, 18, 0, 0)]
+    [InlineData (8, 18, 1, 0)]
+    // 012345678901234567890123456789
+    // ░███░░░░
+    // --^---------------
+    [InlineData (8, 18, 2, 1)]
+    [InlineData (8, 18, 3, 2)]
+    [InlineData (8, 18, 4, 2)]
+    [InlineData (8, 18, 5, 2)]
+    [InlineData (8, 18, 6, 3)]
+    [InlineData (8, 18, 7, 4)]
+    [InlineData (8, 18, 8, 4)]
+    [InlineData (8, 18, 9, 4)]
+
+    // 012345678901234567890123456789
+    // ░░░░░███
+    // ----------^--------
+    [InlineData (8, 18, 10, 5)]
+    [InlineData (8, 18, 11, 5)]
+    [InlineData (8, 18, 12, 5)]
+    [InlineData (8, 18, 13, 5)]
+    [InlineData (8, 18, 14, 5)]
+    [InlineData (8, 18, 15, 5)]
+    [InlineData (8, 18, 16, 5)]
+    [InlineData (8, 18, 17, 5)]
+    [InlineData (8, 18, 18, 5)]
+    [InlineData (8, 18, 19, 5)]
+    [InlineData (8, 18, 20, 5)]
+    [InlineData (8, 18, 21, 5)]
+    [InlineData (8, 18, 22, 5)]
+    [InlineData (8, 18, 23, 5)]
+    // ------------------   ^
+    [InlineData (8, 18, 24, 5)]
+    [InlineData (8, 18, 25, 5)]
+
+    //// 0123456789
+    //// ◄████░░░░►
+    //// ^-----------------
+    //[InlineData (10, 20, 0, 5, 0)]
+    //[InlineData (10, 20, 1, 5, 0)]
+    //[InlineData (10, 20, 2, 5, 0)]
+    //[InlineData (10, 20, 3, 5, 0)]
+    //[InlineData (10, 20, 4, 5, 1)]
+    //[InlineData (10, 20, 5, 5, 1)]
+    //[InlineData (10, 20, 6, 5, 1)]
+    //[InlineData (10, 20, 7, 5, 2)]
+    //[InlineData (10, 20, 8, 5, 2)]
+    //[InlineData (10, 20, 9, 5, 2)]
+    //[InlineData (10, 20, 10, 5, 3)]
+    //[InlineData (10, 20, 11, 5, 3)]
+    //[InlineData (10, 20, 12, 5, 3)]
+    //[InlineData (10, 20, 13, 5, 3)]
+    //[InlineData (10, 20, 14, 5, 4)]
+    //[InlineData (10, 20, 15, 5, 4)]
+    //[InlineData (10, 20, 16, 5, 4)]
+    //[InlineData (10, 20, 17, 5, 5)]
+    //[InlineData (10, 20, 18, 5, 5)]
+    //[InlineData (10, 20, 19, 5, 5)]
+    //[InlineData (10, 20, 20, 5, 6)]
+    //[InlineData (10, 20, 21, 5, 6)]
+    //[InlineData (10, 20, 22, 5, 6)]
+    //[InlineData (10, 20, 23, 5, 6)]
+    //[InlineData (10, 20, 24, 5, 6)]
+    //[InlineData (10, 20, 25, 5, 6)]
+
+    public void CalculatePosition_Calculates_Correctly (int visibleContentSize, int scrollableContentSize, int contentPosition, int expectedSliderPosition)
+    {
+        // Arrange
+
+        // Act
+        var sliderPosition = ScrollSlider.CalculatePosition (
+                                                                 scrollableContentSize: scrollableContentSize,
+                                                                 visibleContentSize: visibleContentSize,
+                                                                 contentPosition: contentPosition,
+                                                                 sliderBounds: visibleContentSize,
+                                                                 NavigationDirection.Forward);
+
+        // Assert
+        Assert.Equal (expectedSliderPosition, sliderPosition);
+    }
+
+    [Theory]
+    [InlineData (8, 18, 0, 0)]
+
+    public void CalculateContentPosition_Calculates_Correctly (
+        int visibleContentSize,
+        int scrollableContentSize,
+        int sliderPosition,
+        int expectedContentPosition
+    )
+    {
+        // Arrange
+
+        // Act
+        var contentPosition = ScrollSlider.CalculateContentPosition (
+                                                             scrollableContentSize: scrollableContentSize,
+                                                             visibleContentSize: visibleContentSize,
+                                                             sliderPosition: sliderPosition,
+                                                             sliderBounds: visibleContentSize);
+
+        // Assert
+        Assert.Equal (expectedContentPosition, contentPosition);
+    }
+
+
+    [Theory]
+    [CombinatorialData]
+    public void ClampPosition_WithSuperView_Clamps_To_ViewPort_Minus_Size_If_VisibleContentSize_Not_Set ([CombinatorialRange (10, 10, 1)] int dimension, [CombinatorialRange (1, 5, 1)] int sliderSize, [CombinatorialRange (-1, 10, 2)] int sliderPosition, Orientation orientation)
+    {
+        View super = new ()
+        {
+            Id = "super",
+            Height = dimension,
+            Width = dimension,
+        };
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            Size = sliderSize,
+        };
+        super.Add (scrollSlider);
+        super.Layout ();
+
+        Assert.Equal (dimension, scrollSlider.VisibleContentSize);
+
+        int clampedPosition = scrollSlider.ClampPosition (sliderPosition);
+
+        Assert.InRange (clampedPosition, 0, dimension - sliderSize);
+    }
+
+    [Theory]
+    [CombinatorialData]
+    public void ClampPosition_WithSuperView_Clamps_To_VisibleContentSize_Minus_Size ([CombinatorialRange (10, 10, 1)] int dimension, [CombinatorialRange (1, 5, 1)] int sliderSize, [CombinatorialRange (-1, 10, 2)] int sliderPosition, Orientation orientation)
+    {
+        View super = new ()
+        {
+            Id = "super",
+            Height = dimension + 2,
+            Width = dimension + 2,
+        };
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            VisibleContentSize = dimension,
+            Size = sliderSize,
+        };
+        super.Add (scrollSlider);
+        super.Layout ();
+
+        int clampedPosition = scrollSlider.ClampPosition (sliderPosition);
+
+        Assert.InRange (clampedPosition, 0, dimension - sliderSize);
+    }
+
+    [Theory]
+    [CombinatorialData]
+    public void ClampPosition_NoSuperView_Clamps_To_VisibleContentSize_Minus_Size ([CombinatorialRange (10, 10, 1)] int dimension, [CombinatorialRange (1, 5, 1)] int sliderSize, [CombinatorialRange (-1, 10, 2)] int sliderPosition, Orientation orientation)
+    {
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            VisibleContentSize = dimension,
+            Size = sliderSize,
+        };
+
+        int clampedPosition = scrollSlider.ClampPosition (sliderPosition);
+
+        Assert.InRange (clampedPosition, 0, dimension - sliderSize);
+    }
+
+    [Theory]
+    [CombinatorialData]
+    public void Position_Clamps_To_VisibleContentSize ([CombinatorialRange (0, 5, 1)] int dimension, [CombinatorialRange (1, 5, 1)] int sliderSize, [CombinatorialRange (-1, 10, 2)] int sliderPosition, Orientation orientation)
+    {
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            VisibleContentSize = dimension,
+            Size = sliderSize,
+            Position = sliderPosition
+        };
+
+        Assert.True (scrollSlider.Position <= 5);
+    }
+
+
+    [Theory]
+    [CombinatorialData]
+    public void Position_Clamps_To_SuperView_Viewport ([CombinatorialRange (0, 5, 1)] int sliderSize, [CombinatorialRange (-2, 10, 2)] int sliderPosition, Orientation orientation)
+    {
+        var super = new View
+        {
+            Id = "super",
+            Width = 5,
+            Height = 5
+        };
+
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+        };
+        super.Add (scrollSlider);
+        scrollSlider.Size = sliderSize;
+        scrollSlider.Layout ();
+
+        scrollSlider.Position = sliderPosition;
+
+        Assert.True (scrollSlider.Position <= 5);
+    }
+
+
+    [Theory]
+    [CombinatorialData]
+    public void Position_Clamps_To_VisibleContentSize_With_SuperView ([CombinatorialRange (0, 5, 1)] int dimension, [CombinatorialRange (1, 5, 1)] int sliderSize, [CombinatorialRange (-2, 10, 2)] int sliderPosition, Orientation orientation)
+    {
+        var super = new View
+        {
+            Id = "super",
+            Width = 10,
+            Height = 10
+        };
+
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            VisibleContentSize = dimension,
+            Size = sliderSize,
+            Position = sliderPosition
+        };
+
+        super.Add (scrollSlider);
+        scrollSlider.Size = sliderSize;
+        scrollSlider.Layout ();
+
+        scrollSlider.Position = sliderPosition;
+
+        Assert.True (scrollSlider.Position <= 5);
+    }
+
+    [Theory]
+    [SetupFakeDriver]
+    [InlineData (
+                    3,
+                    10,
+                    1,
+                    0,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│███│
+│   │
+│   │
+│   │
+│   │
+│   │
+│   │
+│   │
+│   │
+│   │
+└───┘")]
+    [InlineData (
+                    10,
+                    1,
+                    3,
+                    0,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│███       │
+└──────────┘")]
+    [InlineData (
+                    3,
+                    10,
+                    3,
+                    0,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│███│
+│███│
+│███│
+│   │
+│   │
+│   │
+│   │
+│   │
+│   │
+│   │
+└───┘")]
+
+
+
+    [InlineData (
+                    3,
+                    10,
+                    5,
+                    0,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│███│
+│███│
+│███│
+│███│
+│███│
+│   │
+│   │
+│   │
+│   │
+│   │
+└───┘")]
+
+    [InlineData (
+                    3,
+                    10,
+                    5,
+                    1,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│   │
+│███│
+│███│
+│███│
+│███│
+│███│
+│   │
+│   │
+│   │
+│   │
+└───┘")]
+    [InlineData (
+                    3,
+                    10,
+                    5,
+                    4,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│   │
+│   │
+│   │
+│   │
+│███│
+│███│
+│███│
+│███│
+│███│
+│   │
+└───┘")]
+    [InlineData (
+                    3,
+                    10,
+                    5,
+                    5,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│   │
+│   │
+│   │
+│   │
+│   │
+│███│
+│███│
+│███│
+│███│
+│███│
+└───┘")]
+    [InlineData (
+                    3,
+                    10,
+                    5,
+                    6,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│   │
+│   │
+│   │
+│   │
+│   │
+│███│
+│███│
+│███│
+│███│
+│███│
+└───┘")]
+
+    [InlineData (
+                    3,
+                    10,
+                    10,
+                    0,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+└───┘")]
+
+    [InlineData (
+                    3,
+                    10,
+                    10,
+                    5,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+└───┘")]
+    [InlineData (
+                    3,
+                    10,
+                    11,
+                    0,
+                    Orientation.Vertical,
+                    @"
+┌───┐
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+│███│
+└───┘")]
+
+    [InlineData (
+                    10,
+                    3,
+                    5,
+                    0,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│█████     │
+│█████     │
+│█████     │
+└──────────┘")]
+
+    [InlineData (
+                    10,
+                    3,
+                    5,
+                    1,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│ █████    │
+│ █████    │
+│ █████    │
+└──────────┘")]
+    [InlineData (
+                    10,
+                    3,
+                    5,
+                    4,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│    █████ │
+│    █████ │
+│    █████ │
+└──────────┘")]
+    [InlineData (
+                    10,
+                    3,
+                    5,
+                    5,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│     █████│
+│     █████│
+│     █████│
+└──────────┘")]
+    [InlineData (
+                    10,
+                    3,
+                    5,
+                    6,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│     █████│
+│     █████│
+│     █████│
+└──────────┘")]
+
+    [InlineData (
+                    10,
+                    3,
+                    10,
+                    0,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│██████████│
+│██████████│
+│██████████│
+└──────────┘")]
+
+    [InlineData (
+                    10,
+                    3,
+                    10,
+                    5,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│██████████│
+│██████████│
+│██████████│
+└──────────┘")]
+    [InlineData (
+                    10,
+                    3,
+                    11,
+                    0,
+                    Orientation.Horizontal,
+                    @"
+┌──────────┐
+│██████████│
+│██████████│
+│██████████│
+└──────────┘")]
+    public void Draws_Correctly (int superViewportWidth, int superViewportHeight, int sliderSize, int position, Orientation orientation, string expected)
+    {
+        var super = new Window
+        {
+            Id = "super",
+            Width = superViewportWidth + 2,
+            Height = superViewportHeight + 2
+        };
+
+        var scrollSlider = new ScrollSlider
+        {
+            Orientation = orientation,
+            Size = sliderSize,
+            //Position = position,
+        };
+        Assert.Equal (sliderSize, scrollSlider.Size);
+        super.Add (scrollSlider);
+        scrollSlider.Position = position;
+
+        super.Layout ();
+        super.Draw ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+    }
+}

+ 0 - 1155
UnitTests/Views/ScrollViewTests.cs

@@ -1,1155 +0,0 @@
-using System.Text;
-using JetBrains.Annotations;
-using Xunit.Abstractions;
-
-namespace Terminal.Gui.ViewsTests;
-
-public class ScrollViewTests (ITestOutputHelper output)
-{
-#if meh
-    [Fact]
-    public void Adding_Views ()
-    {
-        var sv = new ScrollView { Width = 20, Height = 10 };
-        sv.SetContentSize (new (30, 20));
-
-        sv.Add (
-                new View { Width = 10, Height = 5 },
-                new View { X = 12, Y = 7, Width = 10, Height = 5 }
-               );
-
-        Assert.Equal (new (30, 20), sv.GetContentSize ());
-        Assert.Equal (2, sv.Subviews [0].Subviews.Count);
-    }
-
-    [Fact (Skip = "#3798 broke - Fix with #3498")]
-    [AutoInitShutdown]
-    public void AutoHideScrollBars_False_ShowHorizontalScrollIndicator_ShowVerticalScrollIndicator ()
-    {
-        var sv = new ScrollView { Width = 10, Height = 10, AutoHideScrollBars = false };
-
-        sv.ShowHorizontalScrollIndicator = true;
-        sv.ShowVerticalScrollIndicator = true;
-
-        var top = new Toplevel ();
-        top.Add (sv);
-        Application.Begin (top);
-
-        Assert.Equal (new (0, 0, 10, 10), sv.Viewport);
-
-        Assert.False (sv.AutoHideScrollBars);
-        Assert.True (sv.ShowHorizontalScrollIndicator);
-        Assert.True (sv.ShowVerticalScrollIndicator);
-        sv.Draw ();
-
-        TestHelpers.AssertDriverContentsAre (
-                                             @"
-         ▲
-         ┬
-         │
-         │
-         │
-         │
-         │
-         ┴
-         ▼
-◄├─────┤► 
-",
-                                             output
-                                            );
-
-        sv.ShowHorizontalScrollIndicator = false;
-        Assert.Equal (new (0, 0, 10, 10), sv.Viewport);
-        sv.ShowVerticalScrollIndicator = true;
-        Assert.Equal (new (0, 0, 10, 10), sv.Viewport);
-
-        Assert.False (sv.AutoHideScrollBars);
-        Assert.False (sv.ShowHorizontalScrollIndicator);
-        Assert.True (sv.ShowVerticalScrollIndicator);
-        top.Layout ();
-        sv.Draw ();
-
-        TestHelpers.AssertDriverContentsAre (
-                                             @"
-         ▲
-         ┬
-         │
-         │
-         │
-         │
-         │
-         │
-         ┴
-         ▼
-",
-                                             output
-                                            );
-
-        sv.ShowHorizontalScrollIndicator = true;
-        sv.ShowVerticalScrollIndicator = false;
-
-        Assert.False (sv.AutoHideScrollBars);
-        Assert.True (sv.ShowHorizontalScrollIndicator);
-        Assert.False (sv.ShowVerticalScrollIndicator);
-        top.Layout ();
-        sv.Draw ();
-
-        TestHelpers.AssertDriverContentsAre (
-                                             @"
-         
-         
-         
-         
-         
-         
-         
-         
-         
-◄├──────┤► 
-",
-                                             output
-                                            );
-
-        sv.ShowHorizontalScrollIndicator = false;
-        sv.ShowVerticalScrollIndicator = false;
-
-        Assert.False (sv.AutoHideScrollBars);
-        Assert.False (sv.ShowHorizontalScrollIndicator);
-        Assert.False (sv.ShowVerticalScrollIndicator);
-        top.Layout ();
-        sv.Draw ();
-
-        TestHelpers.AssertDriverContentsAre (
-                                             @"
-         
-         
-         
-         
-         
-         
-         
-         
-         
-         
-",
-                                             output
-                                            );
-        top.Dispose ();
-    }
-
-    [Fact (Skip = "#3798 broke - Fix with #3498")]
-    [AutoInitShutdown]
-    public void AutoHideScrollBars_ShowHorizontalScrollIndicator_ShowVerticalScrollIndicator ()
-    {
-        var sv = new ScrollView { Width = 10, Height = 10 };
-
-        var top = new Toplevel ();
-        top.Add (sv);
-        Application.Begin (top);
-
-        Assert.True (sv.AutoHideScrollBars);
-        Assert.False (sv.ShowHorizontalScrollIndicator);
-        Assert.False (sv.ShowVerticalScrollIndicator);
-        TestHelpers.AssertDriverContentsWithFrameAre ("", output);
-
-        sv.AutoHideScrollBars = false;
-        sv.ShowHorizontalScrollIndicator = true;
-        sv.ShowVerticalScrollIndicator = true;
-        sv.LayoutSubviews ();
-        sv.Draw ();
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-         ▲
-         ┬
-         │
-         │
-         │
-         │
-         │
-         ┴
-         ▼
-◄├─────┤► 
-",
-                                                      output
-                                                     );
-        top.Dispose ();
-    }
-
-    // There are still issue with the lower right corner of the scroll view
-    [Fact]
-    [AutoInitShutdown (configLocation: ConfigurationManager.ConfigLocations.DefaultOnly)]
-    public void Clear_Window_Inside_ScrollView ()
-    {
-        var topLabel = new Label { X = 15, Text = "At 15,0" };
-
-        var sv = new ScrollView
-        {
-            X = 3,
-            Y = 3,
-            Width = 10,
-            Height = 10,
-            KeepContentAlwaysInViewport = false
-        };
-        sv.SetContentSize (new (23, 23));
-        var bottomLabel = new Label { X = 15, Y = 15, Text = "At 15,15" };
-        var top = new Toplevel ();
-        top.Add (topLabel, sv, bottomLabel);
-        RunState rs = Application.Begin (top);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-               At 15,0 
-                       
-                       
-            ▲          
-            ┬          
-            ┴          
-            ░          
-            ░          
-            ░          
-            ░          
-            ░          
-            ▼          
-   ◄├┤░░░░░►           
-                       
-                       
-               At 15,15",
-                                                      output
-                                                     );
-
-        Attribute [] attributes =
-        {
-            Colors.ColorSchemes ["TopLevel"].Normal,
-            Colors.ColorSchemes ["TopLevel"].Focus,
-            Colors.ColorSchemes ["Base"].Normal
-        };
-
-        TestHelpers.AssertDriverAttributesAre (
-                                               @"
-00000000000000000000000
-00000000000000000000000
-00000000000000000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00011111111110000000000
-00000000000000000000000
-00000000000000000000000
-00000000000000000000000",
-                                               output,
-                                               null,
-                                               attributes
-                                              );
-
-        sv.Add (new Window { X = 3, Y = 3, Width = 20, Height = 20 });
-
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-               At 15,0 
-                       
-                       
-            ▲          
-            ┬          
-            ┴          
-      ┌─────░          
-      │     ░          
-      │     ░          
-      │     ░          
-      │     ░          
-      │     ▼          
-   ◄├┤░░░░░►           
-                       
-                       
-               At 15,15",
-                                                      output
-                                                     );
-
-        TestHelpers.AssertDriverAttributesAre (
-                                               @"
-00000000000000000000000
-00000000000000000000000
-00000000000000000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000022222210000000000
-00000022222210000000000
-00000022222210000000000
-00000022222210000000000
-00000022222210000000000
-00000022222210000000000
-00011111111110000000000
-00000000000000000000000
-00000000000000000000000
-00000000000000000000000",
-                                               output,
-                                               null,
-                                               attributes
-                                              );
-
-        sv.ContentOffset = new (20, 20);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-               At 15,0 
-                       
-                       
-     │      ▲          
-     │      ░          
-   ──┘      ░          
-            ░          
-            ░          
-            ┬          
-            │          
-            ┴          
-            ▼          
-   ◄░░░░├─┤►           
-                       
-                       
-               At 15,15",
-                                                      output
-                                                     );
-
-        TestHelpers.AssertDriverAttributesAre (
-                                               @"
-00000000000000000000000
-00000000000000000000000
-00000000000000000000000
-00022200000010000000000
-00022200000010000000000
-00022200000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00000000000010000000000
-00011111111110000000000
-00000000000000000000000
-00000000000000000000000
-00000000000000000000000",
-                                               output,
-                                               null,
-                                               attributes
-                                              );
-        top.Dispose ();
-    }
-
-    [Fact]
-    public void Constructors_Defaults ()
-    {
-        var sv = new ScrollView ();
-        Assert.True (sv.CanFocus);
-        Assert.Equal (new (0, 0, 0, 0), sv.Frame);
-        Assert.Equal (Rectangle.Empty, sv.Frame);
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.Equal (Size.Empty, sv.GetContentSize ());
-        Assert.True (sv.AutoHideScrollBars);
-        Assert.True (sv.KeepContentAlwaysInViewport);
-
-        sv = new () { X = 1, Y = 2, Width = 20, Height = 10 };
-        Assert.True (sv.CanFocus);
-        Assert.Equal (new (1, 2, 20, 10), sv.Frame);
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.Equal (sv.Viewport.Size, sv.GetContentSize ());
-        Assert.True (sv.AutoHideScrollBars);
-        Assert.True (sv.KeepContentAlwaysInViewport);
-    }
-
-    [Fact]
-    [SetupFakeDriver]
-    public void ContentBottomRightCorner_Draw ()
-    {
-        ((FakeDriver)Application.Driver!).SetBufferSize (30, 30);
-
-        var top = new View { Width = 30, Height = 30, ColorScheme = new () { Normal = Attribute.Default } };
-
-        Size size = new (20, 10);
-
-        var sv = new ScrollView
-        {
-            X = 1,
-            Y = 1,
-            Width = 10,
-            Height = 5,
-            ColorScheme = new () { Normal = new (Color.Red, Color.Green) }
-        };
-        sv.SetContentSize (size);
-        string text = null;
-
-        for (var i = 0; i < size.Height; i++)
-        {
-            text += "*".Repeat (size.Width);
-
-            if (i < size.Height)
-            {
-                text += '\n';
-            }
-        }
-
-        var view = new View
-        {
-            ColorScheme = new () { Normal = new (Color.Blue, Color.Yellow) },
-            Width = Dim.Auto (DimAutoStyle.Text),
-            Height = Dim.Auto (DimAutoStyle.Text),
-            Text = text
-        };
-        sv.Add (view);
-
-        top.Add (sv);
-        top.BeginInit ();
-        top.EndInit ();
-
-        top.LayoutSubviews ();
-
-        View.ClipToScreen ();
-        top.Draw ();
-
-        View contentBottomRightCorner = sv.Subviews.First (v => v is ScrollBarView.ContentBottomRightCorner);
-        Assert.True (contentBottomRightCorner is ScrollBarView.ContentBottomRightCorner);
-        Assert.True (contentBottomRightCorner.Visible);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
- *********▲
- *********┬
- *********┴
- *********▼
- ◄├──┤░░░► ",
-                                                      output
-                                                     );
-
-        Attribute [] attrs = { Attribute.Default, new (Color.Red, Color.Green), new (Color.Blue, Color.Yellow) };
-
-        TestHelpers.AssertDriverAttributesAre (
-                                               @"
-000000000000
-022222222210
-022222222210
-022222222210
-022222222210
-011111111110
-000000000000",
-                                               output,
-                                               null,
-                                               attrs
-                                              );
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void
-        ContentOffset_ContentSize_AutoHideScrollBars_ShowHorizontalScrollIndicator_ShowVerticalScrollIndicator ()
-    {
-        var sv = new ScrollView
-        {
-            Width = 10, Height = 10
-        };
-        sv.SetContentSize (new (50, 50));
-        sv.ContentOffset = new (25, 25);
-
-        var top = new Toplevel ();
-        top.Add (sv);
-        Application.Begin (top);
-        Application.LayoutAndDrawToplevels ();
-
-        Assert.Equal (new (-25, -25), sv.ContentOffset);
-        Assert.Equal (new (50, 50), sv.GetContentSize ());
-        Assert.True (sv.AutoHideScrollBars);
-        Assert.True (sv.ShowHorizontalScrollIndicator);
-        Assert.True (sv.ShowVerticalScrollIndicator);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-         ▲
-         ░
-         ░
-         ░
-         ┬
-         │
-         ┴
-         ░
-         ▼
-◄░░░├─┤░► 
-",
-                                                      output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void ContentSize_AutoHideScrollBars_ShowHorizontalScrollIndicator_ShowVerticalScrollIndicator ()
-    {
-        var sv = new ScrollView { Width = 10, Height = 10 };
-        sv.SetContentSize (new (50, 50));
-
-        var top = new Toplevel ();
-        top.Add (sv);
-        Application.Begin (top);
-        Application.LayoutAndDrawToplevels ();
-
-        Assert.Equal (50, sv.GetContentSize ().Width);
-        Assert.Equal (50, sv.GetContentSize ().Height);
-        Assert.True (sv.AutoHideScrollBars);
-        Assert.True (sv.ShowHorizontalScrollIndicator);
-        Assert.True (sv.ShowVerticalScrollIndicator);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-         ▲
-         ┬
-         ┴
-         ░
-         ░
-         ░
-         ░
-         ░
-         ▼
-◄├┤░░░░░► 
-",
-                                                      output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void DrawTextFormatter_Respects_The_Clip_Bounds ()
-    {
-        var rule = "0123456789";
-        Size size = new (40, 40);
-        var view = new View { Frame = new (Point.Empty, size) };
-
-        view.Add (
-                  new Label
-                  {
-                      Width = Dim.Fill (),
-                      Height = 1,
-                      Text = rule.Repeat (size.Width / rule.Length)
-                  }
-                 );
-
-        view.Add (
-                  new Label
-                  {
-                      Height = Dim.Fill (),
-                      Width = 1,
-                      Text = rule.Repeat (size.Height / rule.Length),
-                      TextDirection = TextDirection.TopBottom_LeftRight
-                  }
-                 );
-        view.Add (new Label { X = 1, Y = 1, Text = "[ Press me! ]" });
-
-        var scrollView = new ScrollView
-        {
-            X = 1,
-            Y = 1,
-            Width = 15,
-            Height = 10,
-            ShowHorizontalScrollIndicator = true,
-            ShowVerticalScrollIndicator = true
-        };
-        scrollView.SetContentSize (size);
-        scrollView.Add (view);
-        var win = new Window { X = 1, Y = 1, Width = 20, Height = 14 };
-        win.Add (scrollView);
-        var top = new Toplevel ();
-        top.Add (win);
-        RunState rs = Application.Begin (top);
-        Application.RunIteration(ref rs);
-
-        var expected = @"
- ┌──────────────────┐
- │                  │
- │ 01234567890123▲  │
- │ 1[ Press me! ]┬  │
- │ 2             │  │
- │ 3             ┴  │
- │ 4             ░  │
- │ 5             ░  │
- │ 6             ░  │
- │ 7             ░  │
- │ 8             ▼  │
- │ ◄├───┤░░░░░░░►   │
- │                  │
- └──────────────────┘
-"
-            ;
-
-        Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorRight));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 12345678901234▲  │
- │ [ Press me! ] ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄├───┤░░░░░░░►   │
- │                  │
- └──────────────────┘
-"
-            ;
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorRight));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 23456789012345▲  │
- │  Press me! ]  ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄├────┤░░░░░░►   │
- │                  │
- └──────────────────┘
-"
-            ;
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorRight));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 34567890123456▲  │
- │ Press me! ]   ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄├────┤░░░░░░►   │
- │                  │
- └──────────────────┘
-"
-            ;
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorRight));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 45678901234567▲  │
- │ ress me! ]    ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄░├───┤░░░░░░►   │
- │                  │
- └──────────────────┘
-"
-            ;
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorRight));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 56789012345678▲  │
- │ ess me! ]     ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄░├────┤░░░░░►   │
- │                  │
- └──────────────────┘
-"
-            ;
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorRight));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 67890123456789▲  │
- │ ss me! ]      ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄░├────┤░░░░░►   │
- │                  │
- └──────────────────┘
-"
-            ;
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorRight));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 78901234567890▲  │
- │ s me! ]       ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄░░├───┤░░░░░►   │
- │                  │
- └──────────────────┘
-";
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.End.WithCtrl));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 67890123456789▲  │
- │               ┬  │
- │               │  │
- │               ┴  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ░  │
- │               ▼  │
- │ ◄░░░░░░░├───┤►   │
- │                  │
- └──────────────────┘
-";
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.Home.WithCtrl));
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorDown));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 1[ Press me! ]▲  │
- │ 2             ┬  │
- │ 3             │  │
- │ 4             ┴  │
- │ 5             ░  │
- │ 6             ░  │
- │ 7             ░  │
- │ 8             ░  │
- │ 9             ▼  │
- │ ◄├───┤░░░░░░░►   │
- │                  │
- └──────────────────┘
-";
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorDown));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 2             ▲  │
- │ 3             ┬  │
- │ 4             │  │
- │ 5             ┴  │
- │ 6             ░  │
- │ 7             ░  │
- │ 8             ░  │
- │ 9             ░  │
- │ 0             ▼  │
- │ ◄├───┤░░░░░░░►   │
- │                  │
- └──────────────────┘
-";
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.CursorDown));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 3             ▲  │
- │ 4             ┬  │
- │ 5             │  │
- │ 6             ┴  │
- │ 7             ░  │
- │ 8             ░  │
- │ 9             ░  │
- │ 0             ░  │
- │ 1             ▼  │
- │ ◄├───┤░░░░░░░►   │
- │                  │
- └──────────────────┘
-";
-
-        pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
-        Assert.Equal (new (1, 1, 21, 14), pos);
-
-        Assert.True (scrollView.NewKeyDownEvent (Key.End));
-        Application.RunIteration (ref rs);
-
-        expected = @"
- ┌──────────────────┐
- │                  │
- │ 1             ▲  │
- │ 2             ░  │
- │ 3             ░  │
- │ 4             ░  │
- │ 5             ░  │
- │ 6             ░  │
- │ 7             ┬  │
- │ 8             ┴  │
- │ 9             ▼  │
- │ ◄├───┤░░░░░░░►   │
- │                  │
- └──────────────────┘
-";
-
-        TestHelpers.AssertDriverContentsAre (expected, output);
-
-        top.Dispose ();
-    }
-
-    // There still have an issue with lower right corner of the scroll view
-    [Fact]
-    [AutoInitShutdown]
-    public void Frame_And_Labels_Does_Not_Overspill_ScrollView ()
-    {
-        var sv = new ScrollView
-        {
-            X = 3,
-            Y = 3,
-            Width = 10,
-            Height = 10,
-            TabStop = TabBehavior.TabStop
-        };
-        sv.SetContentSize (new (50, 50));
-
-        for (var i = 0; i < 8; i++)
-        {
-            sv.Add (new CustomButton ("█", $"Button {i}", 20, 3) { Y = i * 3 });
-        }
-
-        var top = new Toplevel ();
-        top.Add (sv);
-        RunState rs = Application.Begin (top);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-   █████████▲
-   ██████But┬
-   █████████┴
-   ┌────────░
-   │     But░
-   └────────░
-   ┌────────░
-   │     But░
-   └────────▼
-   ◄├┤░░░░░► ",
-                                                      output
-                                                     );
-
-        sv.ContentOffset = new (5, 5);
-        Application.RunIteration (ref rs);
-
-        TestHelpers.AssertDriverContentsWithFrameAre (
-                                                      @"
-   ─────────▲
-   ─────────┬
-    Button 2│
-   ─────────┴
-   ─────────░
-    Button 3░
-   ─────────░
-   ─────────░
-    Button 4▼
-   ◄├─┤░░░░► ",
-                                                      output
-                                                     );
-        top.Dispose ();
-    }
-
-    [Fact]
-    public void KeyBindings_Command ()
-    {
-        var sv = new ScrollView { Width = 20, Height = 10 };
-        sv.SetContentSize (new (40, 20));
-
-        sv.Add (
-                new View { Width = 20, Height = 5 },
-                new View { X = 22, Y = 7, Width = 10, Height = 5 }
-               );
-
-        sv.BeginInit ();
-        sv.EndInit ();
-
-        Assert.True (sv.KeepContentAlwaysInViewport);
-        Assert.True (sv.AutoHideScrollBars);
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorUp));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorDown));
-        Assert.Equal (new (0, -1), sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorUp));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.PageUp));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.PageDown));
-        Point point0xMinus10 = new (0, -10);
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.PageDown));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorDown));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.V.WithAlt));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.V.WithCtrl));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorLeft));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (new (-1, -10), sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorLeft));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.PageUp.WithCtrl));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.PageDown.WithCtrl));
-        Point pointMinus20xMinus10 = new (-20, -10);
-        Assert.Equal (pointMinus20xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (pointMinus20xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.Home));
-        Point pointMinus20x0 = new (-20, 0);
-        Assert.Equal (pointMinus20x0, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.Home));
-        Assert.Equal (pointMinus20x0, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.End));
-        Assert.Equal (pointMinus20xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.End));
-        Assert.Equal (pointMinus20xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.Home.WithCtrl));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.Home.WithCtrl));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.End.WithCtrl));
-        Assert.Equal (pointMinus20xMinus10, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.End.WithCtrl));
-        Assert.Equal (pointMinus20xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.Home));
-        Assert.Equal (pointMinus20x0, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.Home.WithCtrl));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-
-        sv.KeepContentAlwaysInViewport = false;
-        Assert.False (sv.KeepContentAlwaysInViewport);
-        Assert.True (sv.AutoHideScrollBars);
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorUp));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorDown));
-        Assert.Equal (new (0, -1), sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorUp));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.PageUp));
-        Assert.Equal (Point.Empty, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.PageDown));
-        Assert.Equal (point0xMinus10, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.PageDown));
-        Point point0xMinus19 = new (0, -19);
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.PageDown));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorDown));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.V.WithAlt));
-        Assert.Equal (new (0, -9), sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.V.WithCtrl));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorLeft));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (new (-1, -19), sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.CursorLeft));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.PageUp.WithCtrl));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.PageDown.WithCtrl));
-        Assert.Equal (new (-20, -19), sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.PageDown.WithCtrl));
-        Point pointMinus39xMinus19 = new (-39, -19);
-        Assert.Equal (pointMinus39xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.PageDown.WithCtrl));
-        Assert.Equal (pointMinus39xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.CursorRight));
-        Assert.Equal (pointMinus39xMinus19, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.PageUp.WithCtrl));
-        var pointMinus19xMinus19 = new Point (-19, -19);
-        Assert.Equal (pointMinus19xMinus19, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.Home));
-        Assert.Equal (new (-19, 0), sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.Home));
-        Assert.Equal (new (-19, 0), sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.End));
-        Assert.Equal (pointMinus19xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.End));
-        Assert.Equal (pointMinus19xMinus19, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.Home.WithCtrl));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.Home.WithCtrl));
-        Assert.Equal (point0xMinus19, sv.ContentOffset);
-        Assert.True (sv.NewKeyDownEvent (Key.End.WithCtrl));
-        Assert.Equal (pointMinus39xMinus19, sv.ContentOffset);
-        Assert.False (sv.NewKeyDownEvent (Key.End.WithCtrl));
-        Assert.Equal (pointMinus39xMinus19, sv.ContentOffset);
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void Remove_Added_View_Is_Allowed ()
-    {
-        var sv = new ScrollView { Width = 20, Height = 20 };
-        sv.SetContentSize (new (100, 100));
-
-        sv.Add (
-                new View { Width = Dim.Fill (), Height = Dim.Fill (50), Id = "View1" },
-                new View { Y = 51, Width = Dim.Fill (), Height = Dim.Fill (), Id = "View2" }
-               );
-
-        var top = new Toplevel ();
-        top.Add (sv);
-        Application.Begin (top);
-        Application.LayoutAndDrawToplevels ();
-
-        Assert.Equal (4, sv.Subviews.Count);
-        Assert.Equal (2, sv.Subviews [0].Subviews.Count);
-
-        sv.Remove (sv.Subviews [0].Subviews [1]);
-        Assert.Equal (4, sv.Subviews.Count);
-        Assert.Single (sv.Subviews [0].Subviews);
-        Assert.Equal ("View1", sv.Subviews [0].Subviews [0].Id);
-        top.Dispose ();
-    }
-
-    private class CustomButton : FrameView
-    {
-        private readonly Label labelFill;
-        private readonly Label labelText;
-
-        public CustomButton (string fill, string text, int width, int height)
-        {
-            Width = width;
-            Height = height;
-
-            labelFill = new () { Width = Dim.Fill (), Height = Dim.Fill (), Visible = false };
-
-            labelFill.SubviewsLaidOut += (s, e) =>
-                                        {
-                                            var fillText = new StringBuilder ();
-
-                                            for (var i = 0; i < labelFill.Viewport.Height; i++)
-                                            {
-                                                if (i > 0)
-                                                {
-                                                    fillText.AppendLine ("");
-                                                }
-
-                                                for (var j = 0; j < labelFill.Viewport.Width; j++)
-                                                {
-                                                    fillText.Append (fill);
-                                                }
-                                            }
-
-                                            labelFill.Text = fillText.ToString ();
-                                        };
-
-            labelText = new () { X = Pos.Center (), Y = Pos.Center (), Text = text };
-            Add (labelFill, labelText);
-            CanFocus = true;
-        }
-
-        protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View previousFocusedView, [CanBeNull] View focusedView)
-        {
-            if (newHasFocus)
-            {
-                Border.LineStyle = LineStyle.None;
-                Border.Thickness = new (0);
-                labelFill.Visible = true;
-            }
-            else
-            {
-                Border.LineStyle = LineStyle.Single;
-                Border.Thickness = new (1);
-                labelFill.Visible = false;
-            }
-        }
-    }
-#endif 
-}

+ 209 - 77
UnitTests/Views/TabViewTests.cs

@@ -3,7 +3,6 @@ using Xunit.Abstractions;
 
 namespace Terminal.Gui.ViewsTests;
 
-#if foo
 public class TabViewTests (ITestOutputHelper output)
 {
     [Fact]
@@ -113,8 +112,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 20;
         tv.Height = 5;
 
-        tv.Layout ();
-
         tv.Draw ();
 
         View tabRow = tv.Subviews [0];
@@ -146,21 +143,21 @@ public class TabViewTests (ITestOutputHelper output)
         {
             args = new () { ScreenPosition = new (i, 1), Flags = MouseFlags.ReportMousePosition };
             Application.RaiseMouseEvent (args);
-            Application.LayoutAndDrawToplevels ();
+            Application.LayoutAndDraw ();
             Assert.Null (clicked);
             Assert.Equal (tab1, tv.SelectedTab);
         }
 
         args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, clicked);
         Assert.Equal (tab1, tv.SelectedTab);
 
         // Click to tab2
         args = new () { ScreenPosition = new (6, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, clicked);
         Assert.Equal (tab2, tv.SelectedTab);
 
@@ -173,7 +170,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         args = new () { ScreenPosition = new (3, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
 
         // Tab 1 was clicked but event handler blocked navigation
         Assert.Equal (tab1, clicked);
@@ -181,7 +178,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         args = new () { ScreenPosition = new (12, 1), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
 
         // Clicking beyond last tab should raise event with null Tab
         Assert.Null (clicked);
@@ -198,8 +195,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 7;
         tv.Height = 5;
 
-        tv.LayoutSubviews ();
-
         tv.Draw ();
 
         View tabRow = tv.Subviews [0];
@@ -236,7 +231,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the right arrow
         var args = new MouseEventArgs { ScreenPosition = new (6, 2), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
@@ -256,7 +251,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the left arrow
         args = new () { ScreenPosition = new (0, 2), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
@@ -286,8 +281,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         Assert.Equal (LineStyle.None, tv.BorderStyle);
         tv.BorderStyle = LineStyle.Single;
-
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
         tv.Draw ();
 
@@ -327,7 +321,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the right arrow
         var args = new MouseEventArgs { ScreenPosition = new (7, 3), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
@@ -349,7 +343,7 @@ public class TabViewTests (ITestOutputHelper output)
         // Click the left arrow
         args = new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked };
         Application.RaiseMouseEvent (args);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Null (clicked);
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
@@ -400,7 +394,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         // Press the cursor up key to focus the selected tab
         Application.RaiseKeyDownEvent (Key.CursorUp);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
 
         // Is the selected tab focused
         Assert.Equal (tab1, tv.SelectedTab);
@@ -418,7 +412,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         // Press the cursor right key to select the next tab
         Application.RaiseKeyDownEvent (Key.CursorRight);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, tv.SelectedTab);
@@ -476,7 +470,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         // Press the cursor left key to select the previous tab
         Application.RaiseKeyDownEvent (Key.CursorLeft);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, tv.SelectedTab);
@@ -486,7 +480,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         // Press the end key to select the last tab
         Application.RaiseKeyDownEvent (Key.End);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, tv.SelectedTab);
@@ -495,7 +489,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         // Press the home key to select the first tab
         Application.RaiseKeyDownEvent (Key.Home);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, tv.SelectedTab);
@@ -504,7 +498,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         // Press the page down key to select the next set of tabs
         Application.RaiseKeyDownEvent (Key.PageDown);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab1, oldChanged);
         Assert.Equal (tab2, newChanged);
         Assert.Equal (tab2, tv.SelectedTab);
@@ -513,7 +507,7 @@ public class TabViewTests (ITestOutputHelper output)
 
         // Press the page up key to select the previous set of tabs
         Application.RaiseKeyDownEvent (Key.PageUp);
-        Application.LayoutAndDrawToplevels ();
+        Application.LayoutAndDraw ();
         Assert.Equal (tab2, oldChanged);
         Assert.Equal (tab1, newChanged);
         Assert.Equal (tab1, tv.SelectedTab);
@@ -610,7 +604,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.ApplyStyleChanges ();
         tv.Layout ();
 
-        View.ClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -633,7 +626,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false };
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
         tv.Draw ();
 
@@ -658,13 +651,13 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Style = new () { ShowTopLine = false };
         tv.ApplyStyleChanges ();
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
 
+        // Ensures that the tab bar subview gets the bounds of the parent TabView
+        tv.Layout ();
+
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -678,8 +671,10 @@ public class TabViewTests (ITestOutputHelper output)
                                                      );
 
         tv.SelectedTab = tab2;
+        Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused);
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -697,8 +692,8 @@ public class TabViewTests (ITestOutputHelper output)
         // Test first tab name too long
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
-
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -713,9 +708,10 @@ public class TabViewTests (ITestOutputHelper output)
 
         //switch to tab2
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
-        tv.Draw ();
 
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
 │13│      
@@ -730,9 +726,9 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
-
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
 │abcdefg│ 
@@ -753,9 +749,8 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
-        View.ClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -778,7 +773,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
         tv.Draw ();
 
@@ -802,15 +797,13 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Style = new () { ShowTopLine = false, TabsOnBottom = true };
         tv.ApplyStyleChanges ();
+        tv.Layout ();
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
-        View.ClipToScreen ();
 
+        tv.Layout ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -824,8 +817,10 @@ public class TabViewTests (ITestOutputHelper output)
                                                      );
 
         tv.SelectedTab = tab2;
+        Assert.Equal (tab2, tv.Subviews.First (v => v.Id.Contains ("tabRowView")).MostFocused);
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -844,7 +839,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -859,7 +855,9 @@ public class TabViewTests (ITestOutputHelper output)
 
         //switch to tab2
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
+
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -876,7 +874,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -899,7 +898,6 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Layout ();
 
-        View.ClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -922,7 +920,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Layout ();
 
-        View.ClipToScreen ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -944,14 +942,11 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 10;
         tv.Height = 5;
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
 
-        View.ClipToScreen ();
+        tv.Layout ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -966,7 +961,8 @@ public class TabViewTests (ITestOutputHelper output)
 
         tv.SelectedTab = tab2;
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -985,7 +981,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1000,7 +997,9 @@ public class TabViewTests (ITestOutputHelper output)
 
         //switch to tab2
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
+
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1017,7 +1016,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1039,13 +1039,11 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Width = 20;
         tv.Height = 5;
 
-        tv.LayoutSubviews ();
-
         tab1.DisplayText = "Tab0";
 
         tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables";
 
-        View.ClipToScreen ();
+        tv.Layout ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1060,7 +1058,8 @@ public class TabViewTests (ITestOutputHelper output)
 
         tv.SelectedTab = tab2;
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1083,7 +1082,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
         tv.Draw ();
 
@@ -1107,7 +1106,7 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
-        tv.LayoutSubviews ();
+        tv.Layout ();
 
         tv.Draw ();
 
@@ -1131,15 +1130,13 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Height = 5;
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
+        tv.Layout ();
 
-        // Ensures that the tab bar subview gets the bounds of the parent TabView
-        tv.LayoutSubviews ();
-
-        // Test two tab names that fit 
+        // Test two tab names that fit
         tab1.DisplayText = "12";
         tab2.DisplayText = "13";
-        View.ClipToScreen ();
 
+        tv.Layout ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1156,7 +1153,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "13";
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1171,7 +1169,9 @@ public class TabViewTests (ITestOutputHelper output)
 
         //switch to tab2
         tv.SelectedTab = tab2;
-        View.ClipToScreen ();
+
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1188,7 +1188,8 @@ public class TabViewTests (ITestOutputHelper output)
         tab1.DisplayText = "12345678910";
         tab2.DisplayText = "abcdefghijklmnopq";
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1212,12 +1213,11 @@ public class TabViewTests (ITestOutputHelper output)
         tv.Style = new () { TabsOnBottom = true };
         tv.ApplyStyleChanges ();
 
-        tv.LayoutSubviews ();
-
         tab1.DisplayText = "Tab0";
 
         tab2.DisplayText = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables";
 
+        tv.Layout ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1232,7 +1232,8 @@ public class TabViewTests (ITestOutputHelper output)
 
         tv.SelectedTab = tab2;
 
-        View.ClipToScreen ();
+        tv.Layout ();
+        View.SetClipToScreen ();
         tv.Draw ();
 
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -1322,6 +1323,138 @@ public class TabViewTests (ITestOutputHelper output)
         Application.Shutdown ();
     }
 
+    [Fact]
+    [SetupFakeDriver]
+    public void Add_Three_TabsOnTop_ChangesTab ()
+    {
+        TabView tv = GetTabView (out Tab tab1, out Tab tab2, false);
+        Tab tab3;
+
+        tv.AddTab (
+                   tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } },
+                   false);
+
+        tv.Width = 20;
+        tv.Height = 5;
+
+        tv.Layout ();
+        tv.Draw ();
+
+        Assert.Equal (tab1, tv.SelectedTab);
+
+        TestHelpers.AssertDriverContentsAre (
+                                             @"
+╭────┬────┬────╮
+│Tab1│Tab2│Tab3│
+│    ╰────┴────┴───╮
+│hi                │
+└──────────────────┘
+",
+                                             output
+                                            );
+
+        tv.SelectedTab = tab2;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+╭────┬────┬────╮    
+│Tab1│Tab2│Tab3│    
+├────╯    ╰────┴───╮
+│hi2               │
+└──────────────────┘
+",
+                                                      output
+                                                     );
+
+        tv.SelectedTab = tab3;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+╭────┬────┬────╮    
+│Tab1│Tab2│Tab3│    
+├────┴────╯    ╰───╮
+│hi3               │
+└──────────────────┘
+",
+                                                      output
+                                                     );
+    }
+
+    [Fact]
+    [SetupFakeDriver]
+    public void Add_Three_TabsOnBottom_ChangesTab ()
+    {
+        TabView tv = GetTabView (out Tab tab1, out Tab tab2, false);
+        Tab tab3;
+
+        tv.AddTab (
+                   tab3 = new () { Id = "tab3", DisplayText = "Tab3", View = new TextView { Id = "tab3.TextView", Width = 3, Height = 1, Text = "hi3" } },
+                   false);
+
+        tv.Width = 20;
+        tv.Height = 5;
+        tv.Style = new () { TabsOnBottom = true };
+        tv.ApplyStyleChanges ();
+
+        tv.Layout ();
+        tv.Draw ();
+
+        Assert.Equal (tab1, tv.SelectedTab);
+
+        TestHelpers.AssertDriverContentsAre (
+                                             @"
+┌──────────────────┐
+│hi                │
+│    ╭────┬────┬───╯
+│Tab1│Tab2│Tab3│
+╰────┴────┴────╯
+",
+                                             output
+                                            );
+
+        tv.SelectedTab = tab2;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+┌──────────────────┐
+│hi2               │
+├────╮    ╭────┬───╯
+│Tab1│Tab2│Tab3│    
+╰────┴────┴────╯    
+",
+                                                      output
+                                                     );
+
+        tv.SelectedTab = tab3;
+
+        tv.Layout ();
+        View.SetClipToScreen ();
+        tv.Draw ();
+
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+┌──────────────────┐
+│hi3               │
+├────┬────╮    ╭───╯
+│Tab1│Tab2│Tab3│    
+╰────┴────┴────╯    
+",
+                                                      output
+                                                     );
+    }
+
     private TabView GetTabView () { return GetTabView (out _, out _); }
 
     private TabView GetTabView (out Tab tab1, out Tab tab2, bool initFakeDriver = true)
@@ -1355,4 +1488,3 @@ public class TabViewTests (ITestOutputHelper output)
         driver.Init ();
     }
 }
-#endif

+ 0 - 55
UnitTests/Views/ToplevelTests.cs

@@ -720,61 +720,6 @@ public partial class ToplevelTests (ITestOutputHelper output)
         top.Dispose ();
     }
 
-    [Fact]
-    [AutoInitShutdown]
-    public void Toplevel_Inside_ScrollView_MouseGrabView ()
-    {
-        var scrollView = new ScrollView
-        {
-            X = 3,
-            Y = 3,
-            Width = 40,
-            Height = 16
-        };
-        scrollView.SetContentSize (new (200, 100));
-        var win = new Window { X = 3, Y = 3, Width = Dim.Fill (3), Height = Dim.Fill (3), Arrangement = ViewArrangement.Movable };
-        scrollView.Add (win);
-        Toplevel top = new ();
-        top.Add (scrollView);
-        Application.Begin (top);
-
-        Assert.Equal (new (0, 0, 80, 25), top.Frame);
-        Assert.Equal (new (3, 3, 40, 16), scrollView.Frame);
-        Assert.Equal (new (0, 0, 200, 100), scrollView.Subviews [0].Frame);
-        Assert.Equal (new (3, 3, 194, 94), win.Frame);
-
-        Application.RaiseMouseEvent (new () { ScreenPosition = new (6, 6), Flags = MouseFlags.Button1Pressed });
-        Assert.Equal (win.Border, Application.MouseGrabView);
-        Assert.Equal (new (3, 3, 194, 94), win.Frame);
-
-        Application.RaiseMouseEvent (new () { ScreenPosition = new (9, 9), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition });
-        Assert.Equal (win.Border, Application.MouseGrabView);
-        top.SetNeedsLayout ();
-        top.LayoutSubviews ();
-        Assert.Equal (new (6, 6, 191, 91), win.Frame);
-        Application.LayoutAndDraw ();
-
-        Application.RaiseMouseEvent (
-                                  new ()
-                                  {
-                                      ScreenPosition = new (5, 5), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition
-                                  });
-        Assert.Equal (win.Border, Application.MouseGrabView);
-        top.SetNeedsLayout ();
-        top.LayoutSubviews ();
-        Assert.Equal (new (2, 2, 195, 95), win.Frame);
-        Application.LayoutAndDraw ();
-
-        Application.RaiseMouseEvent (new () { ScreenPosition = new (5, 5), Flags = MouseFlags.Button1Released });
-
-        // ScrollView always grab the mouse when the container's subview OnMouseEnter don't want grab the mouse
-        Assert.Equal (scrollView, Application.MouseGrabView);
-
-        Application.RaiseMouseEvent (new () { ScreenPosition = new (4, 4), Flags = MouseFlags.ReportMousePosition });
-        Assert.Equal (scrollView, Application.MouseGrabView);
-        top.Dispose ();
-    }
-
     [Fact]
     [AutoInitShutdown]
     public void Window_Viewport_Bigger_Than_Driver_Cols_And_Rows_Allow_Drag_Beyond_Left_Right_And_Bottom ()

+ 6 - 1
docfx/docs/View.md

@@ -14,7 +14,7 @@
   
   * *Parent View* - A view that holds a reference to another view in a parent/child relationship, but is NOT a SuperView of the child. Terminal.Gui uses the terms "Child" and "Parent" sparingly. Generally Subview/SuperView is preferred.
   
-### Layout
+### Layout and Arrangement
 
 See the [Layout Deep Dive](layout.md) and the [Arrangement Deep Dive](arrangement.md).
 
@@ -26,6 +26,11 @@ See the [Drawing Deep Dive](drawing.md).
 
 See the [Navigation Deep Dive](navigation.md).
 
+### Scrolling
+
+See the [Scrolling Deep Dive](scrolling.md).
+
+
 ### Application Concepts 
 
   * *TopLevel* - The v1 term used to describe a view that can have a MenuBar and/or StatusBar. In v2, we will delete the `TopLevel` class and ensure ANY View can have a menu bar and/or status bar (via `Adornments`).

+ 1 - 1
docfx/docs/arrangement.md

@@ -5,7 +5,7 @@ Terminal.Gui provides a feature of Layout known as **Arrangement**, which contro
 
 * **Arrangement** - Describes the feature of [Layout](layout.md) which controls how the user can use the mouse and keyboard to arrange views and enables either **Tiled** or **Overlapped** layouts.
 
-* **Arrange Mode** - When a user presses `Ctrl+F5` (configurable via the @Terminal.Gui.Application.ArrangeKey) the application goes into **Arrange Mode**. In this mode, indicators are displayed on an arrangeable view indicating which aspect of the View can be arranged. If @Terminal.Gui.ViewArrangement.Movable, a `◊` will be displayed in the top-left corner of the @Terminal.Gui.View.Border. If @Terminal.Gui.ViewArrangement.Resizable , pressing `Tab` (or `Shift+Tab`) will cycle to an an indictor in the bottom-right corner of the Border. The up/down/left/right cursor keys will act appropriately. `Esc`, `Ctrl+F5` or clicking outside of the Border will exit Arrange Mode.
+* **Arrange Mode** - The Arrange Modes are set via @Terminal.Gui.View.ViewArrangement. When a user presses `Ctrl+F5` (configurable via the @Terminal.Gui.Application.ArrangeKey) the application goes into **Arrange Mode**. In this mode, indicators are displayed on an arrangeable view indicating which aspect of the View can be arranged. If @Terminal.Gui.ViewArrangement.Movable, a `◊` will be displayed in the top-left corner of the @Terminal.Gui.View.Border. If @Terminal.Gui.ViewArrangement.Resizable , pressing `Tab` (or `Shift+Tab`) will cycle to an an indictor in the bottom-right corner of the Border. The up/down/left/right cursor keys will act appropriately. `Esc`, `Ctrl+F5` or clicking outside of the Border will exit Arrange Mode.
 
 * **Modal** - A modal view is one that is run as an "application" via @Terminal.Gui.Application.Run(System.Func{System.Exception,System.Boolean},Terminal.Gui.ConsoleDriver) where `Modal == true`. `Dialog`, `Messagebox`, and `Wizard` are the prototypical examples. When run this way, there IS a `z-order` but it is highly-constrained: the modal view has a z-order of 1 and everything else is at 0. 
 

+ 1 - 20
docfx/docs/index.md

@@ -91,7 +91,7 @@ var button = new Button () {
     Width = Dim.Fill (),
     Height = Dim.Fill () - 1
 };
-button.Clicked += () => {
+button.Accepting += () => {
     MessageBox.Query (50, 5, "Hi", "Hello World! This is a message box", "Ok");
 };
 
@@ -143,26 +143,7 @@ Views can either be Modal or Non-modal. Modal views take over all user input unt
 To run any View (but especially Dialogs, Windows, or Toplevels) modally, invoke the `Application.Run` method on a Toplevel. Use the `Application.RequestStop()` method to terminate the modal execution.
 
 ```csharp
-bool okpressed = false;
-var ok = new Button(3, 14, "Ok") { 
-    Clicked = () => { Application.RequestStop (); okpressed = true; }
-};
-var cancel = new Button(10, 14, "Cancel") {
-    Clicked = () => Application.RequestStop () 
-};
-var dialog = new Dialog ("Login", 60, 18, ok, cancel);
 
-var entry = new TextField () {
-    X = 1, 
-    Y = 1,
-    Width = Dim.Fill (),
-    Height = 1
-};
-dialog.Add (entry);
-Application.Run (dialog);
-if (okpressed)
-    Console.WriteLine ("The user entered: " + entry.Text);
-dialog.Dispose ();
 ```
 
 There is no return value from running modally, so the modal view must have a mechanism to indicate the reason the modal was closed. In the case above, the `okpressed` value is set to true if the user pressed or selected the `Ok` button.

+ 4 - 3
docfx/docs/layout.md

@@ -2,7 +2,7 @@
 
 Terminal.Gui provides a rich system for how [View](View.md) objects are laid out relative to each other. The layout system also defines how coordinates are specified.
 
-See [View Deep Dive](View.md) and [Arrangement Deep Dive](arrangement.md) for more.
+See [View Deep Dive](View.md), [Arrangement Deep Dive](arrangement.md), [Scrolling Deep Dive](scrolling.md), and [Drawing Deep Dive](drawing.md) for more.
 
 ## Lexicon & Taxonomy
 
@@ -80,10 +80,11 @@ The Viewport (@Terminal.Gui.View.Viewport) is a rectangle describing the portion
 
 To enable scrolling call `View.SetContentSize()` and then set `Viewport.Location` to positive values. Making `Viewport.Location` positive moves the Viewport down and to the right in the content. 
 
-The `View.ViewportSettings` property controls how the Viewport is constrained. By default, the `ViewportSettings` is set to `ViewportSettings.None`. To enable the viewport to be moved up-and-to-the-left of the content, use `ViewportSettings.AllowNegativeX` and or `ViewportSettings.AllowNegativeY`. 
+See the [Scrolling Deep Dive](scrolling.md) for details on how to enable scrolling.
 
-The default `ViewportSettings` also constrains the Viewport to the size of the content, ensuring the right-most column or bottom-most row of the content will always be visible (in v1 the equivalent concept was `ScrollBarView.AlwaysKeepContentInViewport`). To allow the Viewport to be smaller than the content, set `ViewportSettings.AllowXGreaterThanContentWidth` and/or `ViewportSettings.AllowXGreaterThanContentHeight`.
+The @Terminal.Gui.View.ViewportSettings property controls how the Viewport is constrained. By default, the `ViewportSettings` is set to `ViewportSettings.None`. To enable the viewport to be moved up-and-to-the-left of the content, use `ViewportSettings.AllowNegativeX` and or `ViewportSettings.AllowNegativeY`. 
 
+The default `ViewportSettings` also constrains the Viewport to the size of the content, ensuring the right-most column or bottom-most row of the content will always be visible (in v1 the equivalent concept was `ScrollBarView.AlwaysKeepContentInViewport`). To allow the Viewport to be smaller than the content, set `ViewportSettings.AllowXGreaterThanContentWidth` and/or `ViewportSettings.AllowXGreaterThanContentHeight`.
 
 * *@Terminal.Gui.View.GetContentSize()* - The content area is the area where the view's content is drawn. Content can be any combination of the @Terminal.Gui.View.Text property, `Subviews`, and other content drawn by the View. The @Terminal.Gui.View.GetContentSize method gets the size of the content area of the view. *Content Area* refers to the rectangle with a location of `0,0` with the size returned by @Terminal.Gui.View.GetContentSize. The [Layout Deep Dive](layout.md) has more details on the Content Area.
 

+ 3 - 0
docfx/docs/migratingfromv1.md

@@ -152,12 +152,15 @@ In v2, the `Border`, `Margin`, and `Padding` properties have been added to all v
 
 In v1, scrolling was enabled by using `ScrollView` or `ScrollBarView`. In v2, the base @Terminal.Gui.View class supports scrolling inherently. The area of a view visible to the user at a given moment was previously a rectangle called `Bounds`. `Bounds.Location` was always `Point.Empty`. In v2 the visible area is a rectangle called `Viewport` which is a protal into the Views content, which can be bigger (or smaller) than the area visible to the user. Causing a view to scroll is as simple as changing `View.Viewport.Location`. The View's content is described by @Terminal.Gui.View.GetContentSize. See [Layout](layout.md) for details.
 
[email protected] replaces `ScrollBarView` with a much cleaner implementation of a scrollbar. In addition, @Terminal.Gui.View.VerticalScrollBar and @Terminal.Gui.View.HorizontalScrollBar provide a simple way to enable scroll bars in any View with almost no code. See See [Scrolling Deep Dive](scrolling.md) for more.
+
 ### How to Fix
 
 * Replace `ScrollView` with @Terminal.Gui.View and use `Viewport` and @Terminal.Gui.View.GetContentSize to control scrolling.
 * Update any code that assumed `Bounds.Location` was always `Point.Empty`.
 * Update any code that used `Bounds` to refer to the size of the view's content. Use @Terminal.Gui.View.GetContentSize instead.
 * Update any code that assumed `Bounds.Size` was the same as `Frame.Size`. `Frame.Size` defines the size of the view in the superview's coordinate system, while `Viewport.Size` defines the visible area of the view in its own coordinate system.
+* Replace `ScrollBarView` with @Terminal.Gui.ScrollBar. See [Scrolling Deep Dive](scrolling.md) for more.
 
 ## Updated Keyboard API
 

+ 2 - 1
docfx/docs/newinv2.md

@@ -21,7 +21,8 @@ The entire library has been reviewed and simplified. As a result, the API is mor
 ## [View](~/api/Terminal.Gui.View.yml) Improvements
 * *Improved!* View Lifetime Management is Now Deterministic - In v1 the rules for lifetime management of `View` objects was unclear and led to non-dterministic behavior and hard to diagnose bugs. This was particularly acute in the behavior of `Application.Run`. In v2, the rules are clear and the code and unit test infrastructure tries to enforce them. See [Migrating From v1 To v2](migratingfromv1.md) for more details.
 * *New!* Adornments - Adornments are a special form of View that appear outside the `Viewport`: @Terminal.Gui.View.Margin, @Terminal.Gui.View.Border, and @Terminal.Gui.View.Padding.
-* *New!* Built-in Scrolling/Virtual Content Area - In v1, to have a view a user could scroll required either a bespoke scrolling implementation, inheriting from `ScrollView`, or managing the complexity of `ScrollBarView` directly. In v2, the base-View class supports scrolling inherently. The area of a view visible to the user at a given moment was previously a rectangle called `Bounds`. `Bounds.Location` was always `Point.Empty`. In v2 the visible area is a rectangle called `Viewport` which is a protal into the Views content, which can be bigger (or smaller) than the area visible to the user. Causing a view to scroll is as simple as changing `View.Viewport.Location`. The View's content described by `View.GetContentSize()`. See [Layout](layout.md) for details.
+* *New!* Built-in Scrolling/Virtual Content Area - In v1, to have a view a user could scroll required either a bespoke scrolling implementation, inheriting from `ScrollView`, or managing the complexity of `ScrollBarView` directly. In v2, the base-View class supports scrolling inherently. The area of a view visible to the user at a given moment was previously a rectangle called `Bounds`. `Bounds.Location` was always `Point.Empty`. In v2 the visible area is a rectangle called `Viewport` which is a portal into the Views content, which can be bigger (or smaller) than the area visible to the user. Causing a view to scroll is as simple as changing `View.Viewport.Location`. The View's content described by `View.GetContentSize()`. See [Layout](layout.md) for details.
+* *Improved!* @Terminal.Gui.ScrollBar replaces `ScrollBarView` with a much cleaner implementation of a scrollbar. In addition, @Terminal.Gui.View.VerticalScrollBar and @Terminal.Gui.View.HorizontalScrollBar provide a simple way to enable scroll bars in any View with almost no code. See See [Scrolling Deep Dive](scrolling.md) for more.
 * *New!* @Terminal.Gui.DimAuto - Automatically sizes the view to fit the view's Text, SubViews, or ContentArea.
 * *Improved!* @Terminal.Gui.PosAnchorEnd - New to v2 is `Pos.AnchorEnd ()` (with no parameters) which allows a view to be anchored to the right or bottom of the SuperView. 
 * *New!* @Terminal.Gui.PosAlign - Aligns a set of views horizontally or vertically (left, right, center, etc...).

+ 52 - 0
docfx/docs/scrolling.md

@@ -0,0 +1,52 @@
+# Scrolling
+
+Terminal.Gui provides a rich system for how [View](View.md) users can scroll content with the keyboard and/or mouse.
+
+## Lexicon & Taxonomy
+
+See [View Deep Dive](View.md) for broader definitions.
+
+* *Scroll* (Verb) - The act of causing content to move either horizontally or vertically within the [View.Viewport](~/api/Terminal.Gui.View.Viewport.yml). Also referred to as "Content Scrolling".
+* *ScrollSlider* - A visual indicator that shows the proportion of the scrollable content to the size of the [View.Viewport](~/api/Terminal.Gui.View.Viewport.yml) and allows the user to use the mouse to scroll. 
+* *[ScrollBar](~/api/Terminal.Gui.ScrollBar.yml)* -  Indicates the size of scrollable content and controls the position of the visible content, either vertically or horizontally. At each end, a @Terminal.Gui.Button is provided, one to scroll up or left and one to scroll down or right. Between the
+ buttons is a @Terminal.Gui.ScrollSlider that can be dragged to control the position of the visible content. The ScrollSlier is sized to show the proportion of the scrollable content to the size of the @Terminal.Gui.View.Viewport.
+
+## Overview
+
+The ability to scroll content is built into View. The [View.Viewport](~/api/Terminal.Gui.View.Viewport.yml) represents the scrollable "viewport" into the View's Content Area (which is defined by the return value of [View.GetContentSize()](~/api/Terminal.Gui.View.GetContentSize.yml)). 
+
+By default, [View](~/api/Terminal.Gui.View.yml), includes no bindings for the typical directional keyboard and mouse input and cause the Content Area.
+
+Terminal.Gui also provides the ability show a visual scroll bar that responds to mouse input. This ability is not enabled by default given how precious TUI screen real estate is.
+
+Scrolling with the mouse and keyboard are enabled by:
+
+1) Making the [View.Viewport](~/api/Terminal.Gui.View.Viewport.yml) size smaller than the size returned by [View.GetContentSize()](~/api/Terminal.Gui.View.GetContentSize.yml). 
+2) Creating key bindings for the appropriate directional keys (e.g. [Key.CursorDown](~/api/Terminal.Gui.Key)), and calling [View.ScrollHorizontal()](~/api/Terminal.Gui.View.ScrollHorizontal.yml)/[ScrollVertical()](~/api/Terminal.Gui.View.ScrollVertical.yml) as needed.
+3) Subscribing to [View.MouseEvent](~/api/Terminal.Gui.View.MouseEvent.yml) and calling calling [View.ScrollHorizontal()](~/api/Terminal.Gui.View.ScrollHorizontal.yml)/[ScrollVertical()](~/api/Terminal.Gui.View.ScrollVertical.yml) as needed.
+4) Enabling the [ScrollBar](~/api/Terminal.Gui.ScrollBar.yml)s built into View ([View.HorizontalScrollBar/VerticalScrollBar](~/api/Terminal.Gui.View.HorizontalScrollBar.yml)) by either enabling automatic show/hide behavior (@Terminal.Gui.ScrollBar.AutoShow) or explicitly making them visible (@Terminal.Gui.View.Visible).
+
+While *[ScrollBar](~/api/Terminal.Gui.ScrollBar.yml)* can be used in a standalone manner to provide proportional scrolling, it is typically enabled automatically via the [View.HorizontalScrollBar](~/api/Terminal.Gui.View.HorizontalScrollBar.yml) and  [View.VerticalScrollBar](~/api/Terminal.Gui.View.VerticalScrollBar.yml) properties.
+
+## Examples
+
+These Scenarios illustrate Terminal.Gui scrolling:
+
+* *Scrolling* - Demonstrates the @Terminal.Gui.ScrollBar objects built into-View.
+* *ScrollBar Demo* - Demonstrates using @Terminal.Gui.ScrollBar view in a standalone manner.
+* *ViewportSettings* - Demonstrates the various [Viewport Settings](~/api/Terminal.Gui.ViewportSettings.yml) (see below) in an interactive manner. Used by the development team to visually verify that convoluted View layout and arrangement scenarios scroll properly.
+* *Character Map* - Demonstrates a sophisticated scrolling use-case. The entire set of Unicode code-points can be scrolled and searched. From a scrolling perspective, this Scenario illustrates how to manually configure `Viewport`, `SetContentArea()`, and `ViewportSettings` to enable horizontal and vertical headers (as might appear in a spreadsheet), full keyboard and mouse support, and more. 
+* *ListView* and *HexEdit* - The source code to these built-in Views are good references for how to support scrolling and ScrollBars in a re-usable View sub-class. 
+
+## [Viewport Settings](~/api/Terminal.Gui.ViewportSettings.yml)
+
+Use  @Terminal.Gui.ViewporSettings to adjust the behavior of scrolling. 
+
+* `AllowNegativeX/Y` - If set, Viewport.Size can be set to negative coordinates enabling scrolling beyond the top-left of the content area.
+
+* `AllowX/YGreaterThanContentWidth` - If set, Viewport.Size can be set values greater than GetContentSize() enabling scrolling beyond the bottom-right of the Content Area. When not set, `Viewport.X/Y` are constrained to the dimension of the content area - 1. This means the last column of the content will remain visible even if there is an attempt to scroll the Viewport past the last column. The practical effect of this is that the last column/row of the content will always be visible.
+
+* `ClipContentOnly` - By default, clipping is applied to [Viewport](~/api/Terminal.Gui.View.Viewport.yml). Setting this flag will cause clipping to be applied to the visible content area.
+
+* `ClearContentOnly`- If set [View.Clear()](~/api/Terminal.Gui.View.Clear.yml) will clear only the portion of the content area that is visible within the Viewport. This is useful for views that have a content area larger than the Viewport and want the area outside the content to be visually distinct.
+

+ 2 - 0
docfx/docs/toc.yml

@@ -16,6 +16,8 @@
   href: arrangement.md
 - name: Navigation
   href: navigation.md
+- name: Scrolling
+  href: scrolling.md
 - name: Keyboard
   href: keyboard.md
 - name: Mouse

二进制
docfx/images/sample.gif