Browse Source

Resolving merge conflicts.

BDisp 1 year ago
parent
commit
d4e6ae6335
100 changed files with 4251 additions and 4131 deletions
  1. 0 51
      Terminal.Gui/Application.MainLoopSyncContext.cs
  2. 12 523
      Terminal.Gui/Application/Application.cs
  3. 298 0
      Terminal.Gui/Application/ApplicationKeyboard.cs
  4. 302 0
      Terminal.Gui/Application/ApplicationMouse.cs
  5. 0 0
      Terminal.Gui/Application/IterationEventArgs.cs
  6. 0 0
      Terminal.Gui/Application/MainLoop.cs
  7. 48 0
      Terminal.Gui/Application/MainLoopSyncContext.cs
  8. 0 0
      Terminal.Gui/Application/RunState.cs
  9. 0 0
      Terminal.Gui/Application/RunStateEventArgs.cs
  10. 0 0
      Terminal.Gui/Application/Timeout.cs
  11. 0 0
      Terminal.Gui/Application/TimeoutEventArgs.cs
  12. 22 3
      Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
  13. 1 1
      Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs
  14. 48 41
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  15. 369 0
      Terminal.Gui/Drawing/Aligner.cs
  16. 82 0
      Terminal.Gui/Drawing/Alignment.cs
  17. 52 0
      Terminal.Gui/Drawing/AlignmentModes.cs
  18. 0 333
      Terminal.Gui/Drawing/Justification.cs
  19. 2 2
      Terminal.Gui/Drawing/Thickness.cs
  20. 41 0
      Terminal.Gui/Input/CommandContext.cs
  21. 14 240
      Terminal.Gui/Input/KeyBinding.cs
  22. 46 0
      Terminal.Gui/Input/KeyBindingScope.cs
  23. 258 0
      Terminal.Gui/Input/KeyBindings.cs
  24. 2 1
      Terminal.Gui/Resources/config.json
  25. 1 0
      Terminal.Gui/Terminal.Gui.csproj
  26. 0 20
      Terminal.Gui/Text/TextAlignment.cs
  27. 52 49
      Terminal.Gui/Text/TextFormatter.cs
  28. 0 20
      Terminal.Gui/Text/VerticalTextAlignment.cs
  29. 30 9
      Terminal.Gui/View/Adornment/Border.cs
  30. 3 4
      Terminal.Gui/View/Layout/Dim.cs
  31. 82 21
      Terminal.Gui/View/Layout/DimAuto.cs
  32. 7 9
      Terminal.Gui/View/Layout/DimAutoStyle.cs
  33. 2 2
      Terminal.Gui/View/Layout/DimPercent.cs
  34. 1 1
      Terminal.Gui/View/Layout/DimPercentMode.cs
  35. 0 38
      Terminal.Gui/View/Layout/LayoutStyle.cs
  36. 35 4
      Terminal.Gui/View/Layout/Pos.cs
  37. 222 0
      Terminal.Gui/View/Layout/PosAlign.cs
  38. 20 83
      Terminal.Gui/View/Layout/ViewLayout.cs
  39. 9 30
      Terminal.Gui/View/View.cs
  40. 4 1
      Terminal.Gui/View/ViewAdornments.cs
  41. 111 52
      Terminal.Gui/View/ViewContent.cs
  42. 19 9
      Terminal.Gui/View/ViewDrawing.cs
  43. 75 30
      Terminal.Gui/View/ViewKeyboard.cs
  44. 4 0
      Terminal.Gui/View/ViewMouse.cs
  45. 22 13
      Terminal.Gui/View/ViewText.cs
  46. 6 6
      Terminal.Gui/View/ViewportSettings.cs
  47. 3 4
      Terminal.Gui/Views/Button.cs
  48. 5 6
      Terminal.Gui/Views/CheckBox.cs
  49. 3 3
      Terminal.Gui/Views/ColorPicker.cs
  50. 37 34
      Terminal.Gui/Views/ComboBox.cs
  51. 2 2
      Terminal.Gui/Views/DateField.cs
  52. 2 4
      Terminal.Gui/Views/DatePicker.cs
  53. 51 176
      Terminal.Gui/Views/Dialog.cs
  54. 2 3
      Terminal.Gui/Views/FrameView.cs
  55. 2 2
      Terminal.Gui/Views/HexView.cs
  56. 117 32
      Terminal.Gui/Views/ListView.cs
  57. 44 474
      Terminal.Gui/Views/Menu/Menu.cs
  58. 127 450
      Terminal.Gui/Views/Menu/MenuBar.cs
  59. 178 0
      Terminal.Gui/Views/Menu/MenuBarItem.cs
  60. 31 0
      Terminal.Gui/Views/Menu/MenuClosingEventArgs.cs
  61. 0 73
      Terminal.Gui/Views/Menu/MenuEventArgs.cs
  62. 273 0
      Terminal.Gui/Views/Menu/MenuItem.cs
  63. 15 0
      Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs
  64. 20 0
      Terminal.Gui/Views/Menu/MenuOpenedEventArgs.cs
  65. 24 0
      Terminal.Gui/Views/Menu/MenuOpeningEventArgs.cs
  66. 112 94
      Terminal.Gui/Views/MessageBox.cs
  67. 19 0
      Terminal.Gui/Views/OrientationEventArgs.cs
  68. 1 1
      Terminal.Gui/Views/ProgressBar.cs
  69. 190 199
      Terminal.Gui/Views/RadioGroup.cs
  70. 2 4
      Terminal.Gui/Views/ScrollBarView.cs
  71. 20 21
      Terminal.Gui/Views/ScrollView.cs
  72. 8 219
      Terminal.Gui/Views/Slider.cs
  73. 14 0
      Terminal.Gui/Views/SliderAttributes.cs
  74. 19 0
      Terminal.Gui/Views/SliderConfiguration.cs
  75. 24 0
      Terminal.Gui/Views/SliderEventArgs.cs
  76. 50 0
      Terminal.Gui/Views/SliderOption.cs
  77. 12 0
      Terminal.Gui/Views/SliderOptionEventArgs.cs
  78. 35 0
      Terminal.Gui/Views/SliderStyle.cs
  79. 40 0
      Terminal.Gui/Views/SliderType.cs
  80. 12 95
      Terminal.Gui/Views/StatusBar.cs
  81. 59 0
      Terminal.Gui/Views/StatusItem.cs
  82. 2 2
      Terminal.Gui/Views/TabView.cs
  83. 13 13
      Terminal.Gui/Views/TableView/ColumnStyle.cs
  84. 2 2
      Terminal.Gui/Views/TableView/TableStyle.cs
  85. 10 10
      Terminal.Gui/Views/TableView/TableView.cs
  86. 1 2
      Terminal.Gui/Views/TextField.cs
  87. 8 9
      Terminal.Gui/Views/TextValidateField.cs
  88. 64 99
      Terminal.Gui/Views/TextView.cs
  89. 1 1
      Terminal.Gui/Views/TimeField.cs
  90. 10 9
      Terminal.Gui/Views/Toplevel.cs
  91. 1 2
      Terminal.Gui/Views/Window.cs
  92. 4 22
      Terminal.Gui/Views/Wizard/Wizard.cs
  93. 1 2
      Terminal.Gui/Views/Wizard/WizardStep.cs
  94. 2 0
      Terminal.sln.DotSettings
  95. 4 3
      UICatalog/KeyBindingsDialog.cs
  96. 1 0
      UICatalog/Resources/config.json
  97. 21 18
      UICatalog/Scenario.cs
  98. 3 2
      UICatalog/Scenarios/ASCIICustomButton.cs
  99. 236 0
      UICatalog/Scenarios/AdornmentEditor.cs
  100. 12 443
      UICatalog/Scenarios/Adornments.cs

+ 0 - 51
Terminal.Gui/Application.MainLoopSyncContext.cs

@@ -1,51 +0,0 @@
-namespace Terminal.Gui;
-
-public static partial class Application
-{
-    /// <summary>
-    ///     provides the sync context set while executing code in Terminal.Gui, to let
-    ///     users use async/await on their code
-    /// </summary>
-    private sealed class MainLoopSyncContext : SynchronizationContext
-    {
-        public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }
-
-        public override void Post (SendOrPostCallback d, object state)
-        {
-            MainLoop?.AddIdle (
-                              () =>
-                              {
-                                  d (state);
-
-                                  return false;
-                              }
-                             );
-        }
-
-        //_mainLoop.Driver.Wakeup ();
-        public override void Send (SendOrPostCallback d, object state)
-        {
-            if (Thread.CurrentThread.ManagedThreadId == _mainThreadId)
-            {
-                d (state);
-            }
-            else
-            {
-                var wasExecuted = false;
-
-                Invoke (
-                        () =>
-                        {
-                            d (state);
-                            wasExecuted = true;
-                        }
-                       );
-
-                while (!wasExecuted)
-                {
-                    Thread.Sleep (15);
-                }
-            }
-        }
-    }
-}

+ 12 - 523
Terminal.Gui/Application.cs → Terminal.Gui/Application/Application.cs

@@ -99,7 +99,6 @@ public static partial class Application
         // Don't dispose the Top. It's up to caller dispose it
         if (Top is { })
         {
-
             Debug.Assert (Top.WasDisposed);
 
             // If End wasn't called _cachedRunStateToplevel may be null
@@ -158,6 +157,7 @@ public static partial class Application
         KeyDown = null;
         KeyUp = null;
         SizeChanging = null;
+        ClearKeyBindings ();
 
         Colors.Reset ();
 
@@ -526,12 +526,8 @@ public static partial class Application
             MoveCurrent (Current);
         }
 
-        //if (Toplevel.LayoutStyle == LayoutStyle.Computed) {
         toplevel.SetRelativeLayout (Driver.Screen.Size);
 
-        //}
-
-        // BUGBUG: This call is likely not needed.
         toplevel.LayoutSubviews ();
         toplevel.PositionToplevels ();
         toplevel.FocusFirst ();
@@ -543,6 +539,7 @@ public static partial class Application
             toplevel.SetNeedsDisplay ();
             toplevel.Draw ();
             Driver.UpdateScreen ();
+
             if (PositionCursor (toplevel))
             {
                 Driver.UpdateCursor ();
@@ -555,13 +552,14 @@ public static partial class Application
     }
 
     /// <summary>
-    /// Calls <see cref="View.PositionCursor"/> on the most focused view in the view starting with <paramref name="view"/>.
+    ///     Calls <see cref="View.PositionCursor"/> on the most focused view in the view starting with <paramref name="view"/>.
     /// </summary>
     /// <remarks>
-    /// Does nothing if <paramref name="view"/> is <see langword="null"/> or if the most focused view is not visible or enabled.
-    /// <para>
-    /// If the most focused view is not visible within it's superview, the cursor will be hidden.
-    /// </para>
+    ///     Does nothing if <paramref name="view"/> is <see langword="null"/> or if the most focused view is not visible or
+    ///     enabled.
+    ///     <para>
+    ///         If the most focused view is not visible within it's superview, the cursor will be hidden.
+    ///     </para>
     /// </remarks>
     /// <returns><see langword="true"/> if a view positioned the cursor and the position is visible.</returns>
     internal static bool PositionCursor (View view)
@@ -585,6 +583,7 @@ public static partial class Application
         if (!mostFocused.Visible || !mostFocused.Enabled)
         {
             Driver.GetCursorVisibility (out CursorVisibility current);
+
             if (current != CursorVisibility.Invisible)
             {
                 Driver.SetCursorVisibility (CursorVisibility.Invisible);
@@ -596,6 +595,7 @@ public static partial class Application
         // If the view is not visible within it's superview, don't position the cursor
         Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty });
         Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver.Screen;
+
         if (!superViewViewport.IntersectsWith (mostFocusedViewport))
         {
             return false;
@@ -677,7 +677,7 @@ public static partial class Application
     /// </param>
     /// <returns>The created T object. The caller is responsible for disposing this object.</returns>
     public static T Run<T> (Func<Exception, bool> errorHandler = null, ConsoleDriver driver = null)
-        where T : Toplevel, new()
+        where T : Toplevel, new ()
     {
         var top = new T ();
 
@@ -964,6 +964,7 @@ public static partial class Application
         {
             state.Toplevel.Draw ();
             Driver.UpdateScreen ();
+
             //Driver.UpdateCursor ();
         }
 
@@ -1424,516 +1425,4 @@ public static partial class Application
     }
 
     #endregion Toplevel handling
-
-    #region Mouse handling
-
-    /// <summary>Disable or enable the mouse. The mouse is enabled by default.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    public static bool IsMouseDisabled { get; set; }
-
-    /// <summary>The current <see cref="View"/> object that wants continuous mouse button pressed events.</summary>
-    public static View WantContinuousButtonPressedView { get; private set; }
-
-    /// <summary>
-    ///     Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to
-    ///     this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
-    /// </summary>
-    public static View MouseGrabView { get; private set; }
-
-    /// <summary>Invoked when a view wants to grab the mouse; can be canceled.</summary>
-    public static event EventHandler<GrabMouseEventArgs> GrabbingMouse;
-
-    /// <summary>Invoked when a view wants un-grab the mouse; can be canceled.</summary>
-    public static event EventHandler<GrabMouseEventArgs> UnGrabbingMouse;
-
-    /// <summary>Invoked after a view has grabbed the mouse.</summary>
-    public static event EventHandler<ViewEventArgs> GrabbedMouse;
-
-    /// <summary>Invoked after a view has un-grabbed the mouse.</summary>
-    public static event EventHandler<ViewEventArgs> UnGrabbedMouse;
-
-    /// <summary>
-    ///     Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/>
-    ///     is called.
-    /// </summary>
-    /// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
-    public static void GrabMouse (View view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        if (!OnGrabbingMouse (view))
-        {
-            OnGrabbedMouse (view);
-            MouseGrabView = view;
-        }
-    }
-
-    /// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
-    public static void UngrabMouse ()
-    {
-        if (MouseGrabView is null)
-        {
-            return;
-        }
-
-        if (!OnUnGrabbingMouse (MouseGrabView))
-        {
-            View view = MouseGrabView;
-            MouseGrabView = null;
-            OnUnGrabbedMouse (view);
-        }
-    }
-
-    private static bool OnGrabbingMouse (View view)
-    {
-        if (view is null)
-        {
-            return false;
-        }
-
-        var evArgs = new GrabMouseEventArgs (view);
-        GrabbingMouse?.Invoke (view, evArgs);
-
-        return evArgs.Cancel;
-    }
-
-    private static bool OnUnGrabbingMouse (View view)
-    {
-        if (view is null)
-        {
-            return false;
-        }
-
-        var evArgs = new GrabMouseEventArgs (view);
-        UnGrabbingMouse?.Invoke (view, evArgs);
-
-        return evArgs.Cancel;
-    }
-
-    private static void OnGrabbedMouse (View view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        GrabbedMouse?.Invoke (view, new (view));
-    }
-
-    private static void OnUnGrabbedMouse (View view)
-    {
-        if (view is null)
-        {
-            return;
-        }
-
-        UnGrabbedMouse?.Invoke (view, new (view));
-    }
-
-#nullable enable
-
-    // Used by OnMouseEvent to track the last view that was clicked on.
-    internal static View? _mouseEnteredView;
-
-    /// <summary>Event fired when a mouse move or click occurs. Coordinates are screen relative.</summary>
-    /// <remarks>
-    ///     <para>
-    ///         Use this event to receive mouse events in screen coordinates. Use <see cref="MouseEvent"/> to
-    ///         receive mouse events relative to a <see cref="View.Viewport"/>.
-    ///     </para>
-    ///     <para>The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.</para>
-    /// </remarks>
-    public static event EventHandler<MouseEvent>? MouseEvent;
-
-    /// <summary>Called when a mouse event occurs. Raises the <see cref="MouseEvent"/> event.</summary>
-    /// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
-    /// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
-    internal static void OnMouseEvent (MouseEvent mouseEvent)
-    {
-        if (IsMouseDisabled)
-        {
-            return;
-        }
-
-        var view = View.FindDeepestView (Current, mouseEvent.Position);
-
-        if (view is { })
-        {
-            mouseEvent.View = view;
-        }
-
-        MouseEvent?.Invoke (null, mouseEvent);
-
-        if (mouseEvent.Handled)
-        {
-            return;
-        }
-
-        if (MouseGrabView is { })
-        {
-            // If the mouse is grabbed, send the event to the view that grabbed it.
-            // The coordinates are relative to the Bounds of the view that grabbed the mouse.
-            Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.Position);
-
-            var viewRelativeMouseEvent = new MouseEvent
-            {
-                Position = frameLoc,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = MouseGrabView
-            };
-
-            if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false)
-            {
-                // The mouse has moved outside the bounds of the view that grabbed the mouse
-                _mouseEnteredView?.NewMouseLeaveEvent (mouseEvent);
-            }
-
-            //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
-            if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true)
-            {
-                return;
-            }
-        }
-
-        if (view is { WantContinuousButtonPressed: true })
-        {
-            WantContinuousButtonPressedView = view;
-        }
-        else
-        {
-            WantContinuousButtonPressedView = null;
-        }
-
-
-        if (view is not Adornment)
-        {
-            if ((view is null || view == OverlappedTop)
-                && Current is { Modal: false }
-                && OverlappedTop != null
-                && mouseEvent.Flags != MouseFlags.ReportMousePosition
-                && mouseEvent.Flags != 0)
-            {
-                // This occurs when there are multiple overlapped "tops"
-                // E.g. "Mdi" - in the Background Worker Scenario
-                View? top = FindDeepestTop (Top, mouseEvent.Position);
-                view = View.FindDeepestView (top, mouseEvent.Position);
-
-                if (view is { } && view != OverlappedTop && top != Current && top is { })
-                {
-                    MoveCurrent ((Toplevel)top);
-                }
-            }
-        }
-
-        if (view is null)
-        {
-            return;
-        }
-
-        MouseEvent? me = null;
-
-        if (view is Adornment adornment)
-        {
-            Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position);
-
-            me = new ()
-            {
-                Position = frameLoc,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = view
-            };
-        }
-        else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position))
-        {
-            Point viewportLocation = view.ScreenToViewport (mouseEvent.Position);
-
-            me = new ()
-            {
-                Position = viewportLocation,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = view
-            };
-        }
-
-        if (me is null)
-        {
-            return;
-        }
-
-        if (_mouseEnteredView is null)
-        {
-            _mouseEnteredView = view;
-            view.NewMouseEnterEvent (me);
-        }
-        else if (_mouseEnteredView != view)
-        {
-            _mouseEnteredView.NewMouseLeaveEvent (me);
-            view.NewMouseEnterEvent (me);
-            _mouseEnteredView = view;
-        }
-
-        if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition)
-        {
-            return;
-        }
-
-        WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
-
-        //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");
-
-        while (view.NewMouseEvent (me) != true)
-        {
-            if (MouseGrabView is { })
-            {
-                break;
-            }
-
-            if (view is Adornment adornmentView)
-            {
-                view = adornmentView.Parent.SuperView;
-            }
-            else
-            {
-                view = view.SuperView;
-            }
-
-            if (view is null)
-            {
-                break;
-            }
-
-            Point boundsPoint = view.ScreenToViewport (mouseEvent.Position);
-
-            me = new ()
-            {
-                Position = boundsPoint,
-                Flags = mouseEvent.Flags,
-                ScreenPosition = mouseEvent.Position,
-                View = view
-            };
-        }
-
-        BringOverlappedTopToFront ();
-    }
-#nullable restore
-
-    #endregion Mouse handling
-
-    #region Keyboard handling
-
-    private static Key _alternateForwardKey = Key.Empty; // Defined in config.json
-
-    /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    [JsonConverter (typeof (KeyJsonConverter))]
-    public static Key AlternateForwardKey
-    {
-        get => _alternateForwardKey;
-        set
-        {
-            if (_alternateForwardKey != value)
-            {
-                Key oldKey = _alternateForwardKey;
-                _alternateForwardKey = value;
-                OnAlternateForwardKeyChanged (new (oldKey, value));
-            }
-        }
-    }
-
-    private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
-    {
-        foreach (Toplevel top in _topLevels.ToArray ())
-        {
-            top.OnAlternateForwardKeyChanged (e);
-        }
-    }
-
-    private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json
-
-    /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    [JsonConverter (typeof (KeyJsonConverter))]
-    public static Key AlternateBackwardKey
-    {
-        get => _alternateBackwardKey;
-        set
-        {
-            if (_alternateBackwardKey != value)
-            {
-                Key oldKey = _alternateBackwardKey;
-                _alternateBackwardKey = value;
-                OnAlternateBackwardKeyChanged (new (oldKey, value));
-            }
-        }
-    }
-
-    private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
-    {
-        foreach (Toplevel top in _topLevels.ToArray ())
-        {
-            top.OnAlternateBackwardKeyChanged (oldKey);
-        }
-    }
-
-    private static Key _quitKey = Key.Empty; // Defined in config.json
-
-    /// <summary>Gets or sets the key to quit the application.</summary>
-    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-    [JsonConverter (typeof (KeyJsonConverter))]
-    public static Key QuitKey
-    {
-        get => _quitKey;
-        set
-        {
-            if (_quitKey != value)
-            {
-                Key oldKey = _quitKey;
-                _quitKey = value;
-                OnQuitKeyChanged (new (oldKey, value));
-            }
-        }
-    }
-
-    private static void OnQuitKeyChanged (KeyChangedEventArgs e)
-    {
-        // Duplicate the list so if it changes during enumeration we're safe
-        foreach (Toplevel top in _topLevels.ToArray ())
-        {
-            top.OnQuitKeyChanged (e);
-        }
-    }
-
-    /// <summary>
-    ///     Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>.
-    ///     <para>
-    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
-    ///         additional processing.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
-    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
-    ///     <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
-    /// </remarks>
-    public static event EventHandler<Key> KeyDown;
-
-    /// <summary>
-    ///     Called by the <see cref="ConsoleDriver"/> when the user presses a key. Fires the <see cref="KeyDown"/> event
-    ///     then calls <see cref="View.NewKeyDownEvent"/> on all top level views. Called after <see cref="OnKeyDown"/> and
-    ///     before <see cref="OnKeyUp"/>.
-    /// </summary>
-    /// <remarks>Can be used to simulate key press events.</remarks>
-    /// <param name="keyEvent"></param>
-    /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool OnKeyDown (Key keyEvent)
-    {
-        if (!_initialized)
-        {
-            return true;
-        }
-
-        KeyDown?.Invoke (null, keyEvent);
-
-        if (keyEvent.Handled)
-        {
-            return true;
-        }
-
-        foreach (Toplevel topLevel in _topLevels.ToList ())
-        {
-            if (topLevel.NewKeyDownEvent (keyEvent))
-            {
-                return true;
-            }
-
-            if (topLevel.Modal)
-            {
-                break;
-            }
-        }
-
-        // Invoke any Global KeyBindings
-        foreach (Toplevel topLevel in _topLevels.ToList ())
-        {
-            foreach (View view in topLevel.Subviews.Where (
-                                                           v => v.KeyBindings.TryGet (
-                                                                                      keyEvent,
-                                                                                      KeyBindingScope.Application,
-                                                                                      out KeyBinding _
-                                                                                     )
-                                                          ))
-            {
-                if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out KeyBinding _))
-                {
-                    bool? handled = view.OnInvokingKeyBindings (keyEvent);
-
-                    if (handled is { } && (bool)handled)
-                    {
-                        return true;
-                    }
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /// <summary>
-    ///     Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
-    ///     <para>
-    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
-    ///         additional processing.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
-    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
-    ///     <para>Fired after <see cref="KeyDown"/>.</para>
-    /// </remarks>
-    public static event EventHandler<Key> KeyUp;
-
-    /// <summary>
-    ///     Called by the <see cref="ConsoleDriver"/> when the user releases a key. Fires the <see cref="KeyUp"/> event
-    ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="OnKeyDown"/>.
-    /// </summary>
-    /// <remarks>Can be used to simulate key press events.</remarks>
-    /// <param name="a"></param>
-    /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool OnKeyUp (Key a)
-    {
-        if (!_initialized)
-        {
-            return true;
-        }
-
-        KeyUp?.Invoke (null, a);
-
-        if (a.Handled)
-        {
-            return true;
-        }
-
-        foreach (Toplevel topLevel in _topLevels.ToList ())
-        {
-            if (topLevel.NewKeyUpEvent (a))
-            {
-                return true;
-            }
-
-            if (topLevel.Modal)
-            {
-                break;
-            }
-        }
-
-        return false;
-    }
-
-    #endregion Keyboard handling
 }

+ 298 - 0
Terminal.Gui/Application/ApplicationKeyboard.cs

@@ -0,0 +1,298 @@
+using System.Text.Json.Serialization;
+
+namespace Terminal.Gui;
+
+partial class Application
+{
+    private static Key _alternateForwardKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key AlternateForwardKey
+    {
+        get => _alternateForwardKey;
+        set
+        {
+            if (_alternateForwardKey != value)
+            {
+                Key oldKey = _alternateForwardKey;
+                _alternateForwardKey = value;
+                OnAlternateForwardKeyChanged (new (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e)
+    {
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnAlternateForwardKeyChanged (e);
+        }
+    }
+
+    private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key AlternateBackwardKey
+    {
+        get => _alternateBackwardKey;
+        set
+        {
+            if (_alternateBackwardKey != value)
+            {
+                Key oldKey = _alternateBackwardKey;
+                _alternateBackwardKey = value;
+                OnAlternateBackwardKeyChanged (new (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey)
+    {
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnAlternateBackwardKeyChanged (oldKey);
+        }
+    }
+
+    private static Key _quitKey = Key.Empty; // Defined in config.json
+
+    /// <summary>Gets or sets the key to quit the application.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    [JsonConverter (typeof (KeyJsonConverter))]
+    public static Key QuitKey
+    {
+        get => _quitKey;
+        set
+        {
+            if (_quitKey != value)
+            {
+                Key oldKey = _quitKey;
+                _quitKey = value;
+                OnQuitKeyChanged (new (oldKey, value));
+            }
+        }
+    }
+
+    private static void OnQuitKeyChanged (KeyChangedEventArgs e)
+    {
+        // Duplicate the list so if it changes during enumeration we're safe
+        foreach (Toplevel top in _topLevels.ToArray ())
+        {
+            top.OnQuitKeyChanged (e);
+        }
+    }
+
+    /// <summary>
+    ///     Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
+    /// </remarks>
+    public static event EventHandler<Key> KeyDown;
+
+    /// <summary>
+    ///     Called by the <see cref="ConsoleDriver"/> when the user presses a key. Fires the <see cref="KeyDown"/> event
+    ///     then calls <see cref="View.NewKeyDownEvent"/> on all top level views. Called after <see cref="OnKeyDown"/> and
+    ///     before <see cref="OnKeyUp"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key press events.</remarks>
+    /// <param name="keyEvent"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    public static bool OnKeyDown (Key keyEvent)
+    {
+        if (!_initialized)
+        {
+            return true;
+        }
+
+        KeyDown?.Invoke (null, keyEvent);
+
+        if (keyEvent.Handled)
+        {
+            return true;
+        }
+
+        foreach (Toplevel topLevel in _topLevels.ToList ())
+        {
+            if (topLevel.NewKeyDownEvent (keyEvent))
+            {
+                return true;
+            }
+
+            if (topLevel.Modal)
+            {
+                break;
+            }
+        }
+
+        // Invoke any global (Application-scoped) KeyBindings.
+        // The first view that handles the key will stop the loop.
+        foreach (KeyValuePair<Key, List<View>> binding in _keyBindings.Where (b => b.Key == keyEvent.KeyCode))
+        {
+            foreach (View view in binding.Value)
+            {
+                bool? handled = view?.OnInvokingKeyBindings (keyEvent);
+
+                if (handled != null && (bool)handled)
+                {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/>.</para>
+    /// </remarks>
+    public static event EventHandler<Key> KeyUp;
+
+    /// <summary>
+    ///     Called by the <see cref="ConsoleDriver"/> when the user releases a key. Fires the <see cref="KeyUp"/> event
+    ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="OnKeyDown"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key press events.</remarks>
+    /// <param name="a"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    public static bool OnKeyUp (Key a)
+    {
+        if (!_initialized)
+        {
+            return true;
+        }
+
+        KeyUp?.Invoke (null, a);
+
+        if (a.Handled)
+        {
+            return true;
+        }
+
+        foreach (Toplevel topLevel in _topLevels.ToList ())
+        {
+            if (topLevel.NewKeyUpEvent (a))
+            {
+                return true;
+            }
+
+            if (topLevel.Modal)
+            {
+                break;
+            }
+        }
+
+        return false;
+    }
+
+    /// <summary>
+    ///     The <see cref="KeyBindingScope.Application"/> key bindings.
+    /// </summary>
+    private static readonly Dictionary<Key, List<View>> _keyBindings = new ();
+
+    /// <summary>
+    /// Gets the list of <see cref="KeyBindingScope.Application"/> key bindings.
+    /// </summary>
+    public static Dictionary<Key, List<View>> GetKeyBindings () { return _keyBindings; }
+
+    /// <summary>
+    ///     Adds an  <see cref="KeyBindingScope.Application"/> scoped key binding.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to add Application key bindings.
+    /// </remarks>
+    /// <param name="key">The key being bound.</param>
+    /// <param name="view">The view that is bound to the key.</param>
+    internal static void AddKeyBinding (Key key, View view)
+    {
+        if (!_keyBindings.ContainsKey (key))
+        {
+            _keyBindings [key] = [];
+        }
+
+        _keyBindings [key].Add (view);
+    }
+
+    /// <summary>
+    ///     Gets the list of Views that have <see cref="KeyBindingScope.Application"/> key bindings.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to add Application key bindings.
+    /// </remarks>
+    /// <returns>The list of Views that have Application-scoped key bindings.</returns>
+    internal static List<View> GetViewsWithKeyBindings () { return _keyBindings.Values.SelectMany (v => v).ToList (); }
+
+    /// <summary>
+    ///     Gets the list of Views that have <see cref="KeyBindingScope.Application"/> key bindings for the specified key.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to add Application key bindings.
+    /// </remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="views">Outputs the list of views bound to <paramref name="key"/></param>
+    /// <returns><see langword="True"/> if successful.</returns>
+    internal static bool TryGetKeyBindings (Key key, out List<View> views) { return _keyBindings.TryGetValue (key, out views); }
+
+    /// <summary>
+    ///     Removes an <see cref="KeyBindingScope.Application"/> scoped key binding.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to remove Application key bindings.
+    /// </remarks>
+    /// <param name="key">The key that was bound.</param>
+    /// <param name="view">The view that is bound to the key.</param>
+    internal static void RemoveKeyBinding (Key key, View view)
+    {
+        if (_keyBindings.TryGetValue (key, out List<View> views))
+        {
+            views.Remove (view);
+
+            if (views.Count == 0)
+            {
+                _keyBindings.Remove (key);
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Removes all <see cref="KeyBindingScope.Application"/> scoped key bindings for the specified view.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to remove Application key bindings.
+    /// </remarks>
+    /// <param name="view">The view that is bound to the key.</param>
+    internal static void ClearKeyBindings (View view)
+    {
+        foreach (Key key in _keyBindings.Keys)
+        {
+            _keyBindings [key].Remove (view);
+        }
+    }
+
+    /// <summary>
+    ///     Removes all <see cref="KeyBindingScope.Application"/> scoped key bindings for the specified view.
+    /// </summary>
+    /// <remarks>
+    ///     This is an internal method used by the <see cref="View"/> class to remove Application key bindings.
+    /// </remarks>
+    internal static void ClearKeyBindings () { _keyBindings.Clear (); }
+}

+ 302 - 0
Terminal.Gui/Application/ApplicationMouse.cs

@@ -0,0 +1,302 @@
+namespace Terminal.Gui;
+
+partial class Application
+{
+    #region Mouse handling
+
+    /// <summary>Disable or enable the mouse. The mouse is enabled by default.</summary>
+    [SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+    public static bool IsMouseDisabled { get; set; }
+
+    /// <summary>The current <see cref="View"/> object that wants continuous mouse button pressed events.</summary>
+    public static View WantContinuousButtonPressedView { get; private set; }
+
+    /// <summary>
+    ///     Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to
+    ///     this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
+    /// </summary>
+    public static View MouseGrabView { get; private set; }
+
+    /// <summary>Invoked when a view wants to grab the mouse; can be canceled.</summary>
+    public static event EventHandler<GrabMouseEventArgs> GrabbingMouse;
+
+    /// <summary>Invoked when a view wants un-grab the mouse; can be canceled.</summary>
+    public static event EventHandler<GrabMouseEventArgs> UnGrabbingMouse;
+
+    /// <summary>Invoked after a view has grabbed the mouse.</summary>
+    public static event EventHandler<ViewEventArgs> GrabbedMouse;
+
+    /// <summary>Invoked after a view has un-grabbed the mouse.</summary>
+    public static event EventHandler<ViewEventArgs> UnGrabbedMouse;
+
+    /// <summary>
+    ///     Grabs the mouse, forcing all mouse events to be routed to the specified view until <see cref="UngrabMouse"/>
+    ///     is called.
+    /// </summary>
+    /// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
+    public static void GrabMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        if (!OnGrabbingMouse (view))
+        {
+            OnGrabbedMouse (view);
+            MouseGrabView = view;
+        }
+    }
+
+    /// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
+    public static void UngrabMouse ()
+    {
+        if (MouseGrabView is null)
+        {
+            return;
+        }
+
+        if (!OnUnGrabbingMouse (MouseGrabView))
+        {
+            View view = MouseGrabView;
+            MouseGrabView = null;
+            OnUnGrabbedMouse (view);
+        }
+    }
+
+    private static bool OnGrabbingMouse (View view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        GrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    private static bool OnUnGrabbingMouse (View view)
+    {
+        if (view is null)
+        {
+            return false;
+        }
+
+        var evArgs = new GrabMouseEventArgs (view);
+        UnGrabbingMouse?.Invoke (view, evArgs);
+
+        return evArgs.Cancel;
+    }
+
+    private static void OnGrabbedMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        GrabbedMouse?.Invoke (view, new (view));
+    }
+
+    private static void OnUnGrabbedMouse (View view)
+    {
+        if (view is null)
+        {
+            return;
+        }
+
+        UnGrabbedMouse?.Invoke (view, new (view));
+    }
+
+#nullable enable
+
+    // Used by OnMouseEvent to track the last view that was clicked on.
+    internal static View? _mouseEnteredView;
+
+    /// <summary>Event fired when a mouse move or click occurs. Coordinates are screen relative.</summary>
+    /// <remarks>
+    ///     <para>
+    ///         Use this event to receive mouse events in screen coordinates. Use <see cref="MouseEvent"/> to
+    ///         receive mouse events relative to a <see cref="View.Viewport"/>.
+    ///     </para>
+    ///     <para>The <see cref="MouseEvent.View"/> will contain the <see cref="View"/> that contains the mouse coordinates.</para>
+    /// </remarks>
+    public static event EventHandler<MouseEvent>? MouseEvent;
+
+    /// <summary>Called when a mouse event occurs. Raises the <see cref="MouseEvent"/> event.</summary>
+    /// <remarks>This method can be used to simulate a mouse event, e.g. in unit tests.</remarks>
+    /// <param name="mouseEvent">The mouse event with coordinates relative to the screen.</param>
+    internal static void OnMouseEvent (MouseEvent mouseEvent)
+    {
+        if (IsMouseDisabled)
+        {
+            return;
+        }
+
+        var view = View.FindDeepestView (Current, mouseEvent.Position);
+
+        if (view is { })
+        {
+            mouseEvent.View = view;
+        }
+
+        MouseEvent?.Invoke (null, mouseEvent);
+
+        if (mouseEvent.Handled)
+        {
+            return;
+        }
+
+        if (MouseGrabView is { })
+        {
+            // If the mouse is grabbed, send the event to the view that grabbed it.
+            // The coordinates are relative to the Bounds of the view that grabbed the mouse.
+            Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.Position);
+
+            var viewRelativeMouseEvent = new MouseEvent
+            {
+                Position = frameLoc,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = MouseGrabView
+            };
+
+            if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false)
+            {
+                // The mouse has moved outside the bounds of the view that grabbed the mouse
+                _mouseEnteredView?.NewMouseLeaveEvent (mouseEvent);
+            }
+
+            //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
+            if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true)
+            {
+                return;
+            }
+        }
+
+        if (view is { WantContinuousButtonPressed: true })
+        {
+            WantContinuousButtonPressedView = view;
+        }
+        else
+        {
+            WantContinuousButtonPressedView = null;
+        }
+
+        if (view is not Adornment)
+        {
+            if ((view is null || view == OverlappedTop)
+                && Current is { Modal: false }
+                && OverlappedTop != null
+                && mouseEvent.Flags != MouseFlags.ReportMousePosition
+                && mouseEvent.Flags != 0)
+            {
+                // This occurs when there are multiple overlapped "tops"
+                // E.g. "Mdi" - in the Background Worker Scenario
+                View? top = FindDeepestTop (Top, mouseEvent.Position);
+                view = View.FindDeepestView (top, mouseEvent.Position);
+
+                if (view is { } && view != OverlappedTop && top != Current && top is { })
+                {
+                    MoveCurrent ((Toplevel)top);
+                }
+            }
+        }
+
+        if (view is null)
+        {
+            return;
+        }
+
+        MouseEvent? me = null;
+
+        if (view is Adornment adornment)
+        {
+            Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position);
+
+            me = new ()
+            {
+                Position = frameLoc,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = view
+            };
+        }
+        else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position))
+        {
+            Point viewportLocation = view.ScreenToViewport (mouseEvent.Position);
+
+            me = new ()
+            {
+                Position = viewportLocation,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = view
+            };
+        }
+
+        if (me is null)
+        {
+            return;
+        }
+
+        if (_mouseEnteredView is null)
+        {
+            _mouseEnteredView = view;
+            view.NewMouseEnterEvent (me);
+        }
+        else if (_mouseEnteredView != view)
+        {
+            _mouseEnteredView.NewMouseLeaveEvent (me);
+            view.NewMouseEnterEvent (me);
+            _mouseEnteredView = view;
+        }
+
+        if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition)
+        {
+            return;
+        }
+
+        WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;
+
+        //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");
+
+        while (view.NewMouseEvent (me) != true)
+        {
+            if (MouseGrabView is { })
+            {
+                break;
+            }
+
+            if (view is Adornment adornmentView)
+            {
+                view = adornmentView.Parent.SuperView;
+            }
+            else
+            {
+                view = view.SuperView;
+            }
+
+            if (view is null)
+            {
+                break;
+            }
+
+            Point boundsPoint = view.ScreenToViewport (mouseEvent.Position);
+
+            me = new ()
+            {
+                Position = boundsPoint,
+                Flags = mouseEvent.Flags,
+                ScreenPosition = mouseEvent.Position,
+                View = view
+            };
+        }
+
+        BringOverlappedTopToFront ();
+    }
+
+    #endregion Mouse handling
+}

+ 0 - 0
Terminal.Gui/IterationEventArgs.cs → Terminal.Gui/Application/IterationEventArgs.cs


+ 0 - 0
Terminal.Gui/MainLoop.cs → Terminal.Gui/Application/MainLoop.cs


+ 48 - 0
Terminal.Gui/Application/MainLoopSyncContext.cs

@@ -0,0 +1,48 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     provides the sync context set while executing code in Terminal.Gui, to let
+///     users use async/await on their code
+/// </summary>
+internal sealed class MainLoopSyncContext : SynchronizationContext
+{
+    public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); }
+
+    public override void Post (SendOrPostCallback d, object state)
+    {
+        Application.MainLoop?.AddIdle (
+                                       () =>
+                                       {
+                                           d (state);
+
+                                           return false;
+                                       }
+                                      );
+    }
+
+    //_mainLoop.Driver.Wakeup ();
+    public override void Send (SendOrPostCallback d, object state)
+    {
+        if (Thread.CurrentThread.ManagedThreadId == Application._mainThreadId)
+        {
+            d (state);
+        }
+        else
+        {
+            var wasExecuted = false;
+
+            Application.Invoke (
+                                () =>
+                                {
+                                    d (state);
+                                    wasExecuted = true;
+                                }
+                               );
+
+            while (!wasExecuted)
+            {
+                Thread.Sleep (15);
+            }
+        }
+    }
+}

+ 0 - 0
Terminal.Gui/RunState.cs → Terminal.Gui/Application/RunState.cs


+ 0 - 0
Terminal.Gui/RunStateEventArgs.cs → Terminal.Gui/Application/RunStateEventArgs.cs


+ 0 - 0
Terminal.Gui/Timeout.cs → Terminal.Gui/Application/Timeout.cs


+ 0 - 0
Terminal.Gui/TimeoutEventArgs.cs → Terminal.Gui/Application/TimeoutEventArgs.cs


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

@@ -315,12 +315,31 @@ public abstract class ConsoleDriver
                 {
                     Contents [row, c] = new Cell
                     {
-                        Rune = (Rune)' ', 
-                        Attribute = new Attribute (Color.White, Color.Black), 
+                        Rune = (Rune)' ',
+                        Attribute = new Attribute (Color.White, Color.Black),
                         IsDirty = true
                     };
-                    _dirtyLines [row] = true;
                 }
+                _dirtyLines [row] = true;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Sets <see cref="Contents"/> as dirty for situations where views
+    /// don't need layout and redrawing, but just refresh the screen.
+    /// </summary>
+    public void SetContentsAsDirty ()
+    {
+        lock (Contents)
+        {
+            for (var row = 0; row < Rows; row++)
+            {
+                for (var c = 0; c < Cols; c++)
+                {
+                    Contents [row, c].IsDirty = true;
+                }
+                _dirtyLines [row] = true;
             }
         }
     }

+ 1 - 1
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs

@@ -76,7 +76,7 @@ public class EscSeqRequests
                 return false;
             }
 
-            if (found is { } && found.NumOutstanding > 0)
+            if (found is { NumOutstanding: > 0 })
             {
                 return true;
             }

+ 48 - 41
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -208,7 +208,7 @@ internal class NetEvents : IDisposable
 
         while (!cancellationToken.IsCancellationRequested)
         {
-            Task.Delay (100);
+            Task.Delay (100, cancellationToken).Wait (cancellationToken);
 
             if (Console.KeyAvailable)
             {
@@ -223,7 +223,7 @@ internal class NetEvents : IDisposable
 
     private void ProcessInputQueue ()
     {
-        while (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
+        while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
         {
             try
             {
@@ -242,13 +242,8 @@ internal class NetEvents : IDisposable
                 ConsoleModifiers mod = 0;
                 ConsoleKeyInfo newConsoleKeyInfo = default;
 
-                while (true)
+                while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
                 {
-                    if (_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
-                    {
-                        return;
-                    }
-
                     ConsoleKeyInfo consoleKeyInfo;
 
                     try
@@ -338,7 +333,7 @@ internal class NetEvents : IDisposable
             while (!cancellationToken.IsCancellationRequested)
             {
                 // Wait for a while then check if screen has changed sizes
-                Task.Delay (500, cancellationToken);
+                Task.Delay (500, cancellationToken).Wait (cancellationToken);
 
                 int buffHeight, buffWidth;
 
@@ -367,13 +362,8 @@ internal class NetEvents : IDisposable
             cancellationToken.ThrowIfCancellationRequested ();
         }
 
-        while (true)
+        while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
         {
-            if (_inputReadyCancellationTokenSource.IsCancellationRequested)
-            {
-                return;
-            }
-
             try
             {
                 _winChange.Wait (_inputReadyCancellationTokenSource.Token);
@@ -852,11 +842,37 @@ internal class NetDriver : ConsoleDriver
         { }
     }
 
-    #region Not Implemented
+    public override void Suspend ()
+    {
+        if (Environment.OSVersion.Platform != PlatformID.Unix)
+        {
+            return;
+        }
+
+        StopReportingMouseMoves ();
 
-    public override void Suspend () { throw new NotImplementedException (); }
+        if (!RunningUnitTests)
+        {
+            Console.ResetColor ();
+            Console.Clear ();
 
-    #endregion
+            //Disable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll);
+
+            //Set cursor key to cursor.
+            Console.Out.Write (EscSeqUtils.CSI_ShowCursor);
+
+            Platform.Suspend ();
+
+            //Enable alternative screen buffer.
+            Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+
+            SetContentsAsDirty ();
+            Refresh ();
+        }
+
+        StartReportingMouseMoves ();
+    }
 
     public override void UpdateScreen ()
     {
@@ -877,7 +893,7 @@ internal class NetDriver : ConsoleDriver
         Attribute? redrawAttr = null;
         int lastCol = -1;
 
-        CursorVisibility? savedVisibitity = _cachedCursorVisibility;
+        CursorVisibility? savedVisibility = _cachedCursorVisibility;
         SetCursorVisibility (CursorVisibility.Invisible);
 
         for (int row = top; row < rows; row++)
@@ -1006,7 +1022,7 @@ internal class NetDriver : ConsoleDriver
 
         SetCursorPosition (0, 0);
 
-        _cachedCursorVisibility = savedVisibitity;
+        _cachedCursorVisibility = savedVisibility;
 
         void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
         {
@@ -1333,12 +1349,9 @@ internal class NetDriver : ConsoleDriver
     {
         _cachedCursorVisibility = visibility;
 
-        bool isVisible = RunningUnitTests
-                             ? visibility == CursorVisibility.Default
-                             : Console.CursorVisible = visibility == CursorVisibility.Default;
-        Console.Out.Write (isVisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
+        Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
 
-        return isVisible;
+        return visibility == CursorVisibility.Default;
     }
 
     public override bool EnsureCursorVisibility ()
@@ -1667,7 +1680,7 @@ internal class NetMainLoop : IMainLoopDriver
     private readonly CancellationTokenSource _inputHandlerTokenSource = new ();
     private readonly Queue<InputResult?> _resultQueue = new ();
     private readonly ManualResetEventSlim _waitForProbe = new (false);
-    private CancellationTokenSource _eventReadyTokenSource = new ();
+    private readonly CancellationTokenSource _eventReadyTokenSource = new ();
     private MainLoop _mainLoop;
 
     /// <summary>Initializes the class with the console driver.</summary>
@@ -1719,14 +1732,13 @@ internal class NetMainLoop : IMainLoopDriver
             _eventReady.Reset ();
         }
 
+        _eventReadyTokenSource.Token.ThrowIfCancellationRequested ();
+
         if (!_eventReadyTokenSource.IsCancellationRequested)
         {
             return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _);
         }
 
-        _eventReadyTokenSource.Dispose ();
-        _eventReadyTokenSource = new CancellationTokenSource ();
-
         return true;
     }
 
@@ -1783,26 +1795,21 @@ internal class NetMainLoop : IMainLoopDriver
                 return;
             }
 
+            _inputHandlerTokenSource.Token.ThrowIfCancellationRequested ();
+
             if (_resultQueue.Count == 0)
             {
                 _resultQueue.Enqueue (_netEvents.DequeueInput ());
             }
 
-            try
+            while (_resultQueue.Count > 0 && _resultQueue.Peek () is null)
             {
-                while (_resultQueue.Peek () is null)
-                {
-                    _resultQueue.Dequeue ();
-                }
-
-                if (_resultQueue.Count > 0)
-                {
-                    _eventReady.Set ();
-                }
+                _resultQueue.Dequeue ();
             }
-            catch (InvalidOperationException)
+
+            if (_resultQueue.Count > 0)
             {
-                // Ignore
+                _eventReady.Set ();
             }
         }
     }

+ 369 - 0
Terminal.Gui/Drawing/Aligner.cs

@@ -0,0 +1,369 @@
+using System.ComponentModel;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Aligns items within a container based on the specified <see cref="Gui.Alignment"/>. Both horizontal and vertical
+///     alignments are supported.
+/// </summary>
+public class Aligner : INotifyPropertyChanged
+{
+    private Alignment _alignment;
+
+    /// <summary>
+    ///     Gets or sets how the <see cref="Aligner"/> aligns items within a container.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         <see cref="AlignmentModes"/> provides additional options for aligning items in a container.
+    ///     </para>
+    /// </remarks>
+    public Alignment Alignment
+    {
+        get => _alignment;
+        set
+        {
+            _alignment = value;
+            PropertyChanged?.Invoke (this, new (nameof (Alignment)));
+        }
+    }
+
+    private AlignmentModes _alignmentMode = AlignmentModes.StartToEnd;
+
+    /// <summary>
+    ///     Gets or sets the modes controlling <see cref="Alignment"/>.
+    /// </summary>
+    public AlignmentModes AlignmentModes
+    {
+        get => _alignmentMode;
+        set
+        {
+            _alignmentMode = value;
+            PropertyChanged?.Invoke (this, new (nameof (AlignmentModes)));
+        }
+    }
+
+    private int _containerSize;
+
+    /// <summary>
+    ///     The size of the container.
+    /// </summary>
+    public int ContainerSize
+    {
+        get => _containerSize;
+        set
+        {
+            _containerSize = value;
+            PropertyChanged?.Invoke (this, new (nameof (ContainerSize)));
+        }
+    }
+
+    /// <inheritdoc/>
+    public event PropertyChangedEventHandler PropertyChanged;
+
+    /// <summary>
+    ///     Takes a list of item sizes and returns a list of the positions of those items when aligned within
+    ///     <see name="ContainerSize"/>
+    ///     using the <see cref="Alignment"/> and <see cref="AlignmentModes"/> settings.
+    /// </summary>
+    /// <param name="sizes">The sizes of the items to align.</param>
+    /// <returns>The locations of the items, from left/top to right/bottom.</returns>
+    public int [] Align (int [] sizes) { return Align (Alignment, AlignmentModes, ContainerSize, sizes); }
+
+    /// <summary>
+    ///     Takes a list of item sizes and returns a list of the  positions of those items when aligned within
+    ///     <paramref name="containerSize"/>
+    ///     using specified parameters.
+    /// </summary>
+    /// <param name="alignment">Specifies how the items will be aligned.</param>
+    /// <param name="alignmentMode"></param>
+    /// <param name="containerSize">The size of the container.</param>
+    /// <param name="sizes">The sizes of the items to align.</param>
+    /// <returns>The positions of the items, from left/top to right/bottom.</returns>
+    public static int [] Align (in Alignment alignment, in AlignmentModes alignmentMode, in int containerSize, in int [] sizes)
+    {
+        if (sizes.Length == 0)
+        {
+            return [];
+        }
+
+        var sizesCopy = sizes;
+        if (alignmentMode.FastHasFlags (AlignmentModes.EndToStart))
+        {
+            sizesCopy = sizes.Reverse ().ToArray ();
+        }
+
+        int maxSpaceBetweenItems = alignmentMode.FastHasFlags (AlignmentModes.AddSpaceBetweenItems) ? 1 : 0;
+        int totalItemsSize = sizes.Sum ();
+        int totalGaps = sizes.Length - 1; // total gaps between items
+        int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spacesToGive if we had enough room
+        int spacesToGive = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out
+
+        if (totalItemsSize >= containerSize)
+        {
+            spacesToGive = 0;
+        }
+        else if (totalItemsAndSpaces > containerSize)
+        {
+            spacesToGive = containerSize - totalItemsSize;
+        }
+
+        switch (alignment)
+        {
+            case Alignment.Start:
+                switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+                {
+                    case AlignmentModes.StartToEnd:
+                        return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive);
+
+                    case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast:
+                        return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
+
+                    case AlignmentModes.EndToStart:
+                        return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray ();
+
+                    case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast:
+                        return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ;
+                }
+
+                break;
+
+            case Alignment.End:
+                switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+                {
+                    case AlignmentModes.StartToEnd:
+                        return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
+
+                    case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast:
+                        return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
+
+                    case AlignmentModes.EndToStart:
+                        return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray ();
+
+                    case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast:
+                        return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ;
+                }
+
+                break;
+
+            case Alignment.Center:
+                switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+                {
+                    case AlignmentModes.StartToEnd:
+                        return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
+
+                    case AlignmentModes.EndToStart:
+                        return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray ();
+                }
+
+                break;
+
+            case Alignment.Fill:
+                switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems)
+                {
+                    case AlignmentModes.StartToEnd:
+                        return Fill (in sizesCopy, containerSize, totalItemsSize);
+
+                    case AlignmentModes.EndToStart:
+                        return Fill (in sizesCopy, containerSize, totalItemsSize).Reverse ().ToArray ();
+                }
+
+                break;
+
+            default:
+                throw new ArgumentOutOfRangeException (nameof (alignment), alignment, null);
+        }
+
+        return [];
+    }
+
+    internal static int [] Start (ref readonly int [] sizes, int maxSpaceBetweenItems, int spacesToGive)
+    {
+        var positions = new int [sizes.Length]; // positions of the items. the return value.
+
+        for (var i = 0; i < sizes.Length; i++)
+        {
+            CheckSizeCannotBeNegative (i, in sizes);
+
+            if (i == 0)
+            {
+                positions [0] = 0; // first item position
+
+                continue;
+            }
+
+            int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
+
+            // subsequent items are placed one space after the previous item
+            positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
+        }
+
+        return positions;
+    }
+
+    internal static int [] IgnoreFirst (
+        ref readonly int [] sizes,
+        int containerSize,
+        int totalItemsSize,
+        int maxSpaceBetweenItems,
+        int spacesToGive
+    )
+    {
+        var positions = new int [sizes.Length]; // positions of the items. the return value.
+
+        if (sizes.Length > 1)
+        {
+            var currentPosition = 0;
+            positions [0] = currentPosition; // first item is flush left
+
+            for (int i = sizes.Length - 1; i >= 0; i--)
+            {
+                CheckSizeCannotBeNegative (i, in sizes);
+
+                if (i == sizes.Length - 1)
+                {
+                    // start at right
+                    currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i];
+                    positions [i] = currentPosition;
+                }
+
+                if (i < sizes.Length - 1 && i > 0)
+                {
+                    int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
+
+                    positions [i] = currentPosition - sizes [i] - spaceBefore;
+                    currentPosition = positions [i];
+                }
+            }
+        }
+        else if (sizes.Length == 1)
+        {
+            CheckSizeCannotBeNegative (0, in sizes);
+            positions [0] = 0; // single item is flush left
+        }
+
+        return positions;
+    }
+
+    internal static int [] IgnoreLast (
+        ref readonly int [] sizes,
+        int containerSize,
+        int totalItemsSize,
+        int maxSpaceBetweenItems,
+        int spacesToGive
+    )
+    {
+        var positions = new int [sizes.Length]; // positions of the items. the return value.
+
+        if (sizes.Length > 1)
+        {
+            var currentPosition = 0;
+            if (totalItemsSize > containerSize)
+            {
+                currentPosition = containerSize - totalItemsSize - spacesToGive;
+            }
+
+            for (var i = 0; i < sizes.Length; i++)
+            {
+                CheckSizeCannotBeNegative (i, in sizes);
+
+                if (i < sizes.Length - 1)
+                {
+                    int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
+
+                    positions [i] = currentPosition;
+                    currentPosition += sizes [i] + spaceBefore;
+                }
+            }
+
+            positions [sizes.Length - 1] = containerSize - sizes [^1];
+        }
+        else if (sizes.Length == 1)
+        {
+            CheckSizeCannotBeNegative (0, in sizes);
+
+            positions [0] = containerSize - sizes [0]; // single item is flush right
+        }
+
+        return positions;
+    }
+
+    internal static int [] Fill (ref readonly int [] sizes, int containerSize, int totalItemsSize)
+    {
+        var positions = new int [sizes.Length]; // positions of the items. the return value.
+
+        int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0;
+        int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0;
+        var currentPosition = 0;
+
+        for (var i = 0; i < sizes.Length; i++)
+        {
+            CheckSizeCannotBeNegative (i, in sizes);
+            positions [i] = currentPosition;
+            int extraSpace = i < remainder ? 1 : 0;
+            currentPosition += sizes [i] + spaceBetween + extraSpace;
+        }
+
+        return positions;
+    }
+
+    internal static int [] Center (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive)
+    {
+        var positions = new int [sizes.Length]; // positions of the items. the return value.
+
+        if (sizes.Length > 1)
+        {
+            // remaining space to be distributed before first and after the items
+            int remainingSpace = containerSize - totalItemsSize - spacesToGive;
+
+            for (var i = 0; i < sizes.Length; i++)
+            {
+                CheckSizeCannotBeNegative (i, in sizes);
+
+                if (i == 0)
+                {
+                    positions [i] = remainingSpace / 2; // first item position
+
+                    continue;
+                }
+
+                int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
+
+                // subsequent items are placed one space after the previous item
+                positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
+            }
+        }
+        else if (sizes.Length == 1)
+        {
+            CheckSizeCannotBeNegative (0, in sizes);
+            positions [0] = (containerSize - sizes [0]) / 2; // single item is centered
+        }
+
+        return positions;
+    }
+
+    internal static int [] End (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive)
+    {
+        var positions = new int [sizes.Length]; // positions of the items. the return value.
+        int currentPosition = containerSize - totalItemsSize - spacesToGive;
+
+        for (var i = 0; i < sizes.Length; i++)
+        {
+            CheckSizeCannotBeNegative (i, in sizes);
+            int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
+
+            positions [i] = currentPosition;
+            currentPosition += sizes [i] + spaceBefore;
+        }
+
+        return positions;
+    }
+
+    private static void CheckSizeCannotBeNegative (int i, ref readonly int [] sizes)
+    {
+        if (sizes [i] < 0)
+        {
+            throw new ArgumentException ("The size of an item cannot be negative.");
+        }
+    }
+}

+ 82 - 0
Terminal.Gui/Drawing/Alignment.cs

@@ -0,0 +1,82 @@
+using Terminal.Gui.Analyzers.Internal.Attributes;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Determines the position of items when arranged in a container.
+/// </summary>
+[GenerateEnumExtensionMethods (FastHasFlags = true)]
+
+public enum Alignment
+{
+    /// <summary>
+    ///     The items will be aligned to the start (left or top) of the container.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         If the container is smaller than the total size of the items, the end items will be clipped (their locations
+    ///         will be greater than the container size).
+    ///     </para>
+    ///     <para>
+    ///         The <see cref="AlignmentModes"/> enumeration provides additional options for aligning items in a container.
+    ///     </para>
+    /// </remarks>
+    /// <example>
+    ///     <c>
+    ///         |111 2222 33333    |
+    ///     </c>
+    /// </example>
+    Start = 0,
+
+    /// <summary>
+    ///     The items will be aligned to the end (right or bottom) of the container.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         If the container is smaller than the total size of the items, the start items will be clipped (their locations
+    ///         will be negative).
+    ///     </para>
+    ///     <para>
+    ///         The <see cref="AlignmentModes"/> enumeration provides additional options for aligning items in a container.
+    ///     </para>
+    /// </remarks>
+    /// <example>
+    ///     <c>
+    ///         |    111 2222 33333|
+    ///     </c>
+    /// </example>
+    End,
+
+    /// <summary>
+    ///     Center in the available space.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///     If centering is not possible, the group will be left-aligned.
+    ///     </para>
+    ///     <para>
+    ///         Extra space will be distributed between the items, biased towards the left.
+    ///     </para>
+    /// </remarks>
+    /// <example>
+    ///     <c>
+    ///         |  111 2222 33333  |
+    ///     </c>
+    /// </example>
+    Center,
+
+    /// <summary>
+    ///     The items will fill the available space.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Extra space will be distributed between the items, biased towards the end.
+    ///     </para>
+    /// </remarks>
+    /// <example>
+    ///     <c>
+    ///        |111  2222    33333|
+    ///     </c>
+    /// </example>
+    Fill,
+}

+ 52 - 0
Terminal.Gui/Drawing/AlignmentModes.cs

@@ -0,0 +1,52 @@
+using Terminal.Gui.Analyzers.Internal.Attributes;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Determines alignment modes for <see cref="Alignment"/>.
+/// </summary>
+[Flags]
+[GenerateEnumExtensionMethods (FastHasFlags = true)]
+public enum AlignmentModes
+{
+    /// <summary>
+    ///     The items will be arranged from start (left/top) to end (right/bottom).
+    /// </summary>
+    StartToEnd = 0,
+
+    /// <summary>
+    ///     The items will be arranged from end (right/bottom) to start (left/top).
+    /// </summary>
+    /// <remarks>
+    ///     Not implemented.
+    /// </remarks>
+    EndToStart = 1,
+
+    /// <summary>
+    ///     At least one space will be added between items. Useful for justifying text where at least one space is needed.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         If the total size of the items is greater than the container size, the space between items will be ignored
+    ///         starting from the end.
+    ///     </para>
+    /// </remarks>
+    AddSpaceBetweenItems = 2,
+
+    /// <summary>
+    ///    When aligning via <see cref="Alignment.Start"/> or <see cref="Alignment.End"/>, the item opposite to the alignment (the first or last item) will be ignored.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         If the container is smaller than the total size of the items, the end items will be clipped (their locations
+    ///         will be greater than the container size).
+    ///     </para>
+    /// </remarks>
+    /// <example>
+    ///     <c>
+    ///         Start: |111 2222     33333|
+    ///         End:   |111     2222 33333|
+    ///     </c>
+    /// </example>
+    IgnoreFirstOrLast = 4,
+}

+ 0 - 333
Terminal.Gui/Drawing/Justification.cs

@@ -1,333 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>
-///     Controls how the <see cref="Justifier"/> justifies items within a container. 
-/// </summary>
-public enum Justification
-{
-    /// <summary>
-    ///     The items will be aligned to the left.
-    ///     Set <see cref="Justifier.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
-    ///     each item.
-    /// </summary>
-    /// <example>
-    ///     <c>
-    ///         111 2222 33333
-    ///     </c>
-    /// </example>
-    Left,
-
-    /// <summary>
-    ///     The items will be aligned to the right.
-    ///     Set <see cref="Justifier.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
-    ///     each item.
-    /// </summary>
-    /// <example>
-    ///     <c>
-    ///         111 2222 33333
-    ///     </c>
-    /// </example>
-    Right,
-
-    /// <summary>
-    ///     The group will be centered in the container.
-    ///     If centering is not possible, the group will be left-justified.
-    ///     Set <see cref="Justifier.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
-    ///     each item.
-    /// </summary>
-    /// <example>
-    ///     <c>
-    ///         111 2222 33333
-    ///     </c>
-    /// </example>
-    Centered,
-
-    /// <summary>
-    ///     The items will be justified. Space will be added between the items such that the first item
-    ///     is at the start and the right side of the last item against the end.
-    ///     Set <see cref="Justifier.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
-    ///     each item.
-    /// </summary>
-    /// <example>
-    ///     <c>
-    ///         111    2222     33333
-    ///     </c>
-    /// </example>
-    Justified,
-
-    /// <summary>
-    ///     The first item will be aligned to the left and the remaining will aligned to the right.
-    ///     Set <see cref="Justifier.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
-    ///     each item.
-    /// </summary>
-    /// <example>
-    ///     <c>
-    ///         111        2222 33333
-    ///     </c>
-    /// </example>
-    FirstLeftRestRight,
-
-    /// <summary>
-    ///     The last item will be aligned to the right and the remaining will aligned to the left.
-    ///     Set <see cref="Justifier.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
-    ///     each item.
-    /// </summary>
-    /// <example>
-    ///     <c>
-    ///         111 2222        33333
-    ///     </c>
-    /// </example>
-    LastRightRestLeft
-}
-
-/// <summary>
-///     Justifies items within a container based on the specified <see cref="Justification"/>.
-/// </summary>
-public class Justifier
-{
-    /// <summary>
-    /// Gets or sets how the <see cref="Justifier"/> justifies items within a container.
-    /// </summary>
-    public Justification Justification { get; set; }
-
-    /// <summary>
-    /// The size of the container.
-    /// </summary>
-    public int ContainerSize { get; set; }
-
-    /// <summary>
-    ///     Gets or sets whether <see cref="Justify(int[])"/> puts a space is placed between items. Default is <see langword="false"/>. If <see langword="true"/>, a space will be
-    ///     placed between each item, which is useful for justifying text.
-    /// </summary>
-    public bool PutSpaceBetweenItems { get; set; }
-
-    /// <summary>
-    ///     Takes a list of items and returns their positions when justified within a container <see name="ContainerSize"/> wide based on the specified
-    ///     <see cref="Justification"/>.
-    /// </summary>
-    /// <param name="sizes">The sizes of the items to justify.</param>
-    /// <returns>The locations of the items, from left to right.</returns>
-    public int [] Justify (int [] sizes)
-    {
-        return Justify (Justification, PutSpaceBetweenItems, ContainerSize, sizes);
-    }
-
-    /// <summary>
-    ///     Takes a list of items and returns their positions when justified within a container <paramref name="containerSize"/> wide based on the specified
-    ///     <see cref="Justification"/>.
-    /// </summary>
-    /// <param name="sizes">The sizes of the items to justify.</param>
-    /// <param name="justification">The justification style.</param>
-    /// <param name="putSpaceBetweenItems"></param>
-    /// <param name="containerSize">The size of the container.</param>
-    /// <returns>The locations of the items, from left to right.</returns>
-    public static int [] Justify (Justification justification, bool putSpaceBetweenItems, int containerSize, int [] sizes)
-    {
-        if (sizes.Length == 0)
-        {
-            return new int [] { };
-        }
-
-        int maxSpaceBetweenItems = putSpaceBetweenItems ? 1 : 0;
-
-        var positions = new int [sizes.Length]; // positions of the items. the return value.
-        int totalItemsSize = sizes.Sum ();
-        int totalGaps = sizes.Length - 1; // total gaps between items
-        int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spaces if we had enough room
-
-        int spaces = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out
-        if (totalItemsSize >= containerSize)
-        {
-            spaces = 0;
-        }
-        else if (totalItemsAndSpaces > containerSize)
-        {
-            spaces = containerSize - totalItemsSize;
-        }
-
-        switch (justification)
-        {
-            case Justification.Left:
-                var currentPosition = 0;
-
-                for (var i = 0; i < sizes.Length; i++)
-                {
-                    if (sizes [i] < 0)
-                    {
-                        throw new ArgumentException ("The size of an item cannot be negative.");
-                    }
-
-                    if (i == 0)
-                    {
-                        positions [0] = 0; // first item position
-
-                        continue;
-                    }
-
-                    int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
-
-                    // subsequent items are placed one space after the previous item
-                    positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
-                }
-
-                break;
-            case Justification.Right:
-                currentPosition = Math.Max (0, containerSize - totalItemsSize - spaces);
-
-                for (var i = 0; i < sizes.Length; i++)
-                {
-                    if (sizes [i] < 0)
-                    {
-                        throw new ArgumentException ("The size of an item cannot be negative.");
-                    }
-
-                    int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
-
-                    positions [i] = currentPosition;
-                    currentPosition += sizes [i] + spaceBefore;
-                }
-
-                break;
-
-            case Justification.Centered:
-                if (sizes.Length > 1)
-                {
-                    // remaining space to be distributed before first and after the items
-                    int remainingSpace = Math.Max (0, containerSize - totalItemsSize - spaces);
-
-                    for (var i = 0; i < sizes.Length; i++)
-                    {
-                        if (sizes [i] < 0)
-                        {
-                            throw new ArgumentException ("The size of an item cannot be negative.");
-                        }
-
-                        if (i == 0)
-                        {
-                            positions [i] = remainingSpace / 2; // first item position
-
-                            continue;
-                        }
-
-                        int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
-
-                        // subsequent items are placed one space after the previous item
-                        positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
-                    }
-                }
-                else if (sizes.Length == 1)
-                {
-                    if (sizes [0] < 0)
-                    {
-                        throw new ArgumentException ("The size of an item cannot be negative.");
-                    }
-
-                    positions [0] = (containerSize - sizes [0]) / 2; // single item is centered
-                }
-
-                break;
-
-            case Justification.Justified:
-                int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0;
-                int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0;
-                currentPosition = 0;
-
-                for (var i = 0; i < sizes.Length; i++)
-                {
-                    if (sizes [i] < 0)
-                    {
-                        throw new ArgumentException ("The size of an item cannot be negative.");
-                    }
-
-                    positions [i] = currentPosition;
-                    int extraSpace = i < remainder ? 1 : 0;
-                    currentPosition += sizes [i] + spaceBetween + extraSpace;
-                }
-
-                break;
-
-            // 111 2222        33333
-            case Justification.LastRightRestLeft:
-                if (sizes.Length > 1)
-                {
-                    currentPosition = 0;
-
-                    for (var i = 0; i < sizes.Length; i++)
-                    {
-                        if (sizes [i] < 0)
-                        {
-                            throw new ArgumentException ("The size of an item cannot be negative.");
-                        }
-
-                        if (i < sizes.Length - 1)
-                        {
-                            int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
-
-                            positions [i] = currentPosition;
-                            currentPosition += sizes [i] + spaceBefore;
-                        }
-                    }
-
-                    positions [sizes.Length - 1] = containerSize - sizes [sizes.Length - 1];
-                }
-                else if (sizes.Length == 1)
-                {
-                    if (sizes [0] < 0)
-                    {
-                        throw new ArgumentException ("The size of an item cannot be negative.");
-                    }
-
-                    positions [0] = containerSize - sizes [0]; // single item is flush right
-                }
-
-                break;
-
-            // 111        2222 33333
-            case Justification.FirstLeftRestRight:
-                if (sizes.Length > 1)
-                {
-                    currentPosition = 0;
-                    positions [0] = currentPosition; // first item is flush left
-
-                    for (int i = sizes.Length - 1; i >= 0; i--)
-                    {
-                        if (sizes [i] < 0)
-                        {
-                            throw new ArgumentException ("The size of an item cannot be negative.");
-                        }
-
-                        if (i == sizes.Length - 1)
-                        {
-                            // start at right
-                            currentPosition = containerSize - sizes [i];
-                            positions [i] = currentPosition;
-                        }
-
-                        if (i < sizes.Length - 1 && i > 0)
-                        {
-                            int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
-
-                            positions [i] = currentPosition - sizes [i] - spaceBefore;
-                            currentPosition = positions [i];
-                        }
-                    }
-                }
-                else if (sizes.Length == 1)
-                {
-                    if (sizes [0] < 0)
-                    {
-                        throw new ArgumentException ("The size of an item cannot be negative.");
-                    }
-
-                    positions [0] = 0; // single item is flush left
-                }
-
-                break;
-
-            default:
-                throw new ArgumentOutOfRangeException (nameof (justification), justification, null);
-        }
-
-        return positions;
-    }
-}

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

@@ -230,8 +230,8 @@ public class Thickness : IEquatable<Thickness>
             var tf = new TextFormatter
             {
                 Text = label is null ? string.Empty : $"{label} {this}",
-                Alignment = TextAlignment.Centered,
-                VerticalAlignment = VerticalTextAlignment.Bottom,
+                Alignment = Alignment.Center,
+                VerticalAlignment = Alignment.End,
                 AutoSize = true
             };
             tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect);

+ 41 - 0
Terminal.Gui/Input/CommandContext.cs

@@ -0,0 +1,41 @@
+#nullable enable
+namespace Terminal.Gui;
+/// <summary>
+///     Provides context for a <see cref="Command"/> that is being invoked.
+/// </summary
+/// <remarks>
+///     <para>
+///         To define a <see cref="Command"/> that is invoked with context,
+///         use <see cref="View.AddCommand(Command,Func{CommandContext,Nullable{bool}})"/>
+///     </para>
+/// </remarks>
+public record struct CommandContext
+{
+    /// <summary>
+    ///     Initializes a new instance of <see cref="CommandContext"/> with the specified <see cref="Command"/>,
+    /// </summary>
+    /// <param name="command"></param>
+    /// <param name="key"></param>
+    /// <param name="keyBinding"></param>
+    public CommandContext (Command command, Key? key, KeyBinding? keyBinding = null)
+    {
+        Command = command;
+        Key = key;
+        KeyBinding = keyBinding;
+    }
+
+    /// <summary>
+    ///     The <see cref="Command"/> that is being invoked.
+    /// </summary>
+    public Command Command { get; set; }
+
+    /// <summary>
+    ///     The <see cref="Key"/> that is being invoked. This is the key that was pressed to invoke the <see cref="Command"/>.
+    /// </summary>
+    public Key? Key { get; set; }
+
+    /// <summary>
+    /// The KeyBinding that was used to invoke the <see cref="Command"/>, if any.
+    /// </summary>
+    public KeyBinding? KeyBinding { get; set; }
+}

+ 14 - 240
Terminal.Gui/Input/KeyBinding.cs

@@ -1,260 +1,34 @@
-// These classes use a key binding system based on the design implemented in Scintilla.Net which is an
+#nullable enable
+
+// These classes use a key binding system based on the design implemented in Scintilla.Net which is an
 // MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs
 
 namespace Terminal.Gui;
 
 /// <summary>
-///     Defines the scope of a <see cref="Command"/> that has been bound to a key with
-///     <see cref="KeyBindings.Add(Key, Terminal.Gui.Command[])"/>.
+/// Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.
 /// </summary>
-/// <remarks>
-///     <para>Key bindings are scoped to the most-focused view (<see cref="Focused"/>) by default.</para>
-/// </remarks>
-[Flags]
-public enum KeyBindingScope
-{
-    /// <summary>The key binding is scoped to just the view that has focus.</summary>
-    Focused = 1,
-
-    /// <summary>
-    ///     The key binding is scoped to the View's SuperView and will be triggered even when the View does not have focus, as
-    ///     long as the SuperView does have focus. This is typically used for <see cref="View.HotKey"/>s.
-    ///     <remarks>
-    ///         <para>
-    ///             Use for Views such as MenuBar and StatusBar which provide commands (shortcuts etc...) that trigger even
-    ///             when not focused.
-    ///         </para>
-    ///         <para>
-    ///             HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or
-    ///             any of its subviews.
-    ///         </para>
-    ///     </remarks>
-    /// </summary>
-    HotKey = 2,
-
-    /// <summary>
-    ///     The key binding will be triggered regardless of which view has focus. This is typically used for global
-    ///     commands.
-    /// </summary>
-    /// <remarks>
-    ///     Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or
-    ///     any of its subviews, and if the key down event was not bound to a <see cref="View.HotKey"/>.
-    /// </remarks>
-    Application = 4
-}
-
-/// <summary>Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.</summary>
-public class KeyBinding
+public record struct KeyBinding
 {
     /// <summary>Initializes a new instance.</summary>
-    /// <param name="commands"></param>
-    /// <param name="scope"></param>
-    public KeyBinding (Command [] commands, KeyBindingScope scope)
+    /// <param name="commands">The commands this key binding will invoke.</param>
+    /// <param name="scope">The scope of the <see cref="Commands"/>.</param>
+    /// <param name="context">Arbitrary context that can be associated with this key binding.</param>
+    public KeyBinding (Command [] commands, KeyBindingScope scope, object? context = null)
     {
         Commands = commands;
         Scope = scope;
+        Context = context;
     }
 
-    /// <summary>The actions which can be performed by the application or bound to keys in a <see cref="View"/> control.</summary>
+    /// <summary>The commands this key binding will invoke.</summary>
     public Command [] Commands { get; set; }
 
-    /// <summary>The scope of the <see cref="Commands"/> bound to a key.</summary>
+    /// <summary>The scope of the <see cref="Commands"/>.</summary>
     public KeyBindingScope Scope { get; set; }
-}
-
-/// <summary>A class that provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.</summary>
-public class KeyBindings
-{
-    // TODO: Add a dictionary comparer that ignores Scope
-    /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
-    public Dictionary<Key, KeyBinding> Bindings { get; } = new ();
-
-    /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
-    /// <param name="key"></param>
-    /// <param name="binding"></param>
-    public void Add (Key key, KeyBinding binding) { Bindings.Add (key, binding); }
-
-    /// <summary>
-    ///     <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
-    ///     <para>
-    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
-    ///         <paramref name="commands"/>.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
-    ///     focus to another view and perform multiple commands there).
-    /// </remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="scope">The scope for the command.</param>
-    /// <param name="commands">
-    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
-    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
-    ///     consumed if any took effect.
-    /// </param>
-    public void Add (Key key, KeyBindingScope scope, params Command [] commands)
-    {
-        if (key is null || !key.IsValid)
-        {
-            //throw new ArgumentException ("Invalid Key", nameof (commands));
-            return;
-        }
-        
-        if (commands.Length == 0)
-        {
-            throw new ArgumentException (@"At least one command must be specified", nameof (commands));
-        }
-
-        if (TryGet (key, out KeyBinding _))
-        {
-            Bindings [key] = new KeyBinding (commands, scope);
-        }
-        else
-        {
-            Bindings.Add (key, new KeyBinding (commands, scope));
-        }
-    }
-
-    /// <summary>
-    ///     <para>
-    ///         Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
-    ///         View - see <see cref="View.GetSupportedCommands"/>).
-    ///     </para>
-    ///     <para>
-    ///         This is a helper function for <see cref="Add(Key,KeyBindingScope,Terminal.Gui.Command[])"/> for
-    ///         <see cref="KeyBindingScope.Focused"/> scoped commands.
-    ///     </para>
-    ///     <para>
-    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
-    ///         <paramref name="commands"/>.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
-    ///     focus to another view and perform multiple commands there).
-    /// </remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="commands">
-    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
-    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
-    ///     consumed if any took effect.
-    /// </param>
-    public void Add (Key key, params Command [] commands) { Add (key, KeyBindingScope.Focused, commands); }
-
-    /// <summary>Removes all <see cref="KeyBinding"/> objects from the collection.</summary>
-    public void Clear () { Bindings.Clear (); }
 
     /// <summary>
-    ///     Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to
-    ///     the same command sets and this method will clear all of them.
+    ///     Arbitrary context that can be associated with this key binding.
     /// </summary>
-    /// <param name="command"></param>
-    public void Clear (params Command [] command)
-    {
-        var kvps = Bindings
-                   .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
-                   .ToArray ();
-        foreach (KeyValuePair<Key, KeyBinding> kvp in kvps)
-        {
-            Bindings.Remove (kvp.Key);
-        }
-    }
-
-    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
-    /// <param name="key"></param>
-    /// <returns></returns>
-    public KeyBinding Get (Key key) { return TryGet (key, out KeyBinding binding) ? binding : null; }
-
-    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
-    /// <param name="key"></param>
-    /// <param name="scope"></param>
-    /// <returns></returns>
-    public KeyBinding Get (Key key, KeyBindingScope scope) { return TryGet (key, scope, out KeyBinding binding) ? binding : null; }
-
-    /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="key"/> if it exists.</summary>
-    /// <param name="key">The key to check.</param>
-    /// <returns>
-    ///     The array of <see cref="Command"/>s if <paramref name="key"/> is bound. An empty <see cref="Command"/> array
-    ///     if not.
-    /// </returns>
-    public Command [] GetCommands (Key key)
-    {
-        if (TryGet (key, out KeyBinding bindings))
-        {
-            return bindings.Commands;
-        }
-
-        return Array.Empty<Command> ();
-    }
-
-    /// <summary>Gets the Key used by a set of commands.</summary>
-    /// <remarks></remarks>
-    /// <param name="commands">The set of commands to search.</param>
-    /// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
-    /// <exception cref="InvalidOperationException">If no matching set of commands was found.</exception>
-    public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; }
-
-    /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
-    /// <param name="key"></param>
-    public void Remove (Key key) { Bindings.Remove (key); }
-
-    /// <summary>Replaces a key combination already bound to a set of <see cref="Command"/>s.</summary>
-    /// <remarks></remarks>
-    /// <param name="fromKey">The key to be replaced.</param>
-    /// <param name="toKey">The new key to be used.</param>
-    public void Replace (Key fromKey, Key toKey)
-    {
-        if (!TryGet (fromKey, out KeyBinding _))
-        {
-            return;
-        }
-
-        KeyBinding value = Bindings [fromKey];
-        Bindings.Remove (fromKey);
-        Bindings [toKey] = value;
-    }
-
-    /// <summary>Gets the commands bound with the specified Key.</summary>
-    /// <remarks></remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="binding">
-    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
-    ///     found; otherwise, null. This parameter is passed uninitialized.
-    /// </param>
-    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
-    public bool TryGet (Key key, out KeyBinding binding)
-    {
-        if (key.IsValid)
-        {
-            return Bindings.TryGetValue (key, out binding);
-        }
-
-        binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
-
-        return false;
-    }
-
-    /// <summary>Gets the commands bound with the specified Key that are scoped to a particular scope.</summary>
-    /// <remarks></remarks>
-    /// <param name="key">The key to check.</param>
-    /// <param name="scope">the scope to filter on</param>
-    /// <param name="binding">
-    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
-    ///     found; otherwise, null. This parameter is passed uninitialized.
-    /// </param>
-    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
-    public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
-    {
-        if (key.IsValid && Bindings.TryGetValue (key, out binding))
-        {
-            if (scope.HasFlag (binding.Scope))
-            {
-                return true;
-            }
-        }
-
-        binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
-
-        return false;
-    }
+    public object? Context { get; set; }
 }

+ 46 - 0
Terminal.Gui/Input/KeyBindingScope.cs

@@ -0,0 +1,46 @@
+using Terminal.Gui.Analyzers.Internal.Attributes;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Defines the scope of a <see cref="Command"/> that has been bound to a key with
+///     <see cref="KeyBindings.Add(Key, Terminal.Gui.Command[])"/>.
+/// </summary>
+/// <remarks>
+///     <para>Key bindings are scoped to the most-focused view (<see cref="Focused"/>) by default.</para>
+/// </remarks>
+[Flags]
+[GenerateEnumExtensionMethods (FastHasFlags = true)]
+public enum KeyBindingScope
+{
+    /// <summary>The key binding is scoped to just the view that has focus.</summary>
+    Focused = 1,
+
+    /// <summary>
+    ///     The key binding is scoped to the View's Superview hierarchy and will be triggered even when the View does not have
+    ///     focus, as
+    ///     long as the SuperView does have focus. This is typically used for <see cref="View.HotKey"/>s.
+    ///     <remarks>
+    ///         <para>
+    ///             The View must be visible.
+    ///         </para>
+    ///         <para>
+    ///             HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or
+    ///             any of its subviews.
+    ///         </para>
+    ///     </remarks>
+    /// </summary>
+    HotKey = 2,
+
+    /// <summary>
+    ///     The key binding will be triggered regardless of which view has focus. This is typically used for global
+    ///     commands, which are called Shortcuts.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or
+    ///         any of its subviews, and if the key was not bound to a <see cref="View.HotKey"/>.
+    ///     </para>
+    /// </remarks>
+    Application = 4
+}

+ 258 - 0
Terminal.Gui/Input/KeyBindings.cs

@@ -0,0 +1,258 @@
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.
+/// </summary>
+public class KeyBindings
+{
+    /// <summary>
+    ///     Initializes a new instance. This constructor is used when the <see cref="KeyBindings"/> are not bound to a
+    ///     <see cref="View"/>, such as in unit tests.
+    /// </summary>
+    public KeyBindings () { }
+
+    /// <summary>Initializes a new instance bound to <paramref name="boundView"/>.</summary>
+    public KeyBindings (View boundView) { BoundView = boundView; }
+
+    /// <summary>
+    ///     The view that the <see cref="KeyBindings"/> are bound to.
+    /// </summary>
+    public View? BoundView { get; }
+
+    // TODO: Add a dictionary comparer that ignores Scope
+    // TODO: This should not be public!
+    /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
+    public Dictionary<Key, KeyBinding> Bindings { get; } = new ();
+
+    /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
+    /// <param name="key"></param>
+    /// <param name="binding"></param>
+    public void Add (Key key, KeyBinding binding)
+    {
+        if (TryGet (key, out KeyBinding _))
+        {
+            Bindings [key] = binding;
+        }
+        else
+        {
+            Bindings.Add (key, binding);
+            if (binding.Scope.FastHasFlags (KeyBindingScope.Application))
+            {
+                Application.AddKeyBinding (key, BoundView);
+            }
+        }
+    }
+
+    /// <summary>
+    ///     <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
+    ///     <para>
+    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
+    ///         <paramref name="commands"/>.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
+    ///     focus to another view and perform multiple commands there).
+    /// </remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="scope">The scope for the command.</param>
+    /// <param name="commands">
+    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
+    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
+    ///     consumed if any took effect.
+    /// </param>
+    public void Add (Key key, KeyBindingScope scope, params Command [] commands)
+    {
+        if (key is null || !key.IsValid)
+        {
+            //throw new ArgumentException ("Invalid Key", nameof (commands));
+            return;
+        }
+
+        if (commands.Length == 0)
+        {
+            throw new ArgumentException (@"At least one command must be specified", nameof (commands));
+        }
+
+        if (TryGet (key, out KeyBinding _))
+        {
+            Bindings [key] = new (commands, scope);
+        }
+        else
+        {
+            Add (key, new KeyBinding (commands, scope));
+        }
+    }
+
+    /// <summary>
+    ///     <para>
+    ///         Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
+    ///         View - see <see cref="View.GetSupportedCommands"/>).
+    ///     </para>
+    ///     <para>
+    ///         This is a helper function for <see cref="Add(Key,KeyBindingScope,Terminal.Gui.Command[])"/> for
+    ///         <see cref="KeyBindingScope.Focused"/> scoped commands.
+    ///     </para>
+    ///     <para>
+    ///         If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
+    ///         <paramref name="commands"/>.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
+    ///     focus to another view and perform multiple commands there).
+    /// </remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="commands">
+    ///     The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
+    ///     multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
+    ///     consumed if any took effect.
+    /// </param>
+    public void Add (Key key, params Command [] commands)
+    {
+        Add (key, KeyBindingScope.Focused, commands);
+    }
+
+    /// <summary>Removes all <see cref="KeyBinding"/> objects from the collection.</summary>
+    public void Clear ()
+    {
+        Application.ClearKeyBindings (BoundView);
+
+        Bindings.Clear ();
+    }
+
+    /// <summary>
+    ///     Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to
+    ///     the same command sets and this method will clear all of them.
+    /// </summary>
+    /// <param name="command"></param>
+    public void Clear (params Command [] command)
+    {
+        KeyValuePair<Key, KeyBinding> [] kvps = Bindings
+                                                .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
+                                                .ToArray ();
+
+        foreach (KeyValuePair<Key, KeyBinding> kvp in kvps)
+        {
+            Remove (kvp.Key);
+        }
+    }
+
+    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
+    /// <param name="key"></param>
+    /// <returns></returns>
+    public KeyBinding Get (Key key)
+    {
+        if (TryGet (key, out KeyBinding binding))
+        {
+            return binding;
+        }
+        throw new InvalidOperationException ($"Key {key} is not bound.");
+    }
+
+    /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
+    /// <param name="key"></param>
+    /// <param name="scope"></param>
+    /// <returns></returns>
+    public KeyBinding Get (Key key, KeyBindingScope scope)
+    {
+        if (TryGet (key, scope, out KeyBinding binding))
+        {
+            return binding;
+        }
+        throw new InvalidOperationException ($"Key {key}/{scope} is not bound.");
+    }
+
+    /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="key"/> if it exists.</summary>
+    /// <param name="key">The key to check.</param>
+    /// <returns>
+    ///     The array of <see cref="Command"/>s if <paramref name="key"/> is bound. An empty <see cref="Command"/> array
+    ///     if not.
+    /// </returns>
+    public Command [] GetCommands (Key key)
+    {
+        if (TryGet (key, out KeyBinding bindings))
+        {
+            return bindings.Commands;
+        }
+
+        return Array.Empty<Command> ();
+    }
+
+    /// <summary>Gets the Key used by a set of commands.</summary>
+    /// <remarks></remarks>
+    /// <param name="commands">The set of commands to search.</param>
+    /// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
+    /// <exception cref="InvalidOperationException">If no matching set of commands was found.</exception>
+    public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; }
+
+    /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
+    /// <param name="key"></param>
+    public void Remove (Key key)
+    {
+        Bindings.Remove (key);
+        Application.RemoveKeyBinding (key, BoundView);
+    }
+
+    /// <summary>Replaces a key combination already bound to a set of <see cref="Command"/>s.</summary>
+    /// <remarks></remarks>
+    /// <param name="oldKey">The key to be replaced.</param>
+    /// <param name="newKey">The new key to be used.</param>
+    public void Replace (Key oldKey, Key newKey)
+    {
+        if (!TryGet (oldKey, out KeyBinding _))
+        {
+            return;
+        }
+
+        KeyBinding value = Bindings [oldKey];
+        Remove (oldKey);
+        Add (newKey, value);
+    }
+
+    /// <summary>Gets the commands bound with the specified Key.</summary>
+    /// <remarks></remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="binding">
+    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
+    ///     found; otherwise, null. This parameter is passed uninitialized.
+    /// </param>
+    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
+    public bool TryGet (Key key, out KeyBinding binding)
+    {
+        if (key.IsValid)
+        {
+            return Bindings.TryGetValue (key, out binding);
+        }
+
+        binding = new (Array.Empty<Command> (), KeyBindingScope.Focused);
+
+        return false;
+    }
+
+    /// <summary>Gets the commands bound with the specified Key that are scoped to a particular scope.</summary>
+    /// <remarks></remarks>
+    /// <param name="key">The key to check.</param>
+    /// <param name="scope">the scope to filter on</param>
+    /// <param name="binding">
+    ///     When this method returns, contains the commands bound with the specified Key, if the Key is
+    ///     found; otherwise, null. This parameter is passed uninitialized.
+    /// </param>
+    /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
+    public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
+    {
+        if (key.IsValid && Bindings.TryGetValue (key, out binding))
+        {
+            if (scope.HasFlag (binding.Scope))
+            {
+                return true;
+            }
+        }
+
+        binding = new (Array.Empty<Command> (), KeyBindingScope.Focused);
+
+        return false;
+    }
+}

+ 2 - 1
Terminal.Gui/Resources/config.json

@@ -24,7 +24,8 @@
   "Themes": [
     {
       "Default": {
-        "Dialog.DefaultButtonAlignment": "Center",
+        "Dialog.DefaultButtonAlignment": "End",
+        "Dialog.DefaultButtonAlignmentModes": "AddSpaceBetweenItems",
         "FrameView.DefaultBorderStyle": "Single",
         "Window.DefaultBorderStyle": "Single",
         "ColorSchemes": [

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

@@ -136,5 +136,6 @@
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
     <EnableSourceLink>true</EnableSourceLink>
     <Authors>Miguel de Icaza, Tig Kindel (@tig), @BDisp</Authors>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 </Project>

+ 0 - 20
Terminal.Gui/Text/TextAlignment.cs

@@ -1,20 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>Text alignment enumeration, controls how text is displayed.</summary>
-public enum TextAlignment
-{
-    /// <summary>The text will be left-aligned.</summary>
-    Left,
-
-    /// <summary>The text will be right-aligned.</summary>
-    Right,
-
-    /// <summary>The text will be centered horizontally.</summary>
-    Centered,
-
-    /// <summary>
-    ///     The text will be justified (spaces will be added to existing spaces such that the text fills the container
-    ///     horizontally).
-    /// </summary>
-    Justified
-}

+ 52 - 49
Terminal.Gui/Text/TextFormatter.cs

@@ -1,3 +1,5 @@
+using System.Diagnostics;
+
 namespace Terminal.Gui;
 
 /// <summary>
@@ -15,14 +17,14 @@ public class TextFormatter
     private Size _size;
     private int _tabWidth = 4;
     private string _text;
-    private TextAlignment _textAlignment;
+    private Alignment _textAlignment = Alignment.Start;
     private TextDirection _textDirection;
-    private VerticalTextAlignment _textVerticalAlignment;
+    private Alignment _textVerticalAlignment = Alignment.Start;
     private bool _wordWrap = true;
 
-    /// <summary>Controls the horizontal text-alignment property.</summary>
+    /// <summary>Get or sets the horizontal text alignment.</summary>
     /// <value>The text alignment.</value>
-    public TextAlignment Alignment
+    public Alignment Alignment
     {
         get => _textAlignment;
         set => _textAlignment = EnableNeedsFormat (value);
@@ -32,8 +34,7 @@ public class TextFormatter
     /// <remarks>
     ///     <para>Used when <see cref="View"/> is using <see cref="Dim.Auto"/> to resize the view's <see cref="View.Viewport"/> to fit <see cref="Size"/>.</para>
     ///     <para>
-    ///         AutoSize is ignored if <see cref="TextAlignment.Justified"/> and
-    ///         <see cref="VerticalTextAlignment.Justified"/> are used.
+    ///         AutoSize is ignored if <see cref="Gui.Alignment.Fill"/> is used.
     ///     </para>
     /// </remarks>
     public bool AutoSize
@@ -68,9 +69,8 @@ public class TextFormatter
     ///     Only the first HotKey specifier found in <see cref="Text"/> is supported.
     /// </remarks>
     /// <param name="isWidth">
-    ///     If <see langword="true"/> (the default) the width required for the HotKey specifier is returned. Otherwise the
-    ///     height
-    ///     is returned.
+    ///     If <see langword="true"/> (the default) the width required for the HotKey specifier is returned. Otherwise, the
+    ///     height is returned.
     /// </param>
     /// <returns>
     ///     The number of characters required for the <see cref="TextFormatter.HotKeySpecifier"/>. If the text
@@ -97,8 +97,8 @@ public class TextFormatter
     /// </summary>
     public int CursorPosition { get; internal set; }
 
-    /// <summary>Controls the text-direction property.</summary>
-    /// <value>The text vertical alignment.</value>
+    /// <summary>Gets or sets the text-direction.</summary>
+    /// <value>The text direction.</value>
     public TextDirection Direction
     {
         get => _textDirection;
@@ -112,8 +112,7 @@ public class TextFormatter
             }
         }
     }
-
-
+    
     /// <summary>
     ///     Determines if the viewport width will be used or only the text width will be used,
     ///     If <see langword="true"/> all the viewport area will be filled with whitespaces and the same background color
@@ -223,9 +222,9 @@ public class TextFormatter
         }
     }
 
-    /// <summary>Controls the vertical text-alignment property.</summary>
+    /// <summary>Gets or sets the vertical text-alignment.</summary>
     /// <value>The text vertical alignment.</value>
-    public VerticalTextAlignment VerticalAlignment
+    public Alignment VerticalAlignment
     {
         get => _textVerticalAlignment;
         set => _textVerticalAlignment = EnableNeedsFormat (value);
@@ -318,10 +317,10 @@ public class TextFormatter
 
             // When text is justified, we lost left or right, so we use the direction to align. 
 
-            int x, y;
+            int x = 0, y = 0;
 
             // Horizontal Alignment
-            if (Alignment is TextAlignment.Right)
+            if (Alignment is Alignment.End)
             {
                 if (isVertical)
                 {
@@ -336,7 +335,7 @@ public class TextFormatter
                     CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
                 }
             }
-            else if (Alignment is TextAlignment.Left)
+            else if (Alignment is Alignment.Start)
             {
                 if (isVertical)
                 {
@@ -352,7 +351,7 @@ public class TextFormatter
 
                 CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
             }
-            else if (Alignment is TextAlignment.Justified)
+            else if (Alignment is Alignment.Fill)
             {
                 if (isVertical)
                 {
@@ -375,7 +374,7 @@ public class TextFormatter
 
                 CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0;
             }
-            else if (Alignment is TextAlignment.Centered)
+            else if (Alignment is Alignment.Center)
             {
                 if (isVertical)
                 {
@@ -395,11 +394,13 @@ public class TextFormatter
             }
             else
             {
-                throw new ArgumentOutOfRangeException ($"{nameof (Alignment)}");
+                Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
+
+                return;
             }
 
             // Vertical Alignment
-            if (VerticalAlignment is VerticalTextAlignment.Bottom)
+            if (VerticalAlignment is Alignment.End)
             {
                 if (isVertical)
                 {
@@ -410,7 +411,7 @@ public class TextFormatter
                     y = screen.Bottom - linesFormatted.Count + line;
                 }
             }
-            else if (VerticalAlignment is VerticalTextAlignment.Top)
+            else if (VerticalAlignment is Alignment.Start)
             {
                 if (isVertical)
                 {
@@ -421,7 +422,7 @@ public class TextFormatter
                     y = screen.Top + line;
                 }
             }
-            else if (VerticalAlignment is VerticalTextAlignment.Justified)
+            else if (VerticalAlignment is Alignment.Fill)
             {
                 if (isVertical)
                 {
@@ -435,7 +436,7 @@ public class TextFormatter
                         line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1;
                 }
             }
-            else if (VerticalAlignment is VerticalTextAlignment.Middle)
+            else if (VerticalAlignment is Alignment.Center)
             {
                 if (isVertical)
                 {
@@ -450,7 +451,9 @@ public class TextFormatter
             }
             else
             {
-                throw new ArgumentOutOfRangeException ($"{nameof (VerticalAlignment)}");
+               Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}");
+
+               return;
             }
 
             int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0;
@@ -471,8 +474,8 @@ public class TextFormatter
                 {
                     if (idx < 0
                         || (isVertical
-                                ? VerticalAlignment != VerticalTextAlignment.Bottom && current < 0
-                                : Alignment != TextAlignment.Right && x + current + colOffset < 0))
+                                ? VerticalAlignment != Alignment.End && current < 0
+                                : Alignment != Alignment.End && x + current + colOffset < 0))
                     {
                         current++;
 
@@ -561,7 +564,7 @@ public class TextFormatter
 
                 if (HotKeyPos > -1 && idx == HotKeyPos)
                 {
-                    if ((isVertical && VerticalAlignment == VerticalTextAlignment.Justified) || (!isVertical && Alignment == TextAlignment.Justified))
+                    if ((isVertical && VerticalAlignment == Alignment.Fill) || (!isVertical && Alignment == Alignment.Fill))
                     {
                         CursorPosition = idx - start;
                     }
@@ -699,7 +702,7 @@ public class TextFormatter
                 _lines = Format (
                                  text,
                                  Size.Height,
-                                 VerticalAlignment == VerticalTextAlignment.Justified,
+                                 VerticalAlignment == Alignment.Fill,
                                  Size.Width > colsWidth && WordWrap,
                                  PreserveTrailingSpaces,
                                  TabWidth,
@@ -723,7 +726,7 @@ public class TextFormatter
                 _lines = Format (
                                  text,
                                  Size.Width,
-                                 Alignment == TextAlignment.Justified,
+                                 Alignment == Alignment.Fill,
                                  Size.Height > 1 && WordWrap,
                                  PreserveTrailingSpaces,
                                  TabWidth,
@@ -977,7 +980,7 @@ public class TextFormatter
         // if value is not wide enough
         if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width)
         {
-            // pad it out with spaces to the given alignment
+            // pad it out with spaces to the given Alignment
             int toPad = width - text.EnumerateRunes ().Sum (c => c.GetColumns ());
 
             return text + new string (' ', toPad);
@@ -999,7 +1002,7 @@ public class TextFormatter
     /// <param name="textFormatter"><see cref="TextFormatter"/> instance to access any of his objects.</param>
     /// <returns>A list of word wrapped lines.</returns>
     /// <remarks>
-    ///     <para>This method does not do any justification.</para>
+    ///     <para>This method does not do any alignment.</para>
     ///     <para>This method strips Newline ('\n' and '\r\n') sequences before processing.</para>
     ///     <para>
     ///         If <paramref name="preserveTrailingSpaces"/> is <see langword="false"/> at most one space will be preserved
@@ -1031,7 +1034,7 @@ public class TextFormatter
         List<Rune> runes = StripCRLF (text).ToRuneList ();
 
         int start = Math.Max (
-                              !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom } && IsVerticalDirection (textDirection)
+                              !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: Alignment.End } && IsVerticalDirection (textDirection)
                                   ? runes.Count - width
                                   : 0,
                               0);
@@ -1249,7 +1252,7 @@ public class TextFormatter
     ///     The number of columns to clip the text to. Text longer than <paramref name="width"/> will be
     ///     clipped.
     /// </param>
-    /// <param name="talign">Alignment.</param>
+    /// <param name="textAlignment">Alignment.</param>
     /// <param name="textDirection">The text direction.</param>
     /// <param name="tabWidth">The number of columns used for a tab.</param>
     /// <param name="textFormatter"><see cref="TextFormatter"/> instance to access any of his objects.</param>
@@ -1257,13 +1260,13 @@ public class TextFormatter
     public static string ClipAndJustify (
         string text,
         int width,
-        TextAlignment talign,
+        Alignment textAlignment,
         TextDirection textDirection = TextDirection.LeftRight_TopBottom,
         int tabWidth = 0,
         TextFormatter textFormatter = null
     )
     {
-        return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection, tabWidth, textFormatter);
+        return ClipAndJustify (text, width, textAlignment == Alignment.Fill, textDirection, tabWidth, textFormatter);
     }
 
     /// <summary>Justifies text within a specified width.</summary>
@@ -1304,12 +1307,12 @@ public class TextFormatter
         {
             if (IsHorizontalDirection (textDirection))
             {
-                if (textFormatter is { Alignment: TextAlignment.Right })
+                if (textFormatter is { Alignment: Alignment.End })
                 {
                     return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
                 }
 
-                if (textFormatter is { Alignment: TextAlignment.Centered })
+                if (textFormatter is { Alignment: Alignment.Center })
                 {
                     return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection);
                 }
@@ -1319,12 +1322,12 @@ public class TextFormatter
 
             if (IsVerticalDirection (textDirection))
             {
-                if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom })
+                if (textFormatter is { VerticalAlignment: Alignment.End })
                 {
                     return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
                 }
 
-                if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle })
+                if (textFormatter is { VerticalAlignment: Alignment.Center })
                 {
                     return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection);
                 }
@@ -1342,14 +1345,14 @@ public class TextFormatter
 
         if (IsHorizontalDirection (textDirection))
         {
-            if (textFormatter is { Alignment: TextAlignment.Right })
+            if (textFormatter is { Alignment: Alignment.End })
             {
                 if (GetRuneWidth (text, tabWidth, textDirection) > width)
                 {
                     return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
                 }
             }
-            else if (textFormatter is { Alignment: TextAlignment.Centered })
+            else if (textFormatter is { Alignment: Alignment.Center })
             {
                 return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection);
             }
@@ -1361,14 +1364,14 @@ public class TextFormatter
 
         if (IsVerticalDirection (textDirection))
         {
-            if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom })
+            if (textFormatter is { VerticalAlignment: Alignment.End })
             {
                 if (runes.Count - zeroLength > width)
                 {
                     return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection);
                 }
             }
-            else if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle })
+            else if (textFormatter is { VerticalAlignment: Alignment.Center })
             {
                 return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection);
             }
@@ -1475,7 +1478,7 @@ public class TextFormatter
     /// <summary>Formats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.</summary>
     /// <param name="text"></param>
     /// <param name="width">The number of columns to constrain the text to for word wrapping and clipping.</param>
-    /// <param name="talign">Specifies how the text will be aligned horizontally.</param>
+    /// <param name="textAlignment">Specifies how the text will be aligned horizontally.</param>
     /// <param name="wordWrap">
     ///     If <see langword="true"/>, the text will be wrapped to new lines no longer than
     ///     <paramref name="width"/>. If <see langword="false"/>, forces text to fit a single line. Line breaks are converted
@@ -1498,7 +1501,7 @@ public class TextFormatter
     public static List<string> Format (
         string text,
         int width,
-        TextAlignment talign,
+        Alignment textAlignment,
         bool wordWrap,
         bool preserveTrailingSpaces = false,
         int tabWidth = 0,
@@ -1510,7 +1513,7 @@ public class TextFormatter
         return Format (
                        text,
                        width,
-                       talign == TextAlignment.Justified,
+                       textAlignment == Alignment.Fill,
                        wordWrap,
                        preserveTrailingSpaces,
                        tabWidth,
@@ -1884,7 +1887,7 @@ public class TextFormatter
         return lineIdx;
     }
 
-    /// <summary>Calculates the rectangle required to hold text, assuming no word wrapping or justification.</summary>
+    /// <summary>Calculates the rectangle required to hold text, assuming no word wrapping or alignment.</summary>
     /// <remarks>
     ///     This API will return incorrect results if the text includes glyphs who's width is dependent on surrounding
     ///     glyphs (e.g. Arabic).

+ 0 - 20
Terminal.Gui/Text/VerticalTextAlignment.cs

@@ -1,20 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>Vertical text alignment enumeration, controls how text is displayed.</summary>
-public enum VerticalTextAlignment
-{
-    /// <summary>The text will be top-aligned.</summary>
-    Top,
-
-    /// <summary>The text will be bottom-aligned.</summary>
-    Bottom,
-
-    /// <summary>The text will centered vertically.</summary>
-    Middle,
-
-    /// <summary>
-    ///     The text will be justified (spaces will be added to existing spaces such that the text fills the container
-    ///     vertically).
-    /// </summary>
-    Justified
-}

+ 30 - 9
Terminal.Gui/View/Adornment/Border.cs

@@ -196,6 +196,26 @@ public class Border : Adornment
         set => _lineStyle = value;
     }
 
+    private bool _showTitle = true;
+
+    /// <summary>
+    ///     Gets or sets whether the title should be shown. The default is <see langword="true"/>.
+    /// </summary>
+    public bool ShowTitle
+    {
+        get => _showTitle;
+        set
+        {
+            if (value == _showTitle)
+            {
+                return;
+            }
+            _showTitle = value;
+
+            Parent?.SetNeedsDisplay ();
+        }
+    }
+
     #region Mouse Support
 
     private Color? _savedForeColor;
@@ -358,7 +378,7 @@ public class Border : Adornment
         }
     }
 
-#endregion Mouse Support
+    #endregion Mouse Support
 
     /// <inheritdoc/>
     public override void OnDrawContent (Rectangle viewport)
@@ -394,12 +414,13 @@ public class Border : Adornment
                                                 Math.Min (screenBounds.Width - 4, borderBounds.Width - 4)
                                                )
                                      );
+
         Parent.TitleTextFormatter.Size = new (maxTitleWidth, 1);
 
         int sideLineLength = borderBounds.Height;
         bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 };
 
-        if (!string.IsNullOrEmpty (Parent?.Title))
+        if (ShowTitle)
         {
             if (Thickness.Top == 2)
             {
@@ -431,13 +452,13 @@ public class Border : Adornment
             }
         }
 
-        if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title))
+        if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && ShowTitle && !string.IsNullOrEmpty (Parent?.Title))
         {
-            var focus = Parent.GetNormalColor();
+            var focus = Parent.GetNormalColor ();
             if (Parent.SuperView is { } && Parent.SuperView?.Subviews!.Count (s => s.CanFocus) > 1)
             {
                 // Only use focus color if there are multiple focusable views
-                focus = Parent.GetFocusColor() ;
+                focus = Parent.GetFocusColor ();
             }
 
             Parent.TitleTextFormatter.Draw (
@@ -450,9 +471,9 @@ public class Border : Adornment
         {
             LineCanvas lc = Parent?.LineCanvas;
 
-            bool drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height > 1;
+            bool drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height >= 1;
             bool drawLeft = Thickness.Left > 0 && (Frame.Height > 1 || Thickness.Top == 0);
-            bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1;
+            bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1 && Frame.Height > 1;
             bool drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0);
 
             Attribute prevAttr = Driver.GetAttribute ();
@@ -470,7 +491,7 @@ public class Border : Adornment
             {
                 // ╔╡Title╞═════╗
                 // ╔╡╞═════╗
-                if (borderBounds.Width < 4 || string.IsNullOrEmpty (Parent?.Title))
+                if (borderBounds.Width < 4 || !ShowTitle || string.IsNullOrEmpty (Parent?.Title))
                 {
                     // ╔╡╞╗ should be ╔══╗
                     lc.AddLine (
@@ -620,7 +641,7 @@ public class Border : Adornment
                 }
 
                 // Redraw title 
-                if (drawTop && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title))
+                if (drawTop && maxTitleWidth > 0 && ShowTitle)
                 {
                     Parent.TitleTextFormatter.Draw (
                                                     new (borderBounds.X + 2, titleY, maxTitleWidth, 1),

+ 3 - 4
Terminal.Gui/View/Layout/Dim.cs

@@ -6,8 +6,7 @@ namespace Terminal.Gui;
 /// <summary>
 ///     <para>
 ///         A Dim object describes the dimensions of a <see cref="View"/>. Dim is the type of the
-///         <see cref="View.Width"/> and <see cref="View.Height"/> properties of <see cref="View"/>. Dim objects enable
-///         Computed Layout (see <see cref="LayoutStyle.Computed"/>) to automatically manage the dimensions of a view.
+///         <see cref="View.Width"/> and <see cref="View.Height"/> properties of <see cref="View"/>.
 ///     </para>
 ///     <para>
 ///         Integer values are implicitly convertible to an absolute <see cref="Dim"/>. These objects are created using
@@ -150,7 +149,7 @@ public abstract class Dim
     /// <summary>Creates a percentage <see cref="Dim"/> object that is a percentage of the width or height of the SuperView.</summary>
     /// <returns>The percent <see cref="Dim"/> object.</returns>
     /// <param name="percent">A value between 0 and 100 representing the percentage.</param>
-    /// <param name="mode"></param>
+    /// <param name="mode">the mode. Defaults to <see cref="DimPercentMode.ContentSize"/>.</param>
     /// <example>
     ///     This initializes a <see cref="TextField"/> that will be centered horizontally, is 50% of the way down, is 30% the
     ///     height,
@@ -187,7 +186,7 @@ public abstract class Dim
     ///     Gets a dimension that is anchored to a certain point in the layout.
     ///     This method is typically used internally by the layout system to determine the size of a View.
     /// </summary>
-    /// <param name="size">The width of the area where the View is being sized (Superview.ContentSize).</param>
+    /// <param name="size">The width of the area where the View is being sized (Superview.GetContentSize ()).</param>
     /// <returns>
     ///     An integer representing the calculated dimension. The way this dimension is calculated depends on the specific
     ///     subclass of Dim that is used. For example, DimAbsolute returns a fixed dimension, DimFactor returns a

+ 82 - 21
Terminal.Gui/View/Layout/DimAuto.cs

@@ -60,7 +60,8 @@ public class DimAuto () : Dim
         var subviewsSize = 0;
 
         int autoMin = MinimumContentDim?.GetAnchor (superviewContentSize) ?? 0;
-        
+        int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? int.MaxValue;
+
         if (Style.FastHasFlags (DimAutoStyle.Text))
         {
             textSize = int.Max (autoMin, dimension == Dimension.Width ? us.TextFormatter.Size.Width : us.TextFormatter.Size.Height);
@@ -68,24 +69,46 @@ public class DimAuto () : Dim
 
         if (Style.FastHasFlags (DimAutoStyle.Content))
         {
-            if (us._contentSize is { })
+            if (!us.ContentSizeTracksViewport)
             {
-                subviewsSize = dimension == Dimension.Width ? us.ContentSize.Width : us.ContentSize.Height;
+                // ContentSize was explicitly set. Ignore subviews.
+                subviewsSize = dimension == Dimension.Width ? us.GetContentSize ().Width : us.GetContentSize ().Height;
             }
             else
             {
+                // ContentSize was NOT explicitly set. Use subviews to determine size.
+
                 // TODO: This whole body of code is a WIP (for https://github.com/gui-cs/Terminal.Gui/pull/3451).
                 subviewsSize = 0;
 
+                List<View> includedSubviews = us.Subviews.ToList();//.Where (v => !v.ExcludeFromLayout).ToList ();
                 List<View> subviews;
 
+                #region Not Anchored and Are Not Dependent
+                // Start with subviews that are not anchored to the end, aligned, or dependent on content size
+                // [x] PosAnchorEnd
+                // [x] PosAlign
+                // [ ] PosCenter
+                // [ ] PosPercent
+                // [ ] PosView
+                // [ ] PosFunc
+                // [x] DimFill
+                // [ ] DimPercent
+                // [ ] DimFunc
+                // [ ] DimView
                 if (dimension == Dimension.Width)
                 {
-                    subviews = us.Subviews.Where (v => v.X is not PosAnchorEnd && v.Width is not DimFill).ToList ();
+                    subviews = includedSubviews.Where (v => v.X is not PosAnchorEnd
+                                                           && v.X is not PosAlign
+                                                           // && v.X is not PosCenter
+                                                           && v.Width is not DimFill).ToList ();
                 }
                 else
                 {
-                    subviews = us.Subviews.Where (v => v.Y is not PosAnchorEnd && v.Height is not DimFill).ToList ();
+                    subviews = includedSubviews.Where (v => v.Y is not PosAnchorEnd
+                                                           && v.Y is not PosAlign
+                                                           // && v.Y is not PosCenter
+                                                           && v.Height is not DimFill).ToList ();
                 }
 
                 for (var i = 0; i < subviews.Count; i++)
@@ -96,17 +119,22 @@ public class DimAuto () : Dim
 
                     if (size > subviewsSize)
                     {
+                        // BUGBUG: Should we break here? Or choose min/max?
                         subviewsSize = size;
                     }
                 }
+                #endregion Not Anchored and Are Not Dependent
 
+                #region Anchored
+                // Now, handle subviews that are anchored to the end
+                // [x] PosAnchorEnd
                 if (dimension == Dimension.Width)
                 {
-                    subviews = us.Subviews.Where (v => v.X is PosAnchorEnd).ToList ();
+                    subviews = includedSubviews.Where (v => v.X is PosAnchorEnd).ToList ();
                 }
                 else
                 {
-                    subviews = us.Subviews.Where (v => v.Y is PosAnchorEnd).ToList ();
+                    subviews = includedSubviews.Where (v => v.Y is PosAnchorEnd).ToList ();
                 }
 
                 int maxAnchorEnd = 0;
@@ -117,31 +145,64 @@ public class DimAuto () : Dim
                 }
 
                 subviewsSize += maxAnchorEnd;
-
-
+                #endregion Anchored
+
+                //#region Center
+                //// Now, handle subviews that are Centered
+                //if (dimension == Dimension.Width)
+                //{
+                //    subviews = us.Subviews.Where (v => v.X is PosCenter).ToList ();
+                //}
+                //else
+                //{
+                //    subviews = us.Subviews.Where (v => v.Y is PosCenter).ToList ();
+                //}
+
+                //int maxCenter = 0;
+                //for (var i = 0; i < subviews.Count; i++)
+                //{
+                //    View v = subviews [i];
+                //    maxCenter = dimension == Dimension.Width ? v.Frame.Width : v.Frame.Height;
+                //}
+
+                //subviewsSize += maxCenter;
+                //#endregion Center
+
+                #region Are Dependent
+                // Now, go back to those that are dependent on content size
+                // [x] DimFill
+                // [ ] DimPercent
                 if (dimension == Dimension.Width)
                 {
-                    subviews = us.Subviews.Where (v => v.Width is DimFill).ToList ();
+                    subviews = includedSubviews.Where (v => v.Width is DimFill
+                                                      // || v.X is PosCenter
+                                                     ).ToList ();
                 }
                 else
                 {
-                    subviews = us.Subviews.Where (v => v.Height is DimFill).ToList ();
+                    subviews = includedSubviews.Where (v => v.Height is DimFill
+                                                      //|| v.Y is PosCenter
+                                                     ).ToList ();
                 }
 
+                int maxFill = 0;
                 for (var i = 0; i < subviews.Count; i++)
                 {
                     View v = subviews [i];
 
                     if (dimension == Dimension.Width)
                     {
-                        v.SetRelativeLayout (new Size (autoMin - subviewsSize, 0));
+                        v.SetRelativeLayout (new Size (autoMax - subviewsSize, 0));
                     }
                     else
                     {
-                        v.SetRelativeLayout (new Size (0, autoMin - subviewsSize));
+                        v.SetRelativeLayout (new Size (0, autoMax - subviewsSize));
                     }
+                    maxFill = dimension == Dimension.Width ? v.Frame.Width : v.Frame.Height;
                 }
 
+                subviewsSize += maxFill;
+                #endregion Are Dependent
             }
         }
 
@@ -156,14 +217,14 @@ public class DimAuto () : Dim
         Thickness thickness = us.GetAdornmentsThickness ();
 
         max += dimension switch
-               {
-                   Dimension.Width => thickness.Horizontal,
-                   Dimension.Height => thickness.Vertical,
-                   Dimension.None => 0,
-                   _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null)
-               };
-
-        return int.Min (max, MaximumContentDim?.GetAnchor (superviewContentSize) ?? max);
+        {
+            Dimension.Width => thickness.Horizontal,
+            Dimension.Height => thickness.Vertical,
+            Dimension.None => 0,
+            _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null)
+        };
+
+        return int.Min (max, autoMax);
     }
 
     internal override bool ReferencesOtherViews ()

+ 7 - 9
Terminal.Gui/View/Layout/DimAutoStyle.cs

@@ -10,11 +10,9 @@ namespace Terminal.Gui;
 public enum DimAutoStyle
 {
     /// <summary>
-    ///     The dimensions will be computed based on the View's non-Text content.
+    ///     The dimensions will be computed based on the View's <see cref="View.GetContentSize ()"/> and/or <see cref="View.Subviews"/>.
     ///     <para>
-    ///         If <see cref="View.ContentSize"/> is explicitly set (is not <see langword="null"/>) then
-    ///         <see cref="View.ContentSize"/>
-    ///         will be used to determine the dimension.
+    ///         If <see cref="View.ContentSizeTracksViewport"/> is <see langword="true"/>, <see cref="View.GetContentSize ()"/> will be used to determine the dimension.
     ///     </para>
     ///     <para>
     ///         Otherwise, the Subview in <see cref="View.Subviews"/> with the largest corresponding position plus dimension
@@ -24,7 +22,7 @@ public enum DimAutoStyle
     ///         The corresponding dimension of the view's <see cref="View.Text"/> will be ignored.
     ///     </para>
     /// </summary>
-    Content = 0,
+    Content = 1,
 
     /// <summary>
     ///     <para>
@@ -33,14 +31,14 @@ public enum DimAutoStyle
     ///         will be used to determine the dimension.
     ///     </para>
     ///     <para>
-    ///         The corresponding dimensions of the <see cref="View.Subviews"/> will be ignored.
+    ///         The corresponding dimensions of <see cref="View.GetContentSize ()"/> and/or <see cref="View.Subviews"/> will be ignored.
     ///     </para>
     /// </summary>
-    Text = 1,
+    Text = 2,
 
     /// <summary>
-    ///     The dimension will be computed using both the view's <see cref="View.Text"/> and
-    ///     <see cref="View.Subviews"/> (whichever is larger).
+    ///     The dimension will be computed using the largest of the view's <see cref="View.Text"/>, <see cref="View.GetContentSize ()"/>, and
+    ///     <see cref="View.Subviews"/> corresponding dimension
     /// </summary>
     Auto = Content | Text,
 }

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

@@ -11,7 +11,7 @@ namespace Terminal.Gui;
 /// <param name="percent">The percentage.</param>
 /// <param name="mode">
 ///     If <see cref="DimPercentMode.Position"/> the dimension is computed using the View's position (<see cref="View.X"/> or
-///     <see cref="View.Y"/>); otherwise, the dimension is computed using the View's <see cref="View.ContentSize"/>.
+///     <see cref="View.Y"/>); otherwise, the dimension is computed using the View's <see cref="View.GetContentSize ()"/>.
 /// </param>
 public class DimPercent (int percent, DimPercentMode mode = DimPercentMode.ContentSize) : Dim
 {
@@ -32,7 +32,7 @@ public class DimPercent (int percent, DimPercentMode mode = DimPercentMode.Conte
     public override string ToString () { return $"Percent({Percent},{Mode})"; }
 
     /// <summary>
-    ///     Gets whether the dimension is computed using the View's position or ContentSize.
+    ///     Gets whether the dimension is computed using the View's position or GetContentSize ().
     /// </summary>
     public DimPercentMode Mode { get; } = mode;
 

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

@@ -15,7 +15,7 @@ public enum DimPercentMode
     Position = 0,
 
     /// <summary>
-    /// The dimension is computed using the View's <see cref="View.ContentSize"/>.
+    /// The dimension is computed using the View's <see cref="View.GetContentSize ()"/>.
     /// </summary>
     ContentSize = 1
 }

+ 0 - 38
Terminal.Gui/View/Layout/LayoutStyle.cs

@@ -1,38 +0,0 @@
-using Terminal.Gui.Analyzers.Internal.Attributes;
-
-namespace Terminal.Gui;
-
-/// <summary>
-///     <para>Indicates the LayoutStyle for the <see cref="View"/>.</para>
-///     <para>
-///         If Absolute, the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and
-///         <see cref="View.Height"/> objects are all absolute values and are not relative. The position and size of the
-///         view is described by <see cref="View.Frame"/>.
-///     </para>
-///     <para>
-///         If Computed, one or more of the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, or
-///         <see cref="View.Height"/> objects are relative to the <see cref="View.SuperView"/> and are computed at layout
-///         time.
-///     </para>
-/// </summary>
-[GenerateEnumExtensionMethods]
-public enum LayoutStyle
-{
-    /// <summary>
-    ///     Indicates the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and
-    ///     <see cref="View.Height"/> objects are all absolute values and are not relative. The position and size of the view
-    ///     is described by <see cref="View.Frame"/>.
-    /// </summary>
-    Absolute,
-
-    /// <summary>
-    ///     Indicates one or more of the <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, or
-    ///     <see cref="View.Height"/>
-    ///     objects are relative to the <see cref="View.SuperView"/> and are computed at layout time.  The position and size of
-    ///     the
-    ///     view
-    ///     will be computed based on these objects at layout time. <see cref="View.Frame"/> will provide the absolute computed
-    ///     values.
-    /// </summary>
-    Computed
-}

+ 35 - 4
Terminal.Gui/View/Layout/Pos.cs

@@ -26,6 +26,14 @@ namespace Terminal.Gui;
 ///             </listheader>
 ///             <item>
 ///                 <term>
+///                     <see cref="Pos.Align"/>
+///                 </term>
+///                 <description>
+///                     Creates a <see cref="Pos"/> object that aligns a set of views.
+///                 </description>
+///             </item>
+///             <item>
+///                 <term>
 ///                     <see cref="Func"/>
 ///                 </term>
 ///                 <description>
@@ -132,6 +140,30 @@ public abstract class Pos
     /// <param name="position">The value to convert to the <see cref="Pos"/>.</param>
     public static Pos Absolute (int position) { return new PosAbsolute (position); }
 
+    /// <summary>
+    ///     Creates a <see cref="Pos"/> object that aligns a set of views according to the specified <see cref="Alignment"/>
+    ///     and <see cref="AlignmentModes"/>.
+    /// </summary>
+    /// <param name="alignment">The alignment. The default includes <see cref="AlignmentModes.AddSpaceBetweenItems"/>.</param>
+    /// <param name="modes">The optional alignment modes.</param>
+    /// <param name="groupId">
+    ///     The optional identifier of a set of views that should be aligned together. When only a single
+    ///     set of views in a SuperView is aligned, this parameter is optional.
+    /// </param>
+    /// <returns></returns>
+    public static Pos Align (Alignment alignment, AlignmentModes modes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems, int groupId = 0)
+    {
+        return new PosAlign
+        {
+            Aligner = new ()
+            {
+                Alignment = alignment,
+                AlignmentModes = modes
+            },
+            GroupId = groupId
+        };
+    }
+
     /// <summary>
     ///     Creates a <see cref="Pos"/> object that is anchored to the end (right side or
     ///     bottom) of the SuperView's Content Area, minus the respective size of the View. This is equivalent to using
@@ -264,12 +296,12 @@ public abstract class Pos
 
     /// <summary>
     ///     Gets the starting point of an element based on the size of the parent element (typically
-    ///     <c>Superview.ContentSize</c>).
+    ///     <c>Superview.GetContentSize ()</c>).
     ///     This method is meant to be overridden by subclasses to provide different ways of calculating the starting point.
     ///     This method is used
     ///     internally by the layout system to determine where a View should be positioned.
     /// </summary>
-    /// <param name="size">The size of the parent element (typically <c>Superview.ContentSize</c>).</param>
+    /// <param name="size">The size of the parent element (typically <c>Superview.GetContentSize ()</c>).</param>
     /// <returns>
     ///     An integer representing the calculated position. The way this position is calculated depends on the specific
     ///     subclass of Pos that is used. For example, PosAbsolute returns a fixed position, PosAnchorEnd returns a
@@ -359,5 +391,4 @@ public abstract class Pos
     }
 
     #endregion operators
-
-}
+}

+ 222 - 0
Terminal.Gui/View/Layout/PosAlign.cs

@@ -0,0 +1,222 @@
+#nullable enable
+
+using System.ComponentModel;
+using System.Drawing;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Enables alignment of a set of views.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Updating the properties of <see cref="Aligner"/> is supported, but will not automatically cause re-layout to
+///         happen. <see cref="View.LayoutSubviews"/>
+///         must be called on the SuperView.
+///     </para>
+///     <para>
+///         Views that should be aligned together must have a distinct <see cref="GroupId"/>. When only a single
+///         set of views is aligned within a SuperView, setting <see cref="GroupId"/> is optional because it defaults to 0.
+///     </para>
+///     <para>
+///         The first view added to the Superview with a given <see cref="GroupId"/> is used to determine the alignment of
+///         the group.
+///         The alignment is applied to all views with the same <see cref="GroupId"/>.
+///     </para>
+/// </remarks>
+public class PosAlign : Pos
+{
+    /// <summary>
+    ///     The cached location. Used to store the calculated location to minimize recalculating it.
+    /// </summary>
+    private int? _cachedLocation;
+
+    /// <summary>
+    ///     Gets the identifier of a set of views that should be aligned together. When only a single
+    ///     set of views in a SuperView is aligned, setting <see cref="GroupId"/> is not needed because it defaults to 0.
+    /// </summary>
+    public int GroupId { get; init; }
+
+    private readonly Aligner? _aligner;
+
+    /// <summary>
+    ///     Gets the alignment settings.
+    /// </summary>
+    public required Aligner Aligner
+    {
+        get => _aligner!;
+        init
+        {
+            if (_aligner is { })
+            {
+                _aligner.PropertyChanged -= Aligner_PropertyChanged;
+            }
+
+            _aligner = value;
+            _aligner.PropertyChanged += Aligner_PropertyChanged;
+        }
+    }
+
+    /// <summary>
+    ///     Aligns the views in <paramref name="views"/> that have the same group ID as <paramref name="groupId"/>.
+    ///     Updates each view's cached _location.
+    /// </summary>
+    /// <param name="groupId"></param>
+    /// <param name="views"></param>
+    /// <param name="dimension"></param>
+    /// <param name="size"></param>
+    private static void AlignAndUpdateGroup (int groupId, IList<View> views, Dimension dimension, int size)
+    {
+        List<int> dimensionsList = new ();
+
+        // PERF: If this proves a perf issue, consider caching a ref to this list in each item
+        List<View> viewsInGroup = views.Where (
+                                               v =>
+                                               {
+                                                   return dimension switch
+                                                   {
+                                                       Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
+                                                       Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
+                                                       _ => false
+                                                   };
+                                               })
+                                       .ToList ();
+
+        if (viewsInGroup.Count == 0)
+        {
+            return;
+        }
+
+        // PERF: We iterate over viewsInGroup multiple times here.
+
+        Aligner? firstInGroup = null;
+
+        // Update the dimensionList with the sizes of the views
+        for (var index = 0; index < viewsInGroup.Count; index++)
+        {
+            View view = viewsInGroup [index];
+            PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
+
+            if (posAlign is { })
+            {
+                if (index == 0)
+                {
+                    firstInGroup = posAlign.Aligner;
+                }
+
+                dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
+            }
+        }
+
+        // Update the first item in the group with the new container size.
+        firstInGroup!.ContainerSize = size;
+
+        // Align
+        int [] locations = firstInGroup.Align (dimensionsList.ToArray ());
+
+        // Update the cached location for each item
+        for (var index = 0; index < viewsInGroup.Count; index++)
+        {
+            View view = viewsInGroup [index];
+            PosAlign? align = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
+
+            if (align is { })
+            {
+                align._cachedLocation = locations [index];
+            }
+        }
+    }
+
+    private void Aligner_PropertyChanged (object? sender, PropertyChangedEventArgs e) { _cachedLocation = null; }
+
+    /// <inheritdoc/>
+    public override bool Equals (object? other)
+    {
+        return other is PosAlign align
+               && GroupId == align.GroupId
+               && align.Aligner.Alignment == Aligner.Alignment
+               && align.Aligner.AlignmentModes == Aligner.AlignmentModes;
+    }
+
+    /// <inheritdoc/>
+    public override int GetHashCode () { return HashCode.Combine (Aligner, GroupId); }
+
+    /// <inheritdoc/>
+    public override string ToString () { return $"Align(alignment={Aligner.Alignment},modes={Aligner.AlignmentModes},groupId={GroupId})"; }
+
+    internal override int GetAnchor (int width) { return _cachedLocation ?? 0 - width; }
+
+    internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension)
+    {
+        if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension)
+        {
+            return _cachedLocation.Value;
+        }
+
+        if (us?.SuperView is null)
+        {
+            return 0;
+        }
+
+        AlignAndUpdateGroup (GroupId, us.SuperView.Subviews, dimension, superviewDimension);
+
+        if (_cachedLocation.HasValue)
+        {
+            return _cachedLocation.Value;
+        }
+
+        return 0;
+    }
+
+    internal int CalculateMinDimension (int groupId, IList<View> views, Dimension dimension)
+    {
+        List<int> dimensionsList = new ();
+
+        // PERF: If this proves a perf issue, consider caching a ref to this list in each item
+        List<View> viewsInGroup = views.Where (
+                                               v =>
+                                               {
+                                                   return dimension switch
+                                                          {
+                                                              Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
+                                                              Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
+                                                              _ => false
+                                                          };
+                                               })
+                                       .ToList ();
+
+        if (viewsInGroup.Count == 0)
+        {
+            return 0;
+        }
+
+        // PERF: We iterate over viewsInGroup multiple times here.
+
+        Aligner? firstInGroup = null;
+
+        // Update the dimensionList with the sizes of the views
+        for (var index = 0; index < viewsInGroup.Count; index++)
+        {
+            View view = viewsInGroup [index];
+
+            PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
+
+            if (posAlign is { })
+            {
+                if (index == 0)
+                {
+                    firstInGroup = posAlign.Aligner;
+                }
+
+                dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
+            }
+        }
+
+        // Align
+        var aligner = firstInGroup;
+        aligner.ContainerSize = dimensionsList.Sum();
+        int [] locations = aligner.Align (dimensionsList.ToArray ());
+
+        return locations.Sum ();
+    }
+}

+ 20 - 83
Terminal.Gui/View/Layout/ViewLayout.cs

@@ -1,6 +1,5 @@
 #nullable enable
 using System.Diagnostics;
-using Microsoft.CodeAnalysis;
 
 namespace Terminal.Gui;
 
@@ -13,14 +12,13 @@ public partial class View
     /// <summary>Gets or sets the absolute location and dimension of the view.</summary>
     /// <value>
     ///     The rectangle describing absolute location and dimension of the view, in coordinates relative to the
-    ///     <see cref="SuperView"/>'s Content, which is bound by <see cref="ContentSize"/>.
+    ///     <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>.
     /// </value>
     /// <remarks>
-    ///     <para>Frame is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="ContentSize"/>.</para>
+    ///     <para>Frame is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>.</para>
     ///     <para>
     ///         Setting Frame will set <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/> to the
     ///         values of the corresponding properties of the <paramref name="value"/> parameter.
-    ///         This causes <see cref="LayoutStyle"/> to be <see cref="LayoutStyle.Absolute"/>.
     ///     </para>
     ///     <para>
     ///         Altering the Frame will eventually (when the view hierarchy is next laid out via  see
@@ -41,8 +39,7 @@ public partial class View
 
             SetFrame (value with { Width = Math.Max (value.Width, 0), Height = Math.Max (value.Height, 0) });
 
-            // If Frame gets set, by definition, the View is now LayoutStyle.Absolute, so
-            // set all Pos/Dim to Absolute values.
+            // If Frame gets set, set all Pos/Dim to Absolute values.
             _x = _frame.X;
             _y = _frame.Y;
             _width = _frame.Width;
@@ -136,7 +133,7 @@ public partial class View
     /// <value>The <see cref="Pos"/> object representing the X position.</value>
     /// <remarks>
     ///     <para>
-    ///         The position is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="ContentSize"/>.
+    ///         The position is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>.
     ///     </para>
     ///     <para>
     ///         If set to a relative value (e.g. <see cref="Pos.Center"/>) the value is indeterminate until the view has been
@@ -148,8 +145,7 @@ public partial class View
     ///         <see cref="LayoutSubview(View, Size)"/> and <see cref="OnDrawContent(Rectangle)"/> methods to be called.
     ///     </para>
     ///     <para>
-    ///         Changing this property will cause <see cref="Frame"/> to be updated. If the new value is not of type
-    ///         <see cref="PosAbsolute"/> the <see cref="LayoutStyle"/> will change to <see cref="LayoutStyle.Computed"/>.
+    ///         Changing this property will cause <see cref="Frame"/> to be updated.
     ///     </para>
     ///     <para>The default value is <c>Pos.At (0)</c>.</para>
     /// </remarks>
@@ -175,7 +171,7 @@ public partial class View
     /// <value>The <see cref="Pos"/> object representing the Y position.</value>
     /// <remarks>
     ///     <para>
-    ///         The position is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="ContentSize"/>.
+    ///         The position is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>.
     ///     </para>
     ///     <para>
     ///         If set to a relative value (e.g. <see cref="Pos.Center"/>) the value is indeterminate until the view has been
@@ -187,8 +183,7 @@ public partial class View
     ///         <see cref="LayoutSubview(View, Size)"/> and <see cref="OnDrawContent(Rectangle)"/> methods to be called.
     ///     </para>
     ///     <para>
-    ///         Changing this property will cause <see cref="Frame"/> to be updated. If the new value is not of type
-    ///         <see cref="PosAbsolute"/> the <see cref="LayoutStyle"/> will change to <see cref="LayoutStyle.Computed"/>.
+    ///         Changing this property will cause <see cref="Frame"/> to be updated.
     ///     </para>
     ///     <para>The default value is <c>Pos.At (0)</c>.</para>
     /// </remarks>
@@ -213,7 +208,7 @@ public partial class View
     /// <value>The <see cref="Dim"/> object representing the height of the view (the number of rows).</value>
     /// <remarks>
     ///     <para>
-    ///         The dimension is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="ContentSize"/>
+    ///         The dimension is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>
     ///         .
     ///     </para>
     ///     <para>
@@ -226,8 +221,7 @@ public partial class View
     ///         <see cref="LayoutSubview(View, Size)"/> and <see cref="OnDrawContent(Rectangle)"/> methods to be called.
     ///     </para>
     ///     <para>
-    ///         Changing this property will cause <see cref="Frame"/> to be updated. If the new value is not of type
-    ///         <see cref="DimAbsolute"/> the <see cref="LayoutStyle"/> will change to <see cref="LayoutStyle.Computed"/>.
+    ///         Changing this property will cause <see cref="Frame"/> to be updated.
     ///     </para>
     ///     <para>The default value is <c>Dim.Sized (0)</c>.</para>
     /// </remarks>
@@ -259,7 +253,7 @@ public partial class View
     /// <value>The <see cref="Dim"/> object representing the width of the view (the number of columns).</value>
     /// <remarks>
     ///     <para>
-    ///         The dimension is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="ContentSize"/>
+    ///         The dimension is relative to the <see cref="SuperView"/>'s Content, which is bound by <see cref="GetContentSize ()"/>
     ///         .
     ///     </para>
     ///     <para>
@@ -272,8 +266,7 @@ public partial class View
     ///         <see cref="LayoutSubview(View, Size)"/> and <see cref="OnDrawContent(Rectangle)"/> methods to be called.
     ///     </para>
     ///     <para>
-    ///         Changing this property will cause <see cref="Frame"/> to be updated. If the new value is not of type
-    ///         <see cref="DimAbsolute"/> the <see cref="LayoutStyle"/> will change to <see cref="LayoutStyle.Computed"/>.
+    ///         Changing this property will cause <see cref="Frame"/> to be updated.
     ///     </para>
     ///     <para>The default value is <c>Dim.Sized (0)</c>.</para>
     /// </remarks>
@@ -303,55 +296,6 @@ public partial class View
 
     #region Layout Engine
 
-
-    // @tig Notes on layout flow. Ignore for now.
-    // BeginLayout
-    //   If !LayoutNeeded return
-    //   If !SizeNeeded return
-    //   Call OnLayoutStarted
-    //      Views and subviews can update things
-    //   
-
-
-    // EndLayout
-
-    /// <summary>
-    ///     Controls how the View's <see cref="Frame"/> is computed during <see cref="LayoutSubviews"/>. If the style is
-    ///     set to <see cref="LayoutStyle.Absolute"/>, LayoutSubviews does not change the <see cref="Frame"/>. If the style is
-    ///     <see cref="LayoutStyle.Computed"/> the <see cref="Frame"/> is updated using the <see cref="X"/>, <see cref="Y"/>,
-    ///     <see cref="Width"/>, and <see cref="Height"/> properties.
-    /// </summary>
-    /// <remarks>
-    ///     <para>
-    ///         Setting this property to <see cref="LayoutStyle.Absolute"/> will cause <see cref="Frame"/> to determine the
-    ///         size and position of the view. <see cref="X"/> and <see cref="Y"/> will be set to <see cref="DimAbsolute"/>
-    ///         using <see cref="Frame"/>.
-    ///     </para>
-    ///     <para>
-    ///         Setting this property to <see cref="LayoutStyle.Computed"/> will cause the view to use the
-    ///         <see cref="LayoutSubviews"/> method to size and position of the view. If either of the <see cref="X"/> and
-    ///         <see cref="Y"/> properties are `null` they will be set to <see cref="PosAbsolute"/> using the current value
-    ///         of <see cref="Frame"/>. If either of the <see cref="Width"/> and <see cref="Height"/> properties are `null`
-    ///         they will be set to <see cref="DimAbsolute"/> using <see cref="Frame"/>.
-    ///     </para>
-    /// </remarks>
-    /// <value>The layout style.</value>
-    public LayoutStyle LayoutStyle
-    {
-        get
-        {
-            if (_x is PosAbsolute
-                && _y is PosAbsolute
-                && _width is DimAbsolute
-                && _height is DimAbsolute)
-            {
-                return LayoutStyle.Absolute;
-            }
-
-            return LayoutStyle.Computed;
-        }
-    }
-
     #endregion Layout Engine
 
     /// <summary>
@@ -626,7 +570,7 @@ public partial class View
 
         CheckDimAuto ();
 
-        var contentSize = ContentSize;
+        var contentSize = GetContentSize ();
         OnLayoutStarted (new (contentSize));
 
         LayoutAdornments ();
@@ -650,7 +594,7 @@ public partial class View
         {
             foreach ((View from, View to) in edges)
             {
-                LayoutSubview (to, from.ContentSize);
+                LayoutSubview (to, from.GetContentSize ());
             }
         }
 
@@ -703,8 +647,8 @@ public partial class View
         // Determine our container's ContentSize - 
         //  First try SuperView.Viewport, then Application.Top, then Driver.Viewport.
         //  Finally, if none of those are valid, use int.MaxValue (for Unit tests).
-        Size superViewContentSize = SuperView is { IsInitialized: true } ? SuperView.ContentSize :
-                           Application.Top is { } && Application.Top != this && Application.Top.IsInitialized ? Application.Top.ContentSize :
+        Size superViewContentSize = SuperView is { IsInitialized: true } ? SuperView.GetContentSize () :
+                           Application.Top is { } && Application.Top != this && Application.Top.IsInitialized ? Application.Top.GetContentSize () :
                            Application.Driver?.Screen.Size ?? new (int.MaxValue, int.MaxValue);
 
         SetTextFormatterSize ();
@@ -746,7 +690,7 @@ public partial class View
 
     /// <summary>
     ///     Adjusts <see cref="Frame"/> given the SuperView's ContentSize (nominally the same as
-    ///     <c>this.SuperView.ContentSize</c>)
+    ///     <c>this.SuperView.GetContentSize ()</c>)
     ///     and the position (<see cref="X"/>, <see cref="Y"/>) and dimension (<see cref="Width"/>, and
     ///     <see cref="Height"/>).
     /// </summary>
@@ -758,7 +702,7 @@ public partial class View
     ///     </para>
     /// </remarks>
     /// <param name="superviewContentSize">
-    ///     The size of the SuperView's content (nominally the same as <c>this.SuperView.ContentSize</c>).
+    ///     The size of the SuperView's content (nominally the same as <c>this.SuperView.GetContentSize ()</c>).
     /// </param>
     internal void SetRelativeLayout (Size superviewContentSize)
     {
@@ -796,8 +740,7 @@ public partial class View
 
         if (Frame != newFrame)
         {
-            // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height, making
-            // the view LayoutStyle.Absolute.
+            // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height
             SetFrame (newFrame);
 
             if (_x is PosAbsolute)
@@ -835,12 +778,6 @@ public partial class View
         foreach (View? v in from.InternalSubviews)
         {
             nNodes.Add (v);
-
-            if (v.LayoutStyle != LayoutStyle.Computed)
-            {
-                continue;
-            }
-
             CollectPos (v.X, v, ref nNodes, ref nEdges);
             CollectPos (v.Y, v, ref nNodes, ref nEdges);
             CollectDim (v.Width, v, ref nNodes, ref nEdges);
@@ -1049,13 +986,13 @@ public partial class View
         // Verify none of the subviews are using Dim objects that depend on the SuperView's dimensions.
         foreach (View view in Subviews)
         {
-            if (widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Content) && _contentSize is null)
+            if (widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Content) && ContentSizeTracksViewport)
             {
                 ThrowInvalid (view, view.Width, nameof (view.Width));
                 ThrowInvalid (view, view.X, nameof (view.X));
             }
 
-            if (heightAuto is { } && heightAuto.Style.FastHasFlags (DimAutoStyle.Content) && _contentSize is null)
+            if (heightAuto is { } && heightAuto.Style.FastHasFlags (DimAutoStyle.Content) && ContentSizeTracksViewport)
             {
                 ThrowInvalid (view, view.Height, nameof (view.Height));
                 ThrowInvalid (view, view.Y, nameof (view.Y));

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

@@ -43,13 +43,6 @@ namespace Terminal.Gui;
 ///         more subviews, can respond to user input and render themselves on the screen.
 ///     </para>
 ///     <para>
-///         View supports two layout styles: <see cref="LayoutStyle.Absolute"/> or <see cref="LayoutStyle.Computed"/>.
-///         The style is determined by the values of <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and
-///         <see cref="Height"/>. If any of these is set to non-absolute <see cref="Pos"/> or <see cref="Dim"/> object,
-///         then the layout style is <see cref="LayoutStyle.Computed"/>. Otherwise it is <see cref="LayoutStyle.Absolute"/>
-///         .
-///     </para>
-///     <para>
 ///         To create a View using Absolute layout, call a constructor that takes a Rect parameter to specify the
 ///         absolute position and size or simply set <see cref="View.Frame "/>). To create a View using Computed layout use
 ///         a constructor that does not take a Rect parameter and set the X, Y, Width and Height properties on the view to
@@ -79,9 +72,7 @@ namespace Terminal.Gui;
 ///         To flag the entire view for redraw call <see cref="SetNeedsDisplay()"/>.
 ///     </para>
 ///     <para>
-///         The <see cref="LayoutSubviews"/> method is invoked when the size or layout of a view has changed. The default
-///         processing system will keep the size and dimensions for views that use the <see cref="LayoutStyle.Absolute"/>,
-///         and will recompute the Adornments for the views that use <see cref="LayoutStyle.Computed"/>.
+///         The <see cref="LayoutSubviews"/> method is invoked when the size or layout of a view has changed.
 ///     </para>
 ///     <para>
 ///         Views have a <see cref="ColorScheme"/> property that defines the default colors that subviews should use for
@@ -127,38 +118,25 @@ public partial class View : Responder, ISupportInitializeNotification
     /// <remarks>
     ///     <para>
     ///         Use <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/> properties to dynamically
-    ///         control the size and location of the view. The <see cref="View"/> will be created using
-    ///         <see cref="LayoutStyle.Absolute"/> coordinates. The initial size ( <see cref="View.Frame"/>) will be adjusted
-    ///         to fit the contents of <see cref="Text"/>, including newlines ('\n') for multiple lines.
-    ///     </para>
-    ///     <para>If <see cref="Height"/> is greater than one, word wrapping is provided.</para>
-    ///     <para>
-    ///         This constructor initialize a View with a <see cref="LayoutStyle"/> of <see cref="LayoutStyle.Absolute"/>.
-    ///         Use <see cref="X"/>, <see cref="Y"/>, <see cref="Width"/>, and <see cref="Height"/> properties to dynamically
-    ///         control the size and location of the view, changing it to <see cref="LayoutStyle.Computed"/>.
+    ///         control the size and location of the view.
     ///     </para>
     /// </remarks>
     public View ()
     {
-        CreateAdornments ();
-
-        HotKeySpecifier = (Rune)'_';
-        TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged;
-
-        TextDirection = TextDirection.LeftRight_TopBottom;
-        Text = string.Empty;
+        SetupAdornments ();
+        SetupKeyboard ();
+        //SetupMouse ();
+        SetupText ();
 
         CanFocus = false;
         TabIndex = -1;
         TabStop = false;
-
-        AddCommands ();
     }
 
     /// <summary>
     ///     Event called only once when the <see cref="View"/> is being initialized for the first time. Allows
-    ///     configurations and assignments to be performed before the <see cref="View"/> being shown. This derived from
-    ///     <see cref="ISupportInitializeNotification"/> to allow notify all the views that are being initialized.
+    ///     configurations and assignments to be performed before the <see cref="View"/> being shown.
+    ///     View implements <see cref="ISupportInitializeNotification"/> to allow for more sophisticated initialization.
     /// </summary>
     public event EventHandler Initialized;
 
@@ -525,6 +503,7 @@ public partial class View : Responder, ISupportInitializeNotification
     {
         LineCanvas.Dispose ();
 
+        DisposeKeyboard ();
         DisposeAdornments ();
 
         for (int i = InternalSubviews.Count - 1; i >= 0; i--)

+ 4 - 1
Terminal.Gui/View/ViewAdornments.cs

@@ -2,7 +2,10 @@
 
 public partial class View
 {
-    private void CreateAdornments ()
+    /// <summary>
+    ///    Initializes the Adornments of the View. Called by the constructor.
+    /// </summary>
+    private void SetupAdornments ()
     {
         //// TODO: Move this to Adornment as a static factory method
         if (this is not Adornment)

+ 111 - 52
Terminal.Gui/View/ViewContent.cs

@@ -1,6 +1,4 @@
-using System.Diagnostics;
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 public partial class View
 {
@@ -13,33 +11,31 @@ public partial class View
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         By default, the content size is set to <see langword="null"/>.
-    ///     </para>
-    /// </remarks>
-    /// <param name="contentSize">
-    ///     <para>
-    ///         If <see langword="null"/>, and the View has no visible subviews, <see cref="ContentSize"/> will track the size of <see cref="Viewport"/>.
+    ///         Negative sizes are not supported.
     ///     </para>
     ///     <para>
-    ///         If <see langword="null"/>, and the View has visible subviews, <see cref="ContentSize"/> will track the maximum position plus size of any
-    ///         visible Subviews
-    ///         and <c>Viewport.Location</c>  will track the minimum position and size of any visible Subviews.
+    ///         If not explicitly set, and the View has no visible subviews, <see cref="GetContentSize ()"/> will return the
+    ///         size of
+    ///         <see cref="Viewport"/>.
     ///     </para>
     ///     <para>
-    ///         If not <see langword="null"/>, <see cref="ContentSize"/> is set to the passed value and <see cref="Viewport"/> describes the portion of the content currently visible
-    ///         to the user. This enables virtual scrolling.
+    ///         If not explicitly set, and the View has visible subviews, <see cref="GetContentSize ()"/> will return the
+    ///         maximum
+    ///         position + dimension of the Subviews, supporting <see cref="Dim.Auto"/> with the
+    ///         <see cref="DimAutoStyle.Content"/> flag set.
     ///     </para>
     ///     <para>
-    ///         If not <see langword="null"/>, <see cref="ContentSize"/> is set to the passed value and the behavior of <see cref="DimAutoStyle.Content"/> will be to use the ContentSize
-    ///         to determine the size of the view.
+    ///         If set <see cref="Viewport"/> describes the portion of the content currently visible to the user. This enables
+    ///         virtual scrolling.
     ///     </para>
     ///     <para>
-    ///         Negative sizes are not supported.
+    ///         If set the behavior of <see cref="DimAutoStyle.Content"/> will be to use the ContentSize to determine the size
+    ///         of the view.
     ///     </para>
-    /// </param>
+    /// </remarks>
     public void SetContentSize (Size? contentSize)
     {
-        if (ContentSize.Width < 0 || ContentSize.Height < 0)
+        if (contentSize is { } && (contentSize.Value.Width < 0 || contentSize.Value.Height < 0))
         {
             throw new ArgumentException (@"ContentSize cannot be negative.", nameof (contentSize));
         }
@@ -56,19 +52,86 @@ public partial class View
     /// <summary>
     ///     Gets the size of the View's content.
     /// </summary>
-    /// <remarks>
+    /// <remarks>a>
     ///     <para>
-    ///         Use <see cref="SetContentSize"/> to change to change the content size.
+    ///         If the content size was not explicitly set by <see cref="SetContentSize"/>, and the View has no visible subviews, <see cref="GetContentSize ()"/> will return the
+    ///         size of
+    ///         <see cref="Viewport"/>.
     ///     </para>
     ///     <para>
-    ///         If the content size has not been explicitly set with <see cref="SetContentSize"/>, the value tracks
-    ///         <see cref="Viewport"/>.
+    ///         If the content size was not explicitly set by <see cref="SetContentSize"/>, and the View has visible subviews, <see cref="GetContentSize ()"/> will return the
+    ///         maximum
+    ///         position + dimension of the Subviews, supporting <see cref="Dim.Auto"/> with the
+    ///         <see cref="DimAutoStyle.Content"/> flag set.
+    ///     </para>
+    ///     <para>
+    ///         If set <see cref="Viewport"/> describes the portion of the content currently visible to the user. This enables
+    ///         virtual scrolling.
+    ///     </para>
+    ///     <para>
+    ///         If set the behavior of <see cref="DimAutoStyle.Content"/> will be to use the ContentSize to determine the size
+    ///         of the view.
     ///     </para>
     /// </remarks>
-    public Size ContentSize => _contentSize ?? Viewport.Size;
+    /// <returns>
+    ///     If the content size was not explicitly set by <see cref="SetContentSize"/>, <see cref="GetContentSize ()"/> will
+    ///     return the size of the <see cref="Viewport"/> and <see cref="ContentSizeTracksViewport"/> will be <see langword="true"/>.
+    /// </returns>
+    public Size GetContentSize () { return _contentSize ?? Viewport.Size; }
 
     /// <summary>
-    /// Called when <see cref="ContentSize"/> has changed.
+    ///     Gets or sets a value indicating whether the view's content size tracks the <see cref="Viewport"/>'s
+    ///     size or not.
+    /// </summary>
+    /// <remarks>
+    ///     <list type="bullet">
+    ///         <listheader>
+    ///             <term>Value</term> <description>Result</description>
+    ///         </listheader>
+    ///         <item>
+    ///             <term>
+    ///                 <see langword="true"/>
+    ///             </term>
+    ///             <description>
+    ///                 <para>
+    ///                     <see cref="GetContentSize ()"/> will return the <see cref="Viewport"/>'s size. Content scrolling
+    ///                     will be
+    ///                     disabled.
+    ///                 </para>
+    ///                 <para>
+    ///                     The behavior of <see cref="DimAutoStyle.Content"/> will be to use position and size of the Subviews
+    ///                     to
+    ///                     determine the size of the view, ignoring <see cref="GetContentSize ()"/>.
+    ///                 </para>
+    ///             </description>
+    ///         </item>
+    ///         <item>
+    ///             <term>
+    ///                 <see langword="false"/>
+    ///             </term>
+    ///             <description>
+    ///                 <para>
+    ///                     The return value of <see cref="GetContentSize ()"/> is independent of <see cref="Viewport"/> and <see cref="Viewport"/>
+    ///                     describes the portion of the content currently visible to the user enabling content scrolling.
+    ///                 </para>
+    ///                 <para>
+    ///                     The behavior of <see cref="DimAutoStyle.Content"/> will be to use <see cref="GetContentSize ()"/>
+    ///                     to
+    ///                     determine the
+    ///                     size of the view, ignoring the position and size of the Subviews.
+    ///                 </para>
+    ///             </description>
+    ///         </item>
+    ///     </list>
+    /// </remarks>
+    public bool ContentSizeTracksViewport
+    {
+        get => _contentSize is null;
+        set => _contentSize = value ? null : _contentSize;
+    }
+
+    /// <summary>
+    ///     Called when <see cref="GetContentSize ()"/> has changed.
     /// </summary>
     /// <param name="e"></param>
     /// <returns></returns>
@@ -79,6 +142,7 @@ public partial class View
         if (e.Cancel != true)
         {
             OnResizeNeeded ();
+
             //SetNeedsLayout ();
             //SetNeedsDisplay ();
         }
@@ -87,7 +151,7 @@ public partial class View
     }
 
     /// <summary>
-    ///     Event raised when the <see cref="ContentSize"/> changes.
+    ///     Event raised when the <see cref="GetContentSize ()"/> changes.
     /// </summary>
     public event EventHandler<SizeChangedEventArgs> ContentSizeChanged;
 
@@ -155,37 +219,35 @@ public partial class View
     /// <summary>
     ///     The location of the viewport into the view's content (0,0) is the top-left corner of the content. The Content
     ///     area's size
-    ///     is <see cref="ContentSize"/>.
+    ///     is <see cref="GetContentSize ()"/>.
     /// </summary>
     private Point _viewportLocation;
 
     /// <summary>
     ///     Gets or sets the rectangle describing the portion of the View's content that is visible to the user.
     ///     The viewport Location is relative to the top-left corner of the inner rectangle of <see cref="Padding"/>.
-    ///     If the viewport Size is the same as <see cref="ContentSize"/>, or <see cref="ContentSize"/> is
+    ///     If the viewport Size is the same as <see cref="GetContentSize ()"/>, or <see cref="GetContentSize ()"/> is
     ///     <see langword="null"/> the Location will be <c>0, 0</c>.
     /// </summary>
     /// <value>
     ///     The rectangle describing the location and size of the viewport into the View's virtual content, described by
-    ///     <see cref="ContentSize"/>.
+    ///     <see cref="GetContentSize ()"/>.
     /// </value>
     /// <remarks>
     ///     <para>
     ///         Positive values for the location indicate the visible area is offset into (down-and-right) the View's virtual
-    ///         <see cref="ContentSize"/>. This enables scrolling down and to the right (e.g. in a <see cref="ListView"/>.
+    ///         <see cref="GetContentSize ()"/>. This enables scrolling down and to the right (e.g. in a <see cref="ListView"/>
+    ///         .
     ///     </para>
     ///     <para>
     ///         Negative values for the location indicate the visible area is offset above (up-and-left) the View's virtual
-    ///         <see cref="ContentSize"/>. This enables scrolling up and to the left (e.g. in an image viewer that supports zoom
+    ///         <see cref="GetContentSize ()"/>. This enables scrolling up and to the left (e.g. in an image viewer that
+    ///         supports
+    ///         zoom
     ///         where the image stays centered).
     ///     </para>
     ///     <para>
-    ///         The <see cref="ViewportSettings"/> property controls how scrolling is handled. 
-    ///     </para>
-    ///     <para>
-    ///         If <see cref="LayoutStyle"/> is <see cref="LayoutStyle.Computed"/> the value of Viewport is indeterminate until
-    ///         the view has been initialized ( <see cref="IsInitialized"/> is true) and <see cref="LayoutSubviews"/> has been
-    ///         called.
+    ///         The <see cref="ViewportSettings"/> property controls how scrolling is handled.
     ///     </para>
     ///     <para>
     ///         Updates to the Viewport Size updates <see cref="Frame"/>, and has the same impact as updating the
@@ -207,6 +269,7 @@ public partial class View
             }
 
             Thickness thickness = GetAdornmentsThickness ();
+
             return new (
                         _viewportLocation,
                         new (
@@ -239,6 +302,7 @@ public partial class View
             }
 
             OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport));
+
             return;
         }
 
@@ -254,9 +318,9 @@ public partial class View
         {
             if (!ViewportSettings.HasFlag (ViewportSettings.AllowXGreaterThanContentWidth))
             {
-                if (newViewport.X >= ContentSize.Width)
+                if (newViewport.X >= GetContentSize ().Width)
                 {
-                    newViewport.X = ContentSize.Width - 1;
+                    newViewport.X = GetContentSize ().Width - 1;
                 }
             }
 
@@ -271,9 +335,9 @@ public partial class View
 
             if (!ViewportSettings.HasFlag (ViewportSettings.AllowYGreaterThanContentHeight))
             {
-                if (newViewport.Y >= ContentSize.Height)
+                if (newViewport.Y >= GetContentSize ().Height)
                 {
-                    newViewport.Y = ContentSize.Height - 1;
+                    newViewport.Y = GetContentSize ().Height - 1;
                 }
             }
 
@@ -289,7 +353,8 @@ public partial class View
     }
 
     /// <summary>
-    ///     Fired when the <see cref="Viewport"/> changes. This event is fired after the <see cref="Viewport"/> has been updated.
+    ///     Fired when the <see cref="Viewport"/> changes. This event is fired after the <see cref="Viewport"/> has been
+    ///     updated.
     /// </summary>
     [CanBeNull]
     public event EventHandler<DrawEventArgs> ViewportChanged;
@@ -298,10 +363,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) { ViewportChanged?.Invoke (this, e); }
 
     /// <summary>
     ///     Converts a <see cref="Viewport"/>-relative location and size to a screen-relative location and size.
@@ -311,10 +373,7 @@ public partial class View
     /// </remarks>
     /// <param name="viewport">Viewport-relative location and size.</param>
     /// <returns>Screen-relative location and size.</returns>
-    public Rectangle ViewportToScreen (in Rectangle viewport)
-    {
-        return viewport with { Location = ViewportToScreen (viewport.Location) };
-    }
+    public Rectangle ViewportToScreen (in Rectangle viewport) { return viewport with { Location = ViewportToScreen (viewport.Location) }; }
 
     /// <summary>
     ///     Converts a <see cref="Viewport"/>-relative location to a screen-relative location.
@@ -367,7 +426,7 @@ public partial class View
     /// <returns><see langword="true"/> if the <see cref="Viewport"/> was changed.</returns>
     public bool? ScrollVertical (int rows)
     {
-        if (ContentSize == Size.Empty || ContentSize == Viewport.Size)
+        if (GetContentSize () == Size.Empty || GetContentSize () == Viewport.Size)
         {
             return false;
         }
@@ -388,7 +447,7 @@ public partial class View
     /// <returns><see langword="true"/> if the <see cref="Viewport"/> was changed.</returns>
     public bool? ScrollHorizontal (int cols)
     {
-        if (ContentSize == Size.Empty || ContentSize == Viewport.Size)
+        if (GetContentSize () == Size.Empty || GetContentSize () == Viewport.Size)
         {
             return false;
         }

+ 19 - 9
Terminal.Gui/View/ViewDrawing.cs

@@ -106,7 +106,7 @@ public partial class View
 
         if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly))
         {
-            Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize));
+            Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ()));
             toClear = Rectangle.Intersect (toClear, visibleContent);
         }
 
@@ -172,7 +172,7 @@ public partial class View
         if (ViewportSettings.HasFlag (ViewportSettings.ClipContentOnly))
         {
             // Clamp the Clip to the just content area that is within the viewport
-            Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize));
+            Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ()));
             clip = Rectangle.Intersect (clip, visibleContent);
         }
 
@@ -236,6 +236,16 @@ public partial class View
 
         OnRenderLineCanvas ();
 
+        // TODO: This is a hack to force the border subviews to draw.
+        if (Border?.Subviews is { })
+        {
+            foreach (View view in Border.Subviews)
+            {
+                view.SetNeedsDisplay ();
+                view.Draw ();
+            }
+        }
+
         // Invoke DrawContentCompleteEvent
         OnDrawContentComplete (Viewport);
 
@@ -329,7 +339,7 @@ public partial class View
     public virtual Attribute GetFocusColor ()
     {
         ColorScheme cs = ColorScheme;
-        if (ColorScheme is null)
+        if (cs is null)
         {
             cs = new ();
         }
@@ -347,7 +357,7 @@ public partial class View
     {
         ColorScheme cs = ColorScheme;
 
-        if (ColorScheme is null)
+        if (cs is null)
         {
             cs = new ();
         }
@@ -365,7 +375,7 @@ public partial class View
     {
         ColorScheme cs = ColorScheme;
 
-        if (ColorScheme is null)
+        if (cs is null)
         {
             cs = new ();
         }
@@ -435,12 +445,12 @@ public partial class View
     ///     </para>
     ///     <para>
     ///         The <see cref="Viewport"/> Location and Size indicate what part of the View's content, defined
-    ///         by <see cref="ContentSize"/>, is visible and should be drawn. The coordinates taken by <see cref="Move"/> and
+    ///         by <see cref="GetContentSize ()"/>, is visible and should be drawn. The coordinates taken by <see cref="Move"/> and
     ///         <see cref="AddRune"/> are relative to <see cref="Viewport"/>, thus if <c>ViewPort.Location.Y</c> is <c>5</c>
     ///         the 6th row of the content should be drawn using <c>MoveTo (x, 5)</c>.
     ///     </para>
     ///     <para>
-    ///         If <see cref="ContentSize"/> is larger than <c>ViewPort.Size</c> drawing code should use <see cref="Viewport"/>
+    ///         If <see cref="GetContentSize ()"/> is larger than <c>ViewPort.Size</c> drawing code should use <see cref="Viewport"/>
     ///         to constrain drawing for better performance.
     ///     </para>
     ///     <para>
@@ -475,7 +485,7 @@ public partial class View
 
             // This should NOT clear 
             // TODO: If the output is not in the Viewport, do nothing
-            var drawRect = new Rectangle (ContentToScreen (Point.Empty), ContentSize);
+            var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ());
 
             TextFormatter?.Draw (
                                  drawRect,
@@ -584,7 +594,7 @@ public partial class View
     /// <remarks>
     ///     <para>
     ///         The location of <paramref name="region"/> is relative to the View's content, bound by <c>Size.Empty</c> and
-    ///         <see cref="ContentSize"/>.
+    ///         <see cref="GetContentSize ()"/>.
     ///     </para>
     ///     <para>
     ///         If the view has not been initialized (<see cref="IsInitialized"/> is <see langword="false"/>), the area to be

+ 75 - 30
Terminal.Gui/View/ViewKeyboard.cs

@@ -4,8 +4,15 @@ namespace Terminal.Gui;
 
 public partial class View
 {
-    private void AddCommands ()
+    /// <summary>
+    ///  Helper to configure all things keyboard related for a View. Called from the View constructor.
+    /// </summary>
+    private void SetupKeyboard ()
     {
+        KeyBindings = new (this);
+        HotKeySpecifier = (Rune)'_';
+        TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged;
+
         // By default, the HotKey command sets the focus
         AddCommand (Command.HotKey, OnHotKey);
 
@@ -13,6 +20,15 @@ public partial class View
         AddCommand (Command.Accept, OnAccept);
     }
 
+    /// <summary>
+    ///    Helper to dispose all things keyboard related for a View. Called from the View Dispose method.
+    /// </summary>
+    private void DisposeKeyboard ()
+    {
+        TitleTextFormatter.HotKeyChanged -= TitleTextFormatter_HotKeyChanged;
+        KeyBindings.Clear ();
+    }
+
     #region HotKey Support
 
     /// <summary>
@@ -113,9 +129,10 @@ public partial class View
     /// </remarks>
     /// <param name="prevHotKey">The HotKey <paramref name="hotKey"/> is replacing. Key bindings for this key will be removed.</param>
     /// <param name="hotKey">The new HotKey. If <see cref="Key.Empty"/> <paramref name="prevHotKey"/> bindings will be removed.</param>
+    /// <param name="context">Arbitrary context that can be associated with this key binding.</param>
     /// <returns><see langword="true"/> if the HotKey bindings were added.</returns>
     /// <exception cref="ArgumentException"></exception>
-    public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey)
+    public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, [CanBeNull] object context = null)
     {
         if (_hotKey == hotKey)
         {
@@ -178,15 +195,16 @@ public partial class View
         // Add the new 
         if (newKey != Key.Empty)
         {
+            KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context);
             // Add the base and Alt key
-            KeyBindings.Add (newKey, KeyBindingScope.HotKey, Command.HotKey);
-            KeyBindings.Add (newKey.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
+            KeyBindings.Add (newKey, keyBinding);
+            KeyBindings.Add (newKey.WithAlt, keyBinding);
 
             // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask
             if (newKey.IsKeyCodeAtoZ)
             {
-                KeyBindings.Add (newKey.WithShift, KeyBindingScope.HotKey, Command.HotKey);
-                KeyBindings.Add (newKey.WithShift.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
+                KeyBindings.Add (newKey.WithShift, keyBinding);
+                KeyBindings.Add (newKey.WithShift.WithAlt, keyBinding);
             }
         }
 
@@ -601,9 +619,9 @@ public partial class View
     #region Key Bindings
 
     /// <summary>Gets the key bindings for this view.</summary>
-    public KeyBindings KeyBindings { get; } = new ();
+    public KeyBindings KeyBindings { get; internal set; }
 
-    private Dictionary<Command, Func<bool?>> CommandImplementations { get; } = new ();
+    private Dictionary<Command, Func<CommandContext, bool?>> CommandImplementations { get; } = new ();
 
     /// <summary>
     ///     Low-level API called when a user presses a key; invokes any key bindings set on the view. This is called
@@ -646,17 +664,17 @@ public partial class View
             return true;
         }
 
-        if (Margin is {} && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled))
+        if (Margin is { } && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled))
         {
             return true;
         }
 
-        if (Padding is {} && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled))
+        if (Padding is { } && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled))
         {
             return true;
         }
 
-        if (Border is {} && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled))
+        if (Border is { } && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled))
         {
             return true;
         }
@@ -739,7 +757,7 @@ public partial class View
             }
 
             // each command has its own return value
-            bool? thisReturn = InvokeCommand (command);
+            bool? thisReturn = InvokeCommand (command, key, binding);
 
             // if we haven't got anything yet, the current command result should be used
             toReturn ??= thisReturn;
@@ -758,12 +776,13 @@ public partial class View
     ///     Invokes the specified commands.
     /// </summary>
     /// <param name="commands"></param>
+    /// <param name="key">The key that caused the commands to be invoked, if any.</param>
     /// <returns>
     ///     <see langword="null"/> if no command was found.
     ///     <see langword="true"/> if the command was invoked and it handled the command.
     ///     <see langword="false"/> if the command was invoked and it did not handle the command.
     /// </returns>
-    public bool? InvokeCommands (Command [] commands)
+    public bool? InvokeCommands (Command [] commands, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null)
     {
         bool? toReturn = null;
 
@@ -775,7 +794,7 @@ public partial class View
             }
 
             // each command has its own return value
-            bool? thisReturn = InvokeCommand (command);
+            bool? thisReturn = InvokeCommand (command, key, keyBinding);
 
             // if we haven't got anything yet, the current command result should be used
             toReturn ??= thisReturn;
@@ -791,42 +810,68 @@ public partial class View
     }
 
     /// <summary>Invokes the specified command.</summary>
-    /// <param name="command"></param>
+    /// <param name="command">The command to invoke.</param>
+    /// <param name="key">The key that caused the command to be invoked, if any.</param>
+    /// <param name="keyBinding"></param>
     /// <returns>
-    ///     <see langword="null"/> if no command was found. <see langword="true"/> if the command was invoked and it
-    ///     handled the command. <see langword="false"/> if the command was invoked and it did not handle the command.
+    ///     <see langword="null"/> if no command was found. <see langword="true"/> if the command was invoked, and it
+    ///     handled the command. <see langword="false"/> if the command was invoked, and it did not handle the command.
     /// </returns>
-    public bool? InvokeCommand (Command command)
+    public bool? InvokeCommand (Command command, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null)
     {
-        if (!CommandImplementations.ContainsKey (command))
+        if (CommandImplementations.TryGetValue (command, out Func<CommandContext, bool?> implementation))
         {
-            return null;
+            var context = new CommandContext (command, key, keyBinding); // Create the context here
+            return implementation (context);
         }
 
-        return CommandImplementations [command] ();
+        return null;
+    }
+
+    /// <summary>
+    ///     <para>
+    ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
+    ///        AddCommand for each command they support.
+    ///     </para>
+    ///     <para>
+    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
+    ///         replace the old one.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    /// <para>
+    ///     This version of AddCommand is for commands that require <see cref="CommandContext"/>. Use <see cref="AddCommand(Command,Func{System.Nullable{bool}})"/>
+    ///     in cases where the command does not require a <see cref="CommandContext"/>.
+    /// </para>
+    /// </remarks>
+    /// <param name="command">The command.</param>
+    /// <param name="f">The function.</param>
+    protected void AddCommand (Command command, Func<CommandContext, bool?> f)
+    {
+        CommandImplementations [command] = f;
     }
 
     /// <summary>
     ///     <para>
     ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
-    ///         <see cref="AddCommand"/> for each command they support.
+    ///        AddCommand for each command they support.
     ///     </para>
     ///     <para>
-    ///         If <see cref="AddCommand"/> has already been called for <paramref name="command"/> <paramref name="f"/> will
+    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
     ///         replace the old one.
     ///     </para>
     /// </summary>
+    /// <remarks>
+    /// <para>
+    ///     This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
+    ///     If the command requires context, use <see cref="AddCommand(Command,Func{CommandContext,System.Nullable{bool}})"/>
+    /// </para>
+    /// </remarks>
     /// <param name="command">The command.</param>
     /// <param name="f">The function.</param>
     protected void AddCommand (Command command, Func<bool?> f)
     {
-        // if there is already an implementation of this command
-        // replace that implementation
-        // else record how to perform the action (this should be the normal case)
-        if (CommandImplementations is { })
-        {
-            CommandImplementations [command] = f;
-        }
+        CommandImplementations [command] = ctx => f (); ;
     }
 
     /// <summary>Returns all commands that are supported by this <see cref="View"/>.</summary>

+ 4 - 0
Terminal.Gui/View/ViewMouse.cs

@@ -447,6 +447,10 @@ public partial class View
     internal bool SetHighlight (HighlightStyle style)
     {
         // TODO: Make the highlight colors configurable
+        if (!CanFocus)
+        {
+            return false;
+        }
 
         // Enable override via virtual method and/or event
         if (OnHighlight (style) == true)

+ 22 - 13
Terminal.Gui/View/ViewText.cs

@@ -4,6 +4,15 @@ namespace Terminal.Gui;
 
 public partial class View
 {
+    /// <summary>
+    ///    Initializes the Text of the View. Called by the constructor.
+    /// </summary>
+    private void SetupText ()
+    {
+        Text = string.Empty;
+        TextDirection = TextDirection.LeftRight_TopBottom;
+    }
+
     private string _text;
 
     /// <summary>
@@ -37,11 +46,11 @@ public partial class View
     ///         to <see cref="TextAlignment"/> and <see cref="TextDirection"/>.
     ///     </para>
     ///     <para>
-    ///         The text will word-wrap to additional lines if it does not fit horizontally. If <see cref="ContentSize"/>'s height
+    ///         The text will word-wrap to additional lines if it does not fit horizontally. If <see cref="GetContentSize ()"/>'s height
     ///         is 1, the text will be clipped.
     ///     </para>
     ///     <para>If <see cref="View.Width"/> or <see cref="View.Height"/> are using <see cref="DimAutoStyle.Text"/>,
-    ///     the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
+    ///     the <see cref="GetContentSize ()"/> will be adjusted to fit the text.</para>
     ///     <para>When the text changes, the <see cref="TextChanged"/> is fired.</para>
     /// </remarks>
     public virtual string Text
@@ -84,10 +93,10 @@ public partial class View
     ///     redisplay the <see cref="View"/>.
     /// </summary>
     /// <remarks>
-    ///     <para> <see cref="View.Width"/> or <see cref="View.Height"/> are using <see cref="DimAutoStyle.Text"/>, the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
+    ///     <para> <see cref="View.Width"/> or <see cref="View.Height"/> are using <see cref="DimAutoStyle.Text"/>, the <see cref="GetContentSize ()"/> will be adjusted to fit the text.</para>
     /// </remarks>
     /// <value>The text alignment.</value>
-    public virtual TextAlignment TextAlignment
+    public virtual Alignment TextAlignment
     {
         get => TextFormatter.Alignment;
         set
@@ -103,9 +112,9 @@ public partial class View
     ///     <see cref="View"/>.
     /// </summary>
     /// <remarks>
-    ///     <para> <see cref="View.Width"/> or <see cref="View.Height"/> are using <see cref="DimAutoStyle.Text"/>, the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
+    ///     <para> <see cref="View.Width"/> or <see cref="View.Height"/> are using <see cref="DimAutoStyle.Text"/>, the <see cref="GetContentSize ()"/> will be adjusted to fit the text.</para>
     /// </remarks>
-    /// <value>The text alignment.</value>
+    /// <value>The text direction.</value>
     public virtual TextDirection TextDirection
     {
         get => TextFormatter.Direction;
@@ -127,10 +136,10 @@ public partial class View
     ///     the <see cref="View"/>.
     /// </summary>
     /// <remarks>
-    ///     <para> <see cref="View.Width"/> or <see cref="View.Height"/> are using <see cref="DimAutoStyle.Text"/>, the <see cref="ContentSize"/> will be adjusted to fit the text.</para>
+    ///     <para> <see cref="View.Width"/> or <see cref="View.Height"/> are using <see cref="DimAutoStyle.Text"/>, the <see cref="GetContentSize ()"/> will be adjusted to fit the text.</para>
     /// </remarks>
-    /// <value>The text alignment.</value>
-    public virtual VerticalTextAlignment VerticalTextAlignment
+    /// <value>The vertical text alignment.</value>
+    public virtual Alignment VerticalTextAlignment
     {
         get => TextFormatter.VerticalAlignment;
         set
@@ -179,8 +188,8 @@ public partial class View
         // We need to ensure TextFormatter is accurate by calling it here.
         UpdateTextFormatterText ();
 
-        // Default is to use ContentSize.
-        var size = ContentSize;
+        // Default is to use GetContentSize ().
+        var size = GetContentSize ();
 
         // TODO: This is a hack. Figure out how to move this into DimDimAuto
         // Use _width & _height instead of Width & Height to avoid debug spew
@@ -193,12 +202,12 @@ public partial class View
 
             if (widthAuto is null || !widthAuto.Style.FastHasFlags (DimAutoStyle.Text))
             {
-                size.Width = ContentSize.Width;
+                size.Width = GetContentSize ().Width;
             }
 
             if (heightAuto is null || !heightAuto.Style.FastHasFlags (DimAutoStyle.Text))
             {
-                size.Height = ContentSize.Height;
+                size.Height = GetContentSize ().Height;
             }
         }
 

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

@@ -47,13 +47,13 @@ public enum ViewportSettings
     AllowNegativeLocation = AllowNegativeX | AllowNegativeY,
 
     /// <summary>
-    ///     If set, <see cref="View.Viewport"/><c>.X</c> can be set values greater than <see cref="View.ContentSize"/>
+    ///     If set, <see cref="View.Viewport"/><c>.X</c> can be set values greater than <see cref="View.GetContentSize ()"/>
     ///     <c>.Width</c> enabling scrolling beyond the right
     ///     of the content area.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         When not set, <see cref="View.Viewport"/><c>.X</c> is constrained to <see cref="View.ContentSize"/>
+    ///         When not set, <see cref="View.Viewport"/><c>.X</c> is constrained to <see cref="View.GetContentSize ()"/>
     ///         <c>.Width - 1</c>.
     ///         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.
@@ -65,13 +65,13 @@ public enum ViewportSettings
     AllowXGreaterThanContentWidth = 4,
 
     /// <summary>
-    ///     If set, <see cref="View.Viewport"/><c>.Y</c> can be set values greater than <see cref="View.ContentSize"/>
+    ///     If set, <see cref="View.Viewport"/><c>.Y</c> can be set values greater than <see cref="View.GetContentSize ()"/>
     ///     <c>.Height</c> enabling scrolling beyond the right
     ///     of the content area.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         When not set, <see cref="View.Viewport"/><c>.Y</c> is constrained to <see cref="View.ContentSize"/>
+    ///         When not set, <see cref="View.Viewport"/><c>.Y</c> is constrained to <see cref="View.GetContentSize ()"/>
     ///         <c>.Height - 1</c>.
     ///         This means the last row of the content will remain visible even if there is an attempt to scroll the Viewport
     ///         past the last row.
@@ -83,13 +83,13 @@ public enum ViewportSettings
     AllowYGreaterThanContentHeight = 8,
 
     /// <summary>
-    ///     If set, <see cref="View.Viewport"/><c>.Size</c> can be set values greater than <see cref="View.ContentSize"/>
+    ///     If set, <see cref="View.Viewport"/><c>.Size</c> can be set values greater than <see cref="View.GetContentSize ()"/>
     ///     enabling scrolling beyond the bottom-right
     ///     of the content area.
     /// </summary>
     /// <remarks>
     ///     <para>
-    ///         When not set, <see cref="View.Viewport"/> is constrained to <see cref="View.ContentSize"/><c> -1</c>.
+    ///         When not set, <see cref="View.Viewport"/> is constrained to <see cref="View.GetContentSize ()"/><c> -1</c>.
     ///         This means the last column and row of the content will remain visible even if there is an attempt to
     ///         scroll the Viewport past the last column or row.
     ///     </para>

+ 3 - 4
Terminal.Gui/Views/Button.cs

@@ -33,12 +33,11 @@ public class Button : View
     private readonly Rune _rightDefault;
     private bool _isDefault;
 
-    /// <summary>Initializes a new instance of <see cref="Button"/> using <see cref="LayoutStyle.Computed"/> layout.</summary>
-    /// <remarks>The width of the <see cref="Button"/> is computed based on the text length. The height will always be 1.</remarks>
+    /// <summary>Initializes a new instance of <see cref="Button"/>.</summary>
     public Button ()
     {
-        TextAlignment = TextAlignment.Centered;
-        VerticalTextAlignment = VerticalTextAlignment.Middle;
+        TextAlignment = Alignment.Center;
+        VerticalTextAlignment = Alignment.Center;
 
         _leftBracket = Glyphs.LeftBracket;
         _rightBracket = Glyphs.RightBracket;

+ 5 - 6
Terminal.Gui/Views/CheckBox.cs

@@ -11,8 +11,7 @@ public class CheckBox : View
     private bool? _checked = false;
 
     /// <summary>
-    ///     Initializes a new instance of <see cref="CheckBox"/> based on the given text, using
-    ///     <see cref="LayoutStyle.Computed"/> layout.
+    ///     Initializes a new instance of <see cref="CheckBox"/>.
     /// </summary>
     public CheckBox ()
     {
@@ -155,13 +154,13 @@ public class CheckBox : View
     {
         switch (TextAlignment)
         {
-            case TextAlignment.Left:
-            case TextAlignment.Centered:
-            case TextAlignment.Justified:
+            case Alignment.Start:
+            case Alignment.Center:
+            case Alignment.Fill:
                 TextFormatter.Text = $"{GetCheckedState ()} {Text}";
 
                 break;
-            case TextAlignment.Right:
+            case Alignment.End:
                 TextFormatter.Text = $"{Text} {GetCheckedState ()}";
 
                 break;

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

@@ -39,7 +39,7 @@ public class ColorPicker : View
 
         Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
         Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
-        SetContentSize(new (_boxWidth * _cols, _boxHeight * _rows));
+        SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
 
         MouseClick += ColorPicker_MouseClick;
     }
@@ -178,9 +178,9 @@ public class ColorPicker : View
         Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ());
         var colorIndex = 0;
 
-        for (var y = 0; y < Math.Max(2, viewport.Height / BoxHeight); y++)
+        for (var y = 0; y < Math.Max (2, viewport.Height / BoxHeight); y++)
         {
-            for (var x = 0; x < Math.Max(8, viewport.Width / BoxWidth); x++)
+            for (var x = 0; x < Math.Max (8, viewport.Width / BoxWidth); x++)
             {
                 int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols;
                 Driver.SetAttribute (new Attribute ((ColorName)foregroundColorIndex, (ColorName)colorIndex));

+ 37 - 34
Terminal.Gui/Views/ComboBox.cs

@@ -5,7 +5,7 @@
 //   Ross Ferguson ([email protected])
 //
 
-using System.Collections;
+using System.Collections.ObjectModel;
 using System.ComponentModel;
 
 namespace Terminal.Gui;
@@ -16,7 +16,7 @@ public class ComboBox : View
     private readonly ComboListView _listview;
     private readonly int _minimumHeight = 2;
     private readonly TextField _search;
-    private readonly IList _searchset = new List<object> ();
+    private readonly ObservableCollection<object> _searchSet = [];
     private bool _autoHide = true;
     private bool _hideDropdownListOnClick;
     private int _lastSelectedItem = -1;
@@ -47,9 +47,9 @@ public class ComboBox : View
 
         _listview.SelectedItemChanged += (sender, e) =>
                                          {
-                                             if (!HideDropdownListOnClick && _searchset.Count > 0)
+                                             if (!HideDropdownListOnClick && _searchSet.Count > 0)
                                              {
-                                                 SetValue (_searchset [_listview.SelectedItem]);
+                                                 SetValue (_searchSet [_listview.SelectedItem]);
                                              }
                                          };
 
@@ -174,7 +174,7 @@ public class ComboBox : View
 
     /// <summary>Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ComboBox"/>, enabling custom rendering.</summary>
     /// <value>The source.</value>
-    /// <remarks>Use <see cref="SetSource"/> to set a new <see cref="IList"/> source.</remarks>
+    /// <remarks>Use <see cref="SetSource{T}"/> to set a new <see cref="ObservableCollection{T}"/> source.</remarks>
     public IListDataSource Source
     {
         get => _source;
@@ -366,13 +366,13 @@ public class ComboBox : View
     /// <summary>This event is raised when the selected item in the <see cref="ComboBox"/> has changed.</summary>
     public event EventHandler<ListViewItemEventArgs> SelectedItemChanged;
 
-    /// <summary>Sets the source of the <see cref="ComboBox"/> to an <see cref="IList"/>.</summary>
-    /// <value>An object implementing the IList interface.</value>
+    /// <summary>Sets the source of the <see cref="ComboBox"/> to an <see cref="ObservableCollection{T}"/>.</summary>
+    /// <value>An object implementing the INotifyCollectionChanged and INotifyPropertyChanged interface.</value>
     /// <remarks>
-    ///     Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custome
+    ///     Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom
     ///     rendering.
     /// </remarks>
-    public void SetSource (IList source)
+    public void SetSource<T> (ObservableCollection<T> source)
     {
         if (source is null)
         {
@@ -380,7 +380,7 @@ public class ComboBox : View
         }
         else
         {
-            _listview.SetSource (source);
+            _listview.SetSource<T> (source);
             Source = _listview.Source;
         }
     }
@@ -408,7 +408,7 @@ public class ComboBox : View
 
         return Math.Min (
                          Math.Max (Viewport.Height - 1, _minimumHeight - 1),
-                         _searchset?.Count > 0 ? _searchset.Count :
+                         _searchSet?.Count > 0 ? _searchSet.Count :
                          IsShow ? Math.Max (Viewport.Height - 1, _minimumHeight - 1) : 0
                         );
     }
@@ -468,9 +468,9 @@ public class ComboBox : View
             return -1;
         }
 
-        for (var i = 0; i < _searchset.Count; i++)
+        for (var i = 0; i < _searchSet.Count; i++)
         {
-            if (_searchset [i].ToString () == searchText)
+            if (_searchSet [i].ToString () == searchText)
             {
                 return i;
             }
@@ -504,14 +504,14 @@ public class ComboBox : View
         if (_search.HasFocus)
         {
             // jump to list
-            if (_searchset?.Count > 0)
+            if (_searchSet?.Count > 0)
             {
                 _listview.TabStop = true;
                 _listview.SetFocus ();
 
                 if (_listview.SelectedItem > -1)
                 {
-                    SetValue (_searchset [_listview.SelectedItem]);
+                    SetValue (_searchSet [_listview.SelectedItem]);
                 }
                 else
                 {
@@ -572,7 +572,7 @@ public class ComboBox : View
 
     private bool? MoveUpList ()
     {
-        if (_listview.HasFocus && _listview.SelectedItem == 0 && _searchset?.Count > 0) // jump back to search
+        if (_listview.HasFocus && _listview.SelectedItem == 0 && _searchSet?.Count > 0) // jump back to search
         {
             _search.CursorPosition = _search.Text.GetRuneCount ();
             _search.SetFocus ();
@@ -619,8 +619,8 @@ public class ComboBox : View
         {
             _search.Width = _listview.Width = _autoHide ? Viewport.Width - 1 : Viewport.Width;
             _listview.Height = CalculatetHeight ();
-            _search.SetRelativeLayout (ContentSize);
-            _listview.SetRelativeLayout (ContentSize);
+            _search.SetRelativeLayout (GetContentSize ());
+            _listview.SetRelativeLayout (GetContentSize ());
         }
     }
 
@@ -634,7 +634,7 @@ public class ComboBox : View
 
         ResetSearchSet ();
 
-        _listview.SetSource (_searchset);
+        _listview.SetSource (_searchSet);
         _listview.Height = CalculatetHeight ();
 
         if (Subviews.Count > 0 && HasFocus)
@@ -645,7 +645,7 @@ public class ComboBox : View
 
     private void ResetSearchSet (bool noCopy = false)
     {
-        _searchset.Clear ();
+        _searchSet.Clear ();
 
         if (_autoHide || noCopy)
         {
@@ -680,16 +680,19 @@ public class ComboBox : View
             IsShow = true;
             ResetSearchSet (true);
 
-            foreach (object item in _source.ToList ())
+            if (!string.IsNullOrEmpty (_search.Text))
             {
-                // Iterate to preserver object type and force deep copy
-                if (item.ToString ()
-                        .StartsWith (
-                                     _search.Text,
-                                     StringComparison.CurrentCultureIgnoreCase
-                                    ))
+                foreach (object item in _source.ToList ())
                 {
-                    _searchset.Add (item);
+                    // Iterate to preserver object type and force deep copy
+                    if (item.ToString ()
+                            .StartsWith (
+                                         _search.Text,
+                                         StringComparison.CurrentCultureIgnoreCase
+                                        ))
+                    {
+                        _searchSet.Add (item);
+                    }
                 }
             }
         }
@@ -710,7 +713,7 @@ public class ComboBox : View
         IsShow = false;
         _listview.TabStop = false;
 
-        if (_listview.Source.Count == 0 || (_searchset?.Count ?? 0) == 0)
+        if (_listview.Source.Count == 0 || (_searchSet?.Count ?? 0) == 0)
         {
             _text = "";
             HideList ();
@@ -719,7 +722,7 @@ public class ComboBox : View
             return;
         }
 
-        SetValue (_listview.SelectedItem > -1 ? _searchset [_listview.SelectedItem] : _text);
+        SetValue (_listview.SelectedItem > -1 ? _searchSet [_listview.SelectedItem] : _text);
         _search.CursorPosition = _search.Text.GetColumns ();
         Search_Changed (this, new StateEventArgs<string> (_search.Text, _search.Text));
         OnOpenSelectedItem ();
@@ -738,7 +741,7 @@ public class ComboBox : View
         // force deep copy
         foreach (object item in Source.ToList ())
         {
-            _searchset.Add (item);
+            _searchSet.Add (item);
         }
     }
 
@@ -762,7 +765,7 @@ public class ComboBox : View
     /// Consider making public
     private void ShowList ()
     {
-        _listview.SetSource (_searchset);
+        _listview.SetSource (_searchSet);
         _listview.Clear (); 
         _listview.Height = CalculatetHeight ();
         SuperView?.BringSubviewToFront (this);
@@ -784,9 +787,9 @@ public class ComboBox : View
         private bool _isFocusing;
         public ComboListView (ComboBox container, bool hideDropdownListOnClick) { SetInitialProperties (container, hideDropdownListOnClick); }
 
-        public ComboListView (ComboBox container, IList source, bool hideDropdownListOnClick)
+        public ComboListView (ComboBox container, ObservableCollection<string> source, bool hideDropdownListOnClick)
         {
-            Source = new ListWrapper (source);
+            Source = new ListWrapper<string> (source);
             SetInitialProperties (container, hideDropdownListOnClick);
         }
 

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

@@ -21,10 +21,10 @@ public class DateField : TextField
     private string _format;
     private string _separator;
 
-    /// <summary>Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Computed"/> layout.</summary>
+    /// <summary>Initializes a new instance of <see cref="DateField"/>.</summary>
     public DateField () : this (DateTime.MinValue) { }
 
-    /// <summary>Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Computed"/> layout.</summary>
+    /// <summary>Initializes a new instance of <see cref="DateField"/>.</summary>
     /// <param name="date"></param>
     public DateField (DateTime date)
     {

+ 2 - 4
Terminal.Gui/Views/DatePicker.cs

@@ -215,7 +215,6 @@ public class DatePicker : View
         {
             X = Pos.Center () - 2,
             Y = Pos.Bottom (_calendar) - 1,
-            Height = 1,
             Width = 2,
             Text = GetBackButtonText (),
             WantContinuousButtonPressed = true,
@@ -234,7 +233,6 @@ public class DatePicker : View
         {
             X = Pos.Right (_previousMonthButton) + 2,
             Y = Pos.Bottom (_calendar) - 1,
-            Height = 1,
             Width = 2,
             Text = GetForwardButtonText (),
             WantContinuousButtonPressed = true,
@@ -273,8 +271,8 @@ public class DatePicker : View
                                        Text = _date.ToString (Format);
                                    };
 
-        Height = Dim.Auto ();
-        Width = Dim.Auto ();
+        Width = Dim.Auto (DimAutoStyle.Content);
+        Height = Dim.Auto (DimAutoStyle.Content);
 
         // BUGBUG: Remove when Dim.Auto(subviews) fully works
         SetContentSize (new (_calendar.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 7, _calendar.Frame.Height + 1));

+ 51 - 176
Terminal.Gui/Views/Dialog.cs

@@ -9,27 +9,38 @@ namespace Terminal.Gui;
 /// </summary>
 /// <remarks>
 ///     To run the <see cref="Dialog"/> modally, create the <see cref="Dialog"/>, and pass it to
-///     <see cref="Application.Run(Toplevel, Func{Exception, bool}, ConsoleDriver)"/>. This will execute the dialog until it terminates via the
+///     <see cref="Application.Run(Toplevel, Func{Exception, bool}, ConsoleDriver)"/>. This will execute the dialog until
+///     it terminates via the
 ///     [ESC] or [CTRL-Q] key, or when one of the views or buttons added to the dialog calls
 ///     <see cref="Application.RequestStop"/>.
 /// </remarks>
 public class Dialog : Window
 {
-    /// <summary>Determines the horizontal alignment of the Dialog buttons.</summary>
-    public enum ButtonAlignments
-    {
-        /// <summary>Center-aligns the buttons (the default).</summary>
-        Center = 0,
+    /// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
+    /// <remarks>This property can be set in a Theme.</remarks>
+    [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+    [JsonConverter (typeof (JsonStringEnumConverter))]
+    public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End;
 
-        /// <summary>Justifies the buttons</summary>
-        Justify,
+    /// <summary>The default <see cref="Alignment"/> for <see cref="Dialog"/>.</summary>
+    /// <remarks>This property can be set in a Theme.</remarks>
+    [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+    [JsonConverter (typeof (JsonStringEnumConverter))]
+    public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems;
 
-        /// <summary>Left-aligns the buttons</summary>
-        Left,
+    /// <summary>
+    ///     Defines the default minimum Dialog width, as a percentage of the container width. Can be configured via
+    ///     <see cref="ConfigurationManager"/>.
+    /// </summary>
+    [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+    public static int DefaultMinimumWidth { get; set; } = 25;
 
-        /// <summary>Right-aligns the buttons</summary>
-        Right
-    }
+    /// <summary>
+    ///     Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via
+    ///     <see cref="ConfigurationManager"/>.
+    /// </summary>
+    [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+    public static int DefaultMinimumHeight { get; set; } = 25;
 
     // TODO: Reenable once border/borderframe design is settled
     /// <summary>
@@ -44,42 +55,37 @@ public class Dialog : Window
     private readonly List<Button> _buttons = new ();
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Dialog"/> class using <see cref="LayoutStyle.Computed"/>
-    ///     positioning with no <see cref="Button"/>s.
+    ///     Initializes a new instance of the <see cref="Dialog"/> class with no <see cref="Button"/>s.
     /// </summary>
     /// <remarks>
-    ///     By default, <see cref="View.X"/> and <see cref="View.Y"/> are set to <c>Pos.Center ()</c> and
-    ///     <see cref="View.Width"/> and <see cref="View.Height"/> are set to <c>Width = Dim.Percent (85)</c>, centering the
-    ///     Dialog vertically and horizontally.
+    ///     By default, <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and <see cref="View.Height"/> are
+    ///     set
+    ///     such that the <see cref="Dialog"/> will be centered in, and no larger than 90% of the screen dimensions.
     /// </remarks>
     public Dialog ()
     {
         Arrangement = ViewArrangement.Movable;
         X = Pos.Center ();
         Y = Pos.Center ();
-        //ValidatePosDim = true;
 
-        Width = Dim.Percent (85); 
-        Height = Dim.Percent (85);
+        Width = Dim.Auto (DimAutoStyle.Content, Dim.Percent (DefaultMinimumWidth), Dim.Percent (90));
+        Height = Dim.Auto (DimAutoStyle.Content, Dim.Percent (DefaultMinimumHeight), Dim.Percent (90));
         ColorScheme = Colors.ColorSchemes ["Dialog"];
 
         Modal = true;
         ButtonAlignment = DefaultButtonAlignment;
+        ButtonAlignmentModes = DefaultButtonAlignmentModes;
 
-        AddCommand (Command.QuitToplevel, () =>
-                                          {
-                                              Canceled = true;
-                                              RequestStop ();
-                                              return true;
-                                          });
-        KeyBindings.Add (Key.Esc, Command.QuitToplevel);
-
-        Initialized += Dialog_Initialized; ;
-    }
+        AddCommand (
+                    Command.QuitToplevel,
+                    () =>
+                    {
+                        Canceled = true;
+                        RequestStop ();
 
-    private void Dialog_Initialized (object sender, EventArgs e)
-    {
-        LayoutButtons ();
+                        return true;
+                    });
+        KeyBindings.Add (Key.Esc, Command.QuitToplevel);
     }
 
     private bool _canceled;
@@ -107,12 +113,17 @@ public class Dialog : Window
             }
 #endif
             _canceled = value;
-            return;
         }
     }
 
+    // TODO: Update button.X = Pos.Justify when alignment changes
     /// <summary>Determines how the <see cref="Dialog"/> <see cref="Button"/>s are aligned along the bottom of the dialog.</summary>
-    public ButtonAlignments ButtonAlignment { get; set; }
+    public Alignment ButtonAlignment { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the alignment modes for the dialog's buttons.
+    /// </summary>
+    public AlignmentModes ButtonAlignmentModes { get; set; }
 
     /// <summary>Optional buttons to lay out at the bottom of the dialog.</summary>
     public Button [] Buttons
@@ -132,12 +143,6 @@ public class Dialog : Window
         }
     }
 
-    /// <summary>The default <see cref="ButtonAlignments"/> for <see cref="Dialog"/>.</summary>
-    /// <remarks>This property can be set in a Theme.</remarks>
-    [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    [JsonConverter (typeof (JsonStringEnumConverter))]
-    public static ButtonAlignments DefaultButtonAlignment { get; set; } = ButtonAlignments.Center;
-
     /// <summary>
     ///     Adds a <see cref="Button"/> to the <see cref="Dialog"/>, its layout will be controlled by the
     ///     <see cref="Dialog"/>
@@ -150,6 +155,10 @@ public class Dialog : Window
             return;
         }
 
+        // Use a distinct GroupId so users can use Pos.Align for other views in the Dialog
+        button.X = Pos.Align (ButtonAlignment, ButtonAlignmentModes, GetHashCode ());
+        button.Y = Pos.AnchorEnd ();
+
         _buttons.Add (button);
         Add (button);
 
@@ -160,138 +169,4 @@ public class Dialog : Window
             LayoutSubviews ();
         }
     }
-
-    /// <inheritdoc/>
-    //public override void LayoutSubviews ()
-    //{
-    //    if (_inLayout)
-    //    {
-    //        return;
-    //    }
-
-    //    _inLayout = true;
-    //    SetRelativeLayout(SuperView?.ContentSize ?? Driver.Screen.Size);
-    //    LayoutButtons ();
-    //    base.LayoutSubviews ();
-    //    _inLayout = false;
-    //}
-
-    // Get the width of all buttons, not including any Margin.
-    internal int GetButtonsWidth ()
-    {
-        if (_buttons.Count == 0)
-        {
-            return 0;
-        }
-
-        //var widths = buttons.Select (b => b.TextFormatter.GetFormattedSize ().Width + b.BorderFrame.Thickness.Horizontal + b.Padding.Thickness.Horizontal);
-        IEnumerable<int> widths = _buttons.Select (b => b.Frame.Width);
-
-        return widths.Sum ();
-    }
-
-    private void LayoutButtons ()
-    {
-        if (_buttons.Count == 0 || !IsInitialized)
-        {
-            return;
-        }
-
-        var shiftLeft = 0;
-
-        int buttonsWidth = GetButtonsWidth ();
-
-        switch (ButtonAlignment)
-        {
-            case ButtonAlignments.Center:
-                // Center Buttons
-                shiftLeft = (Viewport.Width - buttonsWidth - _buttons.Count - 1) / 2 + 1;
-
-                for (int i = _buttons.Count - 1; i >= 0; i--)
-                {
-                    Button button = _buttons [i];
-                    shiftLeft += button.Frame.Width + (i == _buttons.Count - 1 ? 0 : 1);
-
-                    if (shiftLeft > -1)
-                    {
-                        button.X = Pos.AnchorEnd (shiftLeft);
-                    }
-                    else
-                    {
-                        button.X = Viewport.Width - shiftLeft;
-                    }
-
-                    button.Y = Pos.AnchorEnd ();
-                }
-
-                break;
-
-            case ButtonAlignments.Justify:
-                // Justify Buttons
-                // leftmost and rightmost buttons are hard against edges. The rest are evenly spaced.
-
-                var spacing = (int)Math.Ceiling ((double)(Viewport.Width - buttonsWidth) / (_buttons.Count - 1));
-
-                for (int i = _buttons.Count - 1; i >= 0; i--)
-                {
-                    Button button = _buttons [i];
-
-                    if (i == _buttons.Count - 1)
-                    {
-                        shiftLeft += button.Frame.Width;
-                        button.X = Pos.AnchorEnd (shiftLeft);
-                    }
-                    else
-                    {
-                        if (i == 0)
-                        {
-                            // first (leftmost) button 
-                            int left = Viewport.Width;
-                            button.X = Pos.AnchorEnd (left);
-                        }
-                        else
-                        {
-                            shiftLeft += button.Frame.Width + spacing;
-                            button.X = Pos.AnchorEnd (shiftLeft);
-                        }
-                    }
-
-                    button.Y = Pos.AnchorEnd ();
-                }
-
-                break;
-
-            case ButtonAlignments.Left:
-                // Left Align Buttons
-                Button prevButton = _buttons [0];
-                prevButton.X = 0;
-                prevButton.Y = Pos.AnchorEnd (1);
-
-                for (var i = 1; i < _buttons.Count; i++)
-                {
-                    Button button = _buttons [i];
-                    button.X = Pos.Right (prevButton) + 1;
-                    button.Y = Pos.AnchorEnd (1);
-                    prevButton = button;
-                }
-
-                break;
-
-            case ButtonAlignments.Right:
-                // Right align buttons
-                shiftLeft = _buttons [_buttons.Count - 1].Frame.Width;
-                _buttons [_buttons.Count - 1].X = Pos.AnchorEnd (shiftLeft);
-                _buttons [_buttons.Count - 1].Y = Pos.AnchorEnd (1);
-
-                for (int i = _buttons.Count - 2; i >= 0; i--)
-                {
-                    Button button = _buttons [i];
-                    shiftLeft += button.Frame.Width + 1;
-                    button.X = Pos.AnchorEnd (shiftLeft);
-                    button.Y = Pos.AnchorEnd ();
-                }
-
-                break;
-        }
-    }
 }

+ 2 - 3
Terminal.Gui/Views/FrameView.cs

@@ -3,13 +3,12 @@
 namespace Terminal.Gui;
 
 /// <summary>
-///     The FrameView is a container frame that draws a frame around the contents. It is similar to a GroupBox in
-///     Windows.
+///     The FrameView is a container View with a border around it. 
 /// </summary>
 public class FrameView : View
 {
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Gui.FrameView"/> class using <see cref="LayoutStyle.Computed"/>
+    ///     Initializes a new instance of the <see cref="Gui.FrameView"/> class.
     ///     layout.
     /// </summary>
     public FrameView ()

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

@@ -38,7 +38,7 @@ public class HexView : View
     private static readonly Rune SpaceCharRune = new (' ');
     private static readonly Rune PeriodCharRune = new ('.');
 
-    /// <summary>Initializes a <see cref="HexView"/> class using <see cref="LayoutStyle.Computed"/> layout.</summary>
+    /// <summary>Initializes a <see cref="HexView"/> class.</summary>
     /// <param name="source">
     ///     The <see cref="Stream"/> to view and edit as hex, this <see cref="Stream"/> must support seeking,
     ///     or an exception will be thrown.
@@ -98,7 +98,7 @@ public class HexView : View
         LayoutComplete += HexView_LayoutComplete;
     }
 
-    /// <summary>Initializes a <see cref="HexView"/> class using <see cref="LayoutStyle.Computed"/> layout.</summary>
+    /// <summary>Initializes a <see cref="HexView"/> class.</summary>
     public HexView () : this (new MemoryStream ()) { }
 
     /// <summary>

+ 117 - 32
Terminal.Gui/Views/ListView.cs

@@ -1,11 +1,18 @@
 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
+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; }
 
@@ -64,7 +71,7 @@ public interface IListDataSource
 ///     </para>
 ///     <para>
 ///         By default <see cref="ListView"/> uses <see cref="object.ToString"/> to render the items of any
-///         <see cref="IList"/> object (e.g. arrays, <see cref="List{T}"/>, and other collections). Alternatively, an
+///         <see cref="ObservableCollection{T}"/> object (e.g. arrays, <see cref="List{T}"/>, and other collections). Alternatively, an
 ///         object that implements <see cref="IListDataSource"/> can be provided giving full control of what is rendered.
 ///     </para>
 ///     <para>
@@ -258,12 +265,18 @@ public class ListView : View
         set
         {
             if (_source == value)
-
             {
                 return;
             }
+
+            _source?.Dispose ();
             _source = value;
 
+            if (_source is { })
+            {
+                _source.CollectionChanged += Source_CollectionChanged;
+            }
+
             SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
             if (IsInitialized)
             {
@@ -277,6 +290,20 @@ public class ListView : View
         }
     }
 
+    private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
+    {
+        SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
+
+        if (Source is { Count: > 0 } && _selected > Source.Count - 1)
+        {
+            SelectedItem = Source.Count - 1;
+        }
+
+        SetNeedsDisplay ();
+
+        OnCollectionChanged (e);
+    }
+
     /// <summary>Gets or sets the index of the item that will appear at the top of the <see cref="View.Viewport"/>.</summary>
     /// <remarks>
     /// This a helper property for accessing <c>listView.Viewport.Y</c>.
@@ -501,7 +528,12 @@ public class ListView : View
 
             if (Viewport.Y + _selected > Viewport.Height - 1)
             {
-                Viewport = Viewport with { Y = _selected };
+                Viewport = Viewport with
+                {
+                    Y = _selected < Viewport.Height - 1
+                            ? Math.Max (Viewport.Height - _selected + 1, 0)
+                            : Math.Max (_selected - Viewport.Height + 1, 0)
+                };
             }
 
             OnSelectedChanged ();
@@ -796,21 +828,26 @@ public class ListView : View
     /// <summary>This event is raised when the selected item in the <see cref="ListView"/> has changed.</summary>
     public event EventHandler<ListViewItemEventArgs> SelectedItemChanged;
 
+    /// <summary>
+    /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed.
+    /// </summary>
+    public event NotifyCollectionChangedEventHandler CollectionChanged;
+
     /// <summary>Sets the source of the <see cref="ListView"/> to an <see cref="IList"/>.</summary>
     /// <value>An object implementing the IList interface.</value>
     /// <remarks>
-    ///     Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custome
+    ///     Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom
     ///     rendering.
     /// </remarks>
-    public void SetSource (IList source)
+    public void SetSource<T> (ObservableCollection<T> source)
     {
-        if (source is null && (Source is null || !(Source is ListWrapper)))
+        if (source is null && Source is not ListWrapper<T>)
         {
             Source = null;
         }
         else
         {
-            Source = new ListWrapper (source);
+            Source = new ListWrapper<T> (source);
         }
     }
 
@@ -820,18 +857,18 @@ public class ListView : View
     ///     Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom
     ///     rendering.
     /// </remarks>
-    public Task SetSourceAsync (IList source)
+    public Task SetSourceAsync<T> (ObservableCollection<T> source)
     {
         return Task.Factory.StartNew (
                                       () =>
                                       {
-                                          if (source is null && (Source is null || !(Source is ListWrapper)))
+                                          if (source is null && (Source is null || !(Source is ListWrapper<T>)))
                                           {
                                               Source = null;
                                           }
                                           else
                                           {
-                                              Source = new ListWrapper (source);
+                                              Source = new ListWrapper<T> (source);
                                           }
 
                                           return source;
@@ -843,35 +880,74 @@ public class ListView : View
     }
 
     private void ListView_LayoutStarted (object sender, LayoutEventArgs e) { EnsureSelectedItemVisible (); }
+    /// <summary>
+    /// Call the event to raises the <see cref="CollectionChanged"/>.
+    /// </summary>
+    /// <param name="e"></param>
+    protected virtual void OnCollectionChanged (NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke (this, e); }
+
+    /// <inheritdoc />
+    protected override void Dispose (bool disposing)
+    {
+        _source?.Dispose ();
+
+        base.Dispose (disposing);
+    }
 }
 
 /// <summary>
 ///     Provides a default implementation of <see cref="IListDataSource"/> that renders <see cref="ListView"/> items
 ///     using <see cref="object.ToString()"/>.
 /// </summary>
-public class ListWrapper : IListDataSource
+public class ListWrapper<T> : IListDataSource, IDisposable
 {
-    private readonly int _count;
-    private readonly BitArray _marks;
-    private readonly IList _source;
+    private int _count;
+    private BitArray _marks;
+    private readonly ObservableCollection<T> _source;
 
     /// <inheritdoc/>
-    public ListWrapper (IList source)
+    public ListWrapper (ObservableCollection<T> source)
     {
         if (source is { })
         {
             _count = source.Count;
             _marks = new BitArray (_count);
             _source = source;
+            _source.CollectionChanged += Source_CollectionChanged;
             Length = GetMaxLengthItem ();
         }
     }
 
+    private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
+    {
+        CheckAndResizeMarksIfRequired ();
+        CollectionChanged?.Invoke (sender, e);
+    }
+
+    /// <inheritdoc />
+    public event NotifyCollectionChangedEventHandler CollectionChanged;
+
     /// <inheritdoc/>
-    public int Count => _source is { } ? _source.Count : 0;
+    public int Count => _source?.Count ?? 0;
 
     /// <inheritdoc/>
-    public int Length { get; }
+    public int Length { get; private set; }
+
+    void CheckAndResizeMarksIfRequired ()
+    {
+        if (_source != null && _count != _source.Count)
+        {
+            _count = _source.Count;
+            BitArray newMarks = new BitArray (_count);
+            for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++)
+            {
+                newMarks [i] = _marks [i];
+            }
+            _marks = newMarks;
+
+            Length = GetMaxLengthItem ();
+        }
+    }
 
     /// <inheritdoc/>
     public void Render (
@@ -886,25 +962,25 @@ public class ListWrapper : IListDataSource
     )
     {
         container.Move (Math.Max (col - start, 0), line);
-        object t = _source? [item];
 
-        if (t is null)
-        {
-            RenderUstr (driver, "", col, line, width);
-        }
-        else
+        if (_source is { })
         {
-            if (t is string u)
-            {
-                RenderUstr (driver, u, col, line, width, start);
-            }
-            else if (t is string s)
+            object t = _source [item];
+
+            if (t is null)
             {
-                RenderUstr (driver, s, col, line, width, start);
+                RenderUstr (driver, "", col, line, width);
             }
             else
             {
-                RenderUstr (driver, t.ToString (), col, line, width, start);
+                if (t is string s)
+                {
+                    RenderUstr (driver, s, col, line, width, start);
+                }
+                else
+                {
+                    RenderUstr (driver, t.ToString (), col, line, width, start);
+                }
             }
         }
     }
@@ -1002,7 +1078,7 @@ public class ListWrapper : IListDataSource
     private void RenderUstr (ConsoleDriver driver, string ustr, int col, int line, int width, int start = 0)
     {
         string str = start > ustr.GetColumns () ? string.Empty : ustr.Substring (Math.Min (start, ustr.ToRunes ().Length - 1));
-        string u = TextFormatter.ClipAndJustify (str, width, TextAlignment.Left);
+        string u = TextFormatter.ClipAndJustify (str, width, Alignment.Start);
         driver.AddStr (u);
         width -= u.GetColumns ();
 
@@ -1011,4 +1087,13 @@ public class ListWrapper : IListDataSource
             driver.AddRune ((Rune)' ');
         }
     }
+
+    /// <inheritdoc />
+    public void Dispose ()
+    {
+        if (_source is { })
+        {
+            _source.CollectionChanged -= Source_CollectionChanged;
+        }
+    }
 }

+ 44 - 474
Terminal.Gui/Views/Menu/Menu.cs

@@ -1,292 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary>Specifies how a <see cref="MenuItem"/> shows selection state.</summary>
-[Flags]
-public enum MenuItemCheckStyle
-{
-    /// <summary>The menu item will be shown normally, with no check indicator. The default.</summary>
-    NoCheck = 0b_0000_0000,
-
-    /// <summary>The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).</summary>
-    Checked = 0b_0000_0001,
-
-    /// <summary>The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.</summary>
-    Radio = 0b_0000_0010
-}
-
-/// <summary>
-///     A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. MenuItems
-///     can also have a checked indicator (see <see cref="Checked"/>).
-/// </summary>
-public class MenuItem
-{
-    private readonly ShortcutHelper _shortcutHelper;
-    private bool _allowNullChecked;
-    private MenuItemCheckStyle _checkType;
-
-    private string _title;
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
-    public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
-    /// <param name="title">Title for the menu item.</param>
-    /// <param name="help">Help text to display.</param>
-    /// <param name="action">Action to invoke when the menu item is activated.</param>
-    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
-    /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
-    /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
-    public MenuItem (
-        string title,
-        string help,
-        Action action,
-        Func<bool> canExecute = null,
-        MenuItem parent = null,
-        KeyCode shortcut = KeyCode.Null
-    )
-    {
-        Title = title ?? "";
-        Help = help ?? "";
-        Action = action;
-        CanExecute = canExecute;
-        Parent = parent;
-        _shortcutHelper = new ShortcutHelper ();
-
-        if (shortcut != KeyCode.Null)
-        {
-            Shortcut = shortcut;
-        }
-    }
-
-    /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
-    /// <value>Method to invoke.</value>
-    public Action Action { get; set; }
-
-    /// <summary>
-    ///     Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
-    ///     <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
-    ///     allows <see cref="Checked"/> to be true or false.
-    /// </summary>
-    public bool AllowNullChecked
-    {
-        get => _allowNullChecked;
-        set
-        {
-            _allowNullChecked = value;
-            Checked ??= false;
-        }
-    }
-
-    /// <summary>
-    ///     Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
-    ///     returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
-    /// </summary>
-    /// <value>Function to determine if the action is can be executed or not.</value>
-    public Func<bool> CanExecute { get; set; }
-
-    /// <summary>
-    ///     Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
-    ///     <see cref="MenuItemCheckStyle"/>.
-    /// </summary>
-    public bool? Checked { set; get; }
-
-    /// <summary>
-    ///     Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
-    ///     <see langword="true"/>.
-    /// </summary>
-    public MenuItemCheckStyle CheckType
-    {
-        get => _checkType;
-        set
-        {
-            _checkType = value;
-
-            if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
-            {
-                Checked = false;
-            }
-        }
-    }
-
-    /// <summary>Gets or sets arbitrary data for the menu item.</summary>
-    /// <remarks>This property is not used internally.</remarks>
-    public object Data { get; set; }
-
-    /// <summary>Gets or sets the help text for the menu item. The help text is drawn to the right of the <see cref="Title"/>.</summary>
-    /// <value>The help text.</value>
-    public string Help { get; set; }
-
-    /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
-    /// <value>The parent.</value>
-    public MenuItem Parent { get; set; }
-
-    /// <summary>Gets or sets the title of the menu item .</summary>
-    /// <value>The title.</value>
-    public string Title
-    {
-        get => _title;
-        set
-        {
-            if (_title == value)
-            {
-                return;
-            }
-
-            _title = value;
-            GetHotKey ();
-        }
-    }
-
-    /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
-    internal bool IsFromSubMenu => Parent != null;
-
-    internal int TitleLength => GetMenuBarItemLength (Title);
-
-    // 
-    // ┌─────────────────────────────┐
-    // │ Quit  Quit UI Catalog  Ctrl+Q │
-    // └─────────────────────────────┘
-    // ┌─────────────────┐
-    // │ ◌ TopLevel Alt+T │
-    // └─────────────────┘
-    // TODO: Replace the `2` literals with named constants 
-    internal int Width => 1
-                          + // space before Title
-                          TitleLength
-                          + 2
-                          + // space after Title - BUGBUG: This should be 1 
-                          (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
-                               ? 2
-                               : 0)
-                          + // check glyph + space 
-                          (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
-                          + // Two spaces before Help
-                          (ShortcutTag.GetColumns () > 0
-                               ? 2 + ShortcutTag.GetColumns ()
-                               : 0); // Pad two spaces before shortcut tag (which are also aligned right)
-
-    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
-    public bool GetMenuBarItem () { return IsFromSubMenu; }
-
-    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
-    public MenuItem GetMenuItem () { return this; }
-
-    /// <summary>
-    ///     Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
-    ///     <see cref="CanExecute"/>.
-    /// </summary>
-    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
-
-    /// <summary>
-    ///     Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
-    ///     <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
-    /// </summary>
-    public void ToggleChecked ()
-    {
-        if (_checkType != MenuItemCheckStyle.Checked)
-        {
-            throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
-        }
-
-        bool? previousChecked = Checked;
-
-        if (AllowNullChecked)
-        {
-            Checked = previousChecked switch
-                      {
-                          null => true,
-                          true => false,
-                          false => null
-                      };
-        }
-        else
-        {
-            Checked = !Checked;
-        }
-    }
-
-    private static int GetMenuBarItemLength (string title)
-    {
-        return title.EnumerateRunes ()
-                    .Where (ch => ch != MenuBar.HotKeySpecifier)
-                    .Sum (ch => Math.Max (ch.GetColumns (), 1));
-    }
-
-    #region Keyboard Handling
-
-    // TODO: Update to use Key instead of Rune
-    /// <summary>
-    ///     The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
-    ///     <see cref="Title"/> of a MenuItem with an underscore ('_').
-    ///     <para>
-    ///         Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
-    ///         not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
-    ///     </para>
-    ///     <para>
-    ///         For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
-    ///         File menu. Pressing the N key will then activate the New MenuItem.
-    ///     </para>
-    ///     <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
-    /// </summary>
-    public Rune HotKey { get; set; }
-
-    private void GetHotKey ()
-    {
-        var nextIsHot = false;
-
-        foreach (char x in _title)
-        {
-            if (x == MenuBar.HotKeySpecifier.Value)
-            {
-                nextIsHot = true;
-            }
-            else
-            {
-                if (nextIsHot)
-                {
-                    HotKey = (Rune)char.ToUpper (x);
-
-                    break;
-                }
-
-                nextIsHot = false;
-                HotKey = default (Rune);
-            }
-        }
-    }
-
-    // TODO: Update to use Key instead of KeyCode
-    /// <summary>
-    ///     Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
-    ///     <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
-    ///     <see cref="MenuItem"/>.
-    ///     <para>
-    ///         The <see cref="KeyCode"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
-    ///         <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
-    ///     </para>
-    /// </summary>
-    public KeyCode Shortcut
-    {
-        get => _shortcutHelper.Shortcut;
-        set
-        {
-            if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
-            {
-                _shortcutHelper.Shortcut = value;
-            }
-        }
-    }
-
-    /// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
-    public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
-                                     ? string.Empty
-                                     : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
-
-    #endregion Keyboard Handling
-}
-
 /// <summary>
 ///     An internal class used to represent a menu pop-up menu. Created and managed by <see cref="MenuBar"/> and
 ///     <see cref="ContextMenu"/>.
@@ -408,15 +121,6 @@ internal sealed class Menu : View
                    );
 
         AddKeyBindings (_barItems);
-#if SUPPORT_ALT_TO_ACTIVATE_MENU
-        Initialized += (s, e) =>
-                       {
-                           if (SuperView is { })
-                           {
-                               SuperView.KeyUp += SuperView_KeyUp;
-                           }
-                       };
-#endif
     }
 
     public Menu ()
@@ -462,9 +166,9 @@ internal sealed class Menu : View
                         return true;
                     }
                    );
-        AddCommand (Command.Select, () => _host?.SelectItem (_menuItemToSelect));
-        AddCommand (Command.ToggleExpandCollapse, () => SelectOrRun ());
-        AddCommand (Command.HotKey, () => _host?.SelectItem (_menuItemToSelect));
+        AddCommand (Command.Select, ctx => _host?.SelectItem (ctx.KeyBinding?.Context as MenuItem));
+        AddCommand (Command.ToggleExpandCollapse, ctx => ExpandCollapse (ctx.KeyBinding?.Context as MenuItem));
+        AddCommand (Command.HotKey, ctx => _host?.SelectItem (ctx.KeyBinding?.Context as MenuItem));
 
         // Default key bindings for this view
         KeyBindings.Add (Key.CursorUp, Command.LineUp);
@@ -473,26 +177,7 @@ internal sealed class Menu : View
         KeyBindings.Add (Key.CursorRight, Command.Right);
         KeyBindings.Add (Key.Esc, Command.Cancel);
         KeyBindings.Add (Key.Enter, Command.Accept);
-        KeyBindings.Add (Key.F9, KeyBindingScope.HotKey, Command.ToggleExpandCollapse);
-
-        KeyBindings.Add (
-                         KeyCode.CtrlMask | KeyCode.Space,
-                         KeyBindingScope.HotKey,
-                         Command.ToggleExpandCollapse
-                        );
-    }
-
-#if SUPPORT_ALT_TO_ACTIVATE_MENU
-    void SuperView_KeyUp (object sender, KeyEventArgs e)
-    {
-        if (SuperView is null || SuperView.CanFocus == false || SuperView.Visible == false)
-        {
-            return;
-        }
-
-        _host.AltKeyUpHandler (e);
     }
-#endif
 
     private void AddKeyBindings (MenuBarItem menuBarItem)
     {
@@ -503,16 +188,18 @@ internal sealed class Menu : View
 
         foreach (MenuItem menuItem in menuBarItem.Children.Where (m => m is { }))
         {
-            KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, Command.ToggleExpandCollapse);
+            KeyBinding keyBinding = new ([Command.ToggleExpandCollapse], KeyBindingScope.HotKey, menuItem);
 
-            KeyBindings.Add (
-                             (KeyCode)menuItem.HotKey.Value | KeyCode.AltMask,
-                             Command.ToggleExpandCollapse
-                            );
+            if ((KeyCode)menuItem.HotKey.Value != KeyCode.Null)
+            {
+                KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, keyBinding);
+                KeyBindings.Add ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask, keyBinding);
+            }
 
             if (menuItem.Shortcut != KeyCode.Null)
             {
-                KeyBindings.Add (menuItem.Shortcut, KeyBindingScope.HotKey, Command.Select);
+                keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
+                KeyBindings.Add (menuItem.Shortcut, keyBinding);
             }
 
             MenuBarItem subMenu = menuBarItem.SubMenu (menuItem);
@@ -520,25 +207,29 @@ internal sealed class Menu : View
         }
     }
 
-    private int _menuBarItemToActivate = -1;
-    private MenuItem _menuItemToSelect;
-
-    /// <summary>Called when a key bound to Command.Select is pressed. This means a hot key was pressed.</summary>
+    /// <summary>Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed.</summary>
     /// <returns></returns>
-    private bool SelectOrRun ()
+    private bool ExpandCollapse (MenuItem menuItem)
     {
         if (!IsInitialized || !Visible)
         {
             return true;
         }
 
-        if (_menuBarItemToActivate != -1)
+
+        for (var c = 0; c < _barItems.Children.Length; c++)
         {
-            _host.Activate (1, _menuBarItemToActivate);
+            if (_barItems.Children [c] == menuItem)
+            {
+                _currentChild = c;
+
+                break;
+            }
         }
-        else if (_menuItemToSelect is { })
+
+        if (menuItem is { })
         {
-            var m = _menuItemToSelect as MenuBarItem;
+            var m = menuItem as MenuBarItem;
 
             if (m?.Children?.Length > 0)
             {
@@ -566,7 +257,7 @@ internal sealed class Menu : View
             }
             else
             {
-                _host.SelectItem (_menuItemToSelect);
+                _host.SelectItem (menuItem);
             }
         }
         else if (_host.IsMenuOpen)
@@ -578,82 +269,12 @@ internal sealed class Menu : View
             _host.OpenMenu ();
         }
 
-        //_openedByHotKey = true;
         return true;
     }
 
     /// <inheritdoc/>
     public override bool? OnInvokingKeyBindings (Key keyEvent)
     {
-        // This is a bit of a hack. We want to handle the key bindings for menu bar but
-        // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
-        // So before we call the base class we set SelectedItem appropriately.
-
-        KeyCode key = keyEvent.KeyCode;
-
-        if (KeyBindings.TryGet (key, out _))
-        {
-            _menuBarItemToActivate = -1;
-            _menuItemToSelect = null;
-
-            MenuItem [] children = _barItems.Children;
-
-            if (children is null)
-            {
-                return base.OnInvokingKeyBindings (keyEvent);
-            }
-
-            // Search for shortcuts first. If there's a shortcut, we don't want to activate the menu item.
-            foreach (MenuItem c in children)
-            {
-                if (key == c?.Shortcut)
-                {
-                    _menuBarItemToActivate = -1;
-                    _menuItemToSelect = c;
-                    //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                    return base.OnInvokingKeyBindings (keyEvent);
-                }
-
-                MenuBarItem subMenu = _barItems.SubMenu (c);
-
-                if (FindShortcutInChildMenu (key, subMenu))
-                {
-                    //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                    return base.OnInvokingKeyBindings (keyEvent);
-                }
-            }
-
-            // Search for hot keys next.
-            for (var c = 0; c < children.Length; c++)
-            {
-                int hotKeyValue = children [c]?.HotKey.Value ?? default (int);
-                var hotKey = (KeyCode)hotKeyValue;
-
-                if (hotKey == KeyCode.Null)
-                {
-                    continue;
-                }
-
-                bool matches = key == hotKey || key == (hotKey | KeyCode.AltMask);
-
-                if (!_host.IsMenuOpen)
-                {
-                    // If the menu is open, only match if Alt is not pressed.
-                    matches = key == hotKey;
-                }
-
-                if (matches)
-                {
-                    _menuItemToSelect = children [c];
-                    _currentChild = c;
-
-                    return base.OnInvokingKeyBindings (keyEvent);
-                }
-            }
-        }
-
         bool? handled = base.OnInvokingKeyBindings (keyEvent);
 
         if (handled is { } && (bool)handled)
@@ -661,34 +282,11 @@ internal sealed class Menu : View
             return true;
         }
 
+        // TODO: Determine if there's a cleaner way to handle this
         // This supports the case where the menu bar is a context menu
         return _host.OnInvokingKeyBindings (keyEvent);
     }
 
-    private bool FindShortcutInChildMenu (KeyCode key, MenuBarItem menuBarItem)
-    {
-        if (menuBarItem?.Children is null)
-        {
-            return false;
-        }
-
-        foreach (MenuItem menuItem in menuBarItem.Children)
-        {
-            if (key == menuItem?.Shortcut)
-            {
-                _menuBarItemToActivate = -1;
-                _menuItemToSelect = menuItem;
-
-                return true;
-            }
-
-            MenuBarItem subMenu = menuBarItem.SubMenu (menuItem);
-            FindShortcutInChildMenu (key, subMenu);
-        }
-
-        return false;
-    }
-
     private void Current_TerminalResized (object sender, SizeChangedEventArgs e)
     {
         if (_host.IsMenuOpen)
@@ -727,6 +325,7 @@ internal sealed class Menu : View
         View view = a.View ?? this;
 
         Point boundsPoint = view.ScreenToViewport (new (a.Position.X, a.Position.Y));
+
         var me = new MouseEvent
         {
             Position = boundsPoint,
@@ -786,12 +385,12 @@ internal sealed class Menu : View
 
             Driver.SetAttribute (
                                  item is null ? GetNormalColor () :
-                                 i == _currentChild ? GetFocusColor() : GetNormalColor ()
+                                 i == _currentChild ? GetFocusColor () : GetNormalColor ()
                                 );
 
             if (item is null && BorderStyle != LineStyle.None)
             {
-                var s = ViewportToScreen (new Point (-1, i));
+                Point s = ViewportToScreen (new Point (-1, i));
                 Driver.Move (s.X, s.Y);
                 Driver.AddRune (Glyphs.LeftTee);
             }
@@ -839,7 +438,7 @@ internal sealed class Menu : View
             {
                 if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width)
                 {
-                    var s = ViewportToScreen (new Point (Frame.Width - 2, i));
+                    Point s = ViewportToScreen (new Point (Frame.Width - 2, i));
                     Driver.Move (s.X, s.Y);
                     Driver.AddRune (Glyphs.RightTee);
                 }
@@ -876,7 +475,8 @@ internal sealed class Menu : View
                 textToDraw = item.Title;
             }
 
-            var screen = ViewportToScreen (new Point(0  , i));
+            Point screen = ViewportToScreen (new Point (0, i));
+
             if (screen.X < Driver.Cols)
             {
                 Driver.Move (screen.X + 1, screen.Y);
@@ -890,12 +490,12 @@ internal sealed class Menu : View
                     var tf = new TextFormatter
                     {
                         AutoSize = true,
-                        Alignment = TextAlignment.Centered, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw
+                        Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw
                     };
 
                     // The -3 is left/right border + one space (not sure what for)
                     tf.Draw (
-                             ViewportToScreen (new Rectangle(1, i, Frame.Width - 3, 1)),
+                             ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)),
                              i == _currentChild ? GetFocusColor () : GetNormalColor (),
                              i == _currentChild ? ColorScheme.HotFocus : ColorScheme.HotNormal,
                              SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty
@@ -934,7 +534,7 @@ internal sealed class Menu : View
 
         Driver.Clip = savedClip;
 
-       // PositionCursor ();
+        // PositionCursor ();
     }
 
     private void Current_DrawContentComplete (object sender, DrawEventArgs e)
@@ -953,13 +553,10 @@ internal sealed class Menu : View
             {
                 return _host?.PositionCursor ();
             }
-            else
-            {
-                Move (2, 1 + _currentChild);
 
-                return null; // Don't show the cursor
+            Move (2, 1 + _currentChild);
 
-            }
+            return null; // Don't show the cursor
         }
 
         return _host?.PositionCursor ();
@@ -1031,11 +628,11 @@ internal sealed class Menu : View
                 _currentChild = 0;
             }
 
-            if (this != _host.openCurrentMenu && _barItems.Children [_currentChild]?.IsFromSubMenu == true && _host._selectedSub > -1)
+            if (this != _host.OpenCurrentMenu && _barItems.Children [_currentChild]?.IsFromSubMenu == true && _host._selectedSub > -1)
             {
                 _host.PreviousMenu (true);
                 _host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild);
-                _host.openCurrentMenu = this;
+                _host.OpenCurrentMenu = this;
             }
 
             MenuItem item = _barItems.Children [_currentChild];
@@ -1096,7 +693,7 @@ internal sealed class Menu : View
 
             if (_host.UseKeysUpDownAsKeysLeftRight && !_host.UseSubMenusSingleFrame)
             {
-                if ((_currentChild == -1 || this != _host.openCurrentMenu)
+                if ((_currentChild == -1 || this != _host.OpenCurrentMenu)
                     && _barItems.Children [_currentChild + 1].IsFromSubMenu
                     && _host._selectedSub > -1)
                 {
@@ -1106,7 +703,7 @@ internal sealed class Menu : View
                     if (_currentChild > 0)
                     {
                         _currentChild--;
-                        _host.openCurrentMenu = this;
+                        _host.OpenCurrentMenu = this;
                     }
 
                     break;
@@ -1176,7 +773,7 @@ internal sealed class Menu : View
         _host?.SetNeedsDisplay ();
     }
 
-    protected internal override bool OnMouseEvent  (MouseEvent me)
+    protected internal override bool OnMouseEvent (MouseEvent me)
     {
         if (!_host._handled && !_host.HandleGrabView (me, this))
         {
@@ -1285,8 +882,8 @@ internal sealed class Menu : View
             }
 
             if (pos == -1
-                && this != _host.openCurrentMenu
-                && subMenu.Children != _host.openCurrentMenu._barItems.Children
+                && this != _host.OpenCurrentMenu
+                && subMenu.Children != _host.OpenCurrentMenu._barItems.Children
                 && !_host.CloseMenu (false, true))
             {
                 return false;
@@ -1307,33 +904,6 @@ internal sealed class Menu : View
         return true;
     }
 
-    private int GetSubMenuIndex (MenuBarItem subMenu)
-    {
-        int pos = -1;
-
-        if (Subviews.Count == 0)
-        {
-            return pos;
-        }
-
-        Menu v = null;
-
-        foreach (View menu in Subviews)
-        {
-            if (((Menu)menu)._barItems == subMenu)
-            {
-                v = (Menu)menu;
-            }
-        }
-
-        if (v is { })
-        {
-            pos = Subviews.IndexOf (v);
-        }
-
-        return pos;
-    }
-
     protected override void Dispose (bool disposing)
     {
         if (Application.Current is { })

File diff suppressed because it is too large
+ 127 - 450
Terminal.Gui/Views/Menu/MenuBar.cs


+ 178 - 0
Terminal.Gui/Views/Menu/MenuBarItem.cs

@@ -0,0 +1,178 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     <see cref="MenuBarItem"/> is a menu item on  <see cref="MenuBar"/>. MenuBarItems do not support
+///     <see cref="MenuItem.Shortcut"/>.
+/// </summary>
+public class MenuBarItem : MenuItem
+{
+    /// <summary>Initializes a new <see cref="MenuBarItem"/> as a <see cref="MenuItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="help">Help text to display. Will be displayed next to the Title surrounded by parentheses.</param>
+    /// <param name="action">Action to invoke when the menu item is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if any.</param>
+    public MenuBarItem (
+        string title,
+        string help,
+        Action action,
+        Func<bool> canExecute = null,
+        MenuItem parent = null
+    ) : base (title, help, action, canExecute, parent)
+    {
+        SetInitialProperties (title, null, null, true);
+    }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="children">The items in the current menu.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if any.</param>
+    public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/> with separate list of items.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="children">The list of items in the current menu.</param>
+    /// <param name="parent">The parent <see cref="MenuItem"/> of this if any.</param>
+    public MenuBarItem (string title, List<MenuItem []> children, MenuItem parent = null) { SetInitialProperties (title, children, parent); }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    /// <param name="children">The items in the current menu.</param>
+    public MenuBarItem (MenuItem [] children) : this ("", children) { }
+
+    /// <summary>Initializes a new <see cref="MenuBarItem"/>.</summary>
+    public MenuBarItem () : this ([]) { }
+
+    /// <summary>
+    ///     Gets or sets an array of <see cref="MenuItem"/> objects that are the children of this
+    ///     <see cref="MenuBarItem"/>
+    /// </summary>
+    /// <value>The children.</value>
+    public MenuItem [] Children { get; set; }
+
+    internal bool IsTopLevel => Parent is null && (Children is null || Children.Length == 0) && Action != null;
+
+    /// <summary>Get the index of a child <see cref="MenuItem"/>.</summary>
+    /// <param name="children"></param>
+    /// <returns>Returns a greater than -1 if the <see cref="MenuItem"/> is a child.</returns>
+    public int GetChildrenIndex (MenuItem children)
+    {
+        var i = 0;
+
+        if (Children is null)
+        {
+            return -1;
+        }
+
+        foreach (MenuItem child in Children)
+        {
+            if (child == children)
+            {
+                return i;
+            }
+
+            i++;
+        }
+
+        return -1;
+    }
+
+    /// <summary>Check if a <see cref="MenuItem"/> is a submenu of this MenuBar.</summary>
+    /// <param name="menuItem"></param>
+    /// <returns>Returns <c>true</c> if it is a submenu. <c>false</c> otherwise.</returns>
+    public bool IsSubMenuOf (MenuItem menuItem)
+    {
+        return Children.Any (child => child == menuItem && child.Parent == menuItem.Parent);
+    }
+
+    /// <summary>Check if a <see cref="MenuItem"/> is a <see cref="MenuBarItem"/>.</summary>
+    /// <param name="menuItem"></param>
+    /// <returns>Returns a <see cref="MenuBarItem"/> or null otherwise.</returns>
+    public MenuBarItem SubMenu (MenuItem menuItem) { return menuItem as MenuBarItem; }
+
+    internal void AddShortcutKeyBindings (MenuBar menuBar)
+    {
+        if (Children is null)
+        {
+            return;
+        }
+
+        foreach (MenuItem menuItem in Children.Where (m => m is { }))
+        {
+            // For MenuBar only add shortcuts for submenus
+
+            if (menuItem.Shortcut != KeyCode.Null)
+            {
+                KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem);
+                menuBar.KeyBindings.Add (menuItem.Shortcut, keyBinding);
+            }
+
+            SubMenu (menuItem)?.AddShortcutKeyBindings (menuBar);
+        }
+    }
+
+    private void SetInitialProperties (string title, object children, MenuItem parent = null, bool isTopLevel = false)
+    {
+        if (!isTopLevel && children is null)
+        {
+            throw new ArgumentNullException (
+                                             nameof (children),
+                                             @"The parameter cannot be null. Use an empty array instead."
+                                            );
+        }
+
+        SetTitle (title ?? "");
+
+        if (parent is { })
+        {
+            Parent = parent;
+        }
+
+        switch (children)
+        {
+            case List<MenuItem []> childrenList:
+            {
+                MenuItem [] newChildren = [];
+
+                foreach (MenuItem [] grandChild in childrenList)
+                {
+                    foreach (MenuItem child in grandChild)
+                    {
+                        SetParent (grandChild);
+                        Array.Resize (ref newChildren, newChildren.Length + 1);
+                        newChildren [^1] = child;
+                    }
+                }
+
+                Children = newChildren;
+
+                break;
+            }
+            case MenuItem [] items:
+                SetParent (items);
+                Children = items;
+
+                break;
+            default:
+                Children = null;
+
+                break;
+        }
+    }
+
+    private void SetParent (MenuItem [] children)
+    {
+        foreach (MenuItem child in children)
+        {
+            if (child is { Parent: null })
+            {
+                child.Parent = this;
+            }
+        }
+    }
+
+    private void SetTitle (string title)
+    {
+        title ??= string.Empty;
+        Title = title;
+    }
+}

+ 31 - 0
Terminal.Gui/Views/Menu/MenuClosingEventArgs.cs

@@ -0,0 +1,31 @@
+namespace Terminal.Gui;
+
+/// <summary>An <see cref="EventArgs"/> which allows passing a cancelable menu closing event.</summary>
+public class MenuClosingEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="MenuClosingEventArgs"/>.</summary>
+    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
+    /// <param name="reopen">Whether the current menu will reopen.</param>
+    /// <param name="isSubMenu">Indicates whether it is a sub-menu.</param>
+    public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
+    {
+        CurrentMenu = currentMenu;
+        Reopen = reopen;
+        IsSubMenu = isSubMenu;
+    }
+
+    /// <summary>
+    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
+    ///     event will be canceled.
+    /// </summary>
+    public bool Cancel { get; set; }
+
+    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
+    public MenuBarItem CurrentMenu { get; }
+
+    /// <summary>Indicates whether the current menu is a sub-menu.</summary>
+    public bool IsSubMenu { get; }
+
+    /// <summary>Indicates whether the current menu will reopen.</summary>
+    public bool Reopen { get; }
+}

+ 0 - 73
Terminal.Gui/Views/Menu/MenuEventArgs.cs

@@ -1,73 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary>
-///     An <see cref="EventArgs"/> which allows passing a cancelable menu opening event or replacing with a new
-///     <see cref="MenuBarItem"/>.
-/// </summary>
-public class MenuOpeningEventArgs : EventArgs
-{
-    /// <summary>Initializes a new instance of <see cref="MenuOpeningEventArgs"/>.</summary>
-    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
-    public MenuOpeningEventArgs (MenuBarItem currentMenu) { CurrentMenu = currentMenu; }
-
-    /// <summary>
-    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
-    ///     event will be canceled.
-    /// </summary>
-    public bool Cancel { get; set; }
-
-    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
-    public MenuBarItem CurrentMenu { get; }
-
-    /// <summary>The new <see cref="MenuBarItem"/> to be replaced.</summary>
-    public MenuBarItem NewMenuBarItem { get; set; }
-}
-
-/// <summary>Defines arguments for the <see cref="MenuBar.MenuOpened"/> event</summary>
-public class MenuOpenedEventArgs : EventArgs
-{
-    /// <summary>Creates a new instance of the <see cref="MenuOpenedEventArgs"/> class</summary>
-    /// <param name="parent"></param>
-    /// <param name="menuItem"></param>
-    public MenuOpenedEventArgs (MenuBarItem parent, MenuItem menuItem)
-    {
-        Parent = parent;
-        MenuItem = menuItem;
-    }
-
-    /// <summary>Gets the <see cref="MenuItem"/> being opened.</summary>
-    public MenuItem MenuItem { get; }
-
-    /// <summary>The parent of <see cref="MenuItem"/>. Will be null if menu opening is the root.</summary>
-    public MenuBarItem Parent { get; }
-}
-
-/// <summary>An <see cref="EventArgs"/> which allows passing a cancelable menu closing event.</summary>
-public class MenuClosingEventArgs : EventArgs
-{
-    /// <summary>Initializes a new instance of <see cref="MenuClosingEventArgs"/>.</summary>
-    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
-    /// <param name="reopen">Whether the current menu will reopen.</param>
-    /// <param name="isSubMenu">Indicates whether it is a sub-menu.</param>
-    public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu)
-    {
-        CurrentMenu = currentMenu;
-        Reopen = reopen;
-        IsSubMenu = isSubMenu;
-    }
-
-    /// <summary>
-    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
-    ///     event will be canceled.
-    /// </summary>
-    public bool Cancel { get; set; }
-
-    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
-    public MenuBarItem CurrentMenu { get; }
-
-    /// <summary>Indicates whether the current menu is a sub-menu.</summary>
-    public bool IsSubMenu { get; }
-
-    /// <summary>Indicates whether the current menu will reopen.</summary>
-    public bool Reopen { get; }
-}

+ 273 - 0
Terminal.Gui/Views/Menu/MenuItem.cs

@@ -0,0 +1,273 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     A <see cref="MenuItem"/> has title, an associated help text, and an action to execute on activation. MenuItems
+///     can also have a checked indicator (see <see cref="Checked"/>).
+/// </summary>
+public class MenuItem
+{
+    private readonly ShortcutHelper _shortcutHelper;
+    private bool _allowNullChecked;
+    private MenuItemCheckStyle _checkType;
+
+    private string _title;
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>Initializes a new instance of <see cref="MenuItem"/></summary>
+    public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { }
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>Initializes a new instance of <see cref="MenuItem"/>.</summary>
+    /// <param name="title">Title for the menu item.</param>
+    /// <param name="help">Help text to display.</param>
+    /// <param name="action">Action to invoke when the menu item is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    /// <param name="parent">The <see cref="Parent"/> of this menu item.</param>
+    /// <param name="shortcut">The <see cref="Shortcut"/> keystroke combination.</param>
+    public MenuItem (
+        string title,
+        string help,
+        Action action,
+        Func<bool> canExecute = null,
+        MenuItem parent = null,
+        KeyCode shortcut = KeyCode.Null
+    )
+    {
+        Title = title ?? "";
+        Help = help ?? "";
+        Action = action;
+        CanExecute = canExecute;
+        Parent = parent;
+        _shortcutHelper = new ();
+
+        if (shortcut != KeyCode.Null)
+        {
+            Shortcut = shortcut;
+        }
+    }
+
+    /// <summary>Gets or sets the action to be invoked when the menu item is triggered.</summary>
+    /// <value>Method to invoke.</value>
+    public Action Action { get; set; }
+
+    /// <summary>
+    ///     Used only if <see cref="CheckType"/> is of <see cref="MenuItemCheckStyle.Checked"/> type. If
+    ///     <see langword="true"/> allows <see cref="Checked"/> to be null, true or false. If <see langword="false"/> only
+    ///     allows <see cref="Checked"/> to be true or false.
+    /// </summary>
+    public bool AllowNullChecked
+    {
+        get => _allowNullChecked;
+        set
+        {
+            _allowNullChecked = value;
+            Checked ??= false;
+        }
+    }
+
+    /// <summary>
+    ///     Gets or sets the action to be invoked to determine if the menu can be triggered. If <see cref="CanExecute"/>
+    ///     returns <see langword="true"/> the menu item will be enabled. Otherwise, it will be disabled.
+    /// </summary>
+    /// <value>Function to determine if the action is can be executed or not.</value>
+    public Func<bool> CanExecute { get; set; }
+
+    /// <summary>
+    ///     Sets or gets whether the <see cref="MenuItem"/> shows a check indicator or not. See
+    ///     <see cref="MenuItemCheckStyle"/>.
+    /// </summary>
+    public bool? Checked { set; get; }
+
+    /// <summary>
+    ///     Sets or gets the <see cref="MenuItemCheckStyle"/> of a menu item where <see cref="Checked"/> is set to
+    ///     <see langword="true"/>.
+    /// </summary>
+    public MenuItemCheckStyle CheckType
+    {
+        get => _checkType;
+        set
+        {
+            _checkType = value;
+
+            if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked is null)
+            {
+                Checked = false;
+            }
+        }
+    }
+
+    /// <summary>Gets or sets arbitrary data for the menu item.</summary>
+    /// <remarks>This property is not used internally.</remarks>
+    public object Data { get; set; }
+
+    /// <summary>Gets or sets the help text for the menu item. The help text is drawn to the right of the <see cref="Title"/>.</summary>
+    /// <value>The help text.</value>
+    public string Help { get; set; }
+
+    /// <summary>Gets the parent for this <see cref="MenuItem"/>.</summary>
+    /// <value>The parent.</value>
+    public MenuItem Parent { get; set; }
+
+    /// <summary>Gets or sets the title of the menu item .</summary>
+    /// <value>The title.</value>
+    public string Title
+    {
+        get => _title;
+        set
+        {
+            if (_title == value)
+            {
+                return;
+            }
+
+            _title = value;
+            GetHotKey ();
+        }
+    }
+
+    /// <summary>Gets if this <see cref="MenuItem"/> is from a sub-menu.</summary>
+    internal bool IsFromSubMenu => Parent != null;
+
+    internal int TitleLength => GetMenuBarItemLength (Title);
+
+    // 
+    // ┌─────────────────────────────┐
+    // │ Quit  Quit UI Catalog  Ctrl+Q │
+    // └─────────────────────────────┘
+    // ┌─────────────────┐
+    // │ ◌ TopLevel Alt+T │
+    // └─────────────────┘
+    // TODO: Replace the `2` literals with named constants 
+    internal int Width => 1
+                          + // space before Title
+                          TitleLength
+                          + 2
+                          + // space after Title - BUGBUG: This should be 1 
+                          (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio)
+                               ? 2
+                               : 0)
+                          + // check glyph + space 
+                          (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0)
+                          + // Two spaces before Help
+                          (ShortcutTag.GetColumns () > 0
+                               ? 2 + ShortcutTag.GetColumns ()
+                               : 0); // Pad two spaces before shortcut tag (which are also aligned right)
+
+    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
+    internal bool GetMenuBarItem () { return IsFromSubMenu; }
+
+    /// <summary>Merely a debugging aid to see the interaction with main.</summary>
+    internal MenuItem GetMenuItem () { return this; }
+
+    /// <summary>
+    ///     Returns <see langword="true"/> if the menu item is enabled. This method is a wrapper around
+    ///     <see cref="CanExecute"/>.
+    /// </summary>
+    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
+
+    /// <summary>
+    ///     Toggle the <see cref="Checked"/> between three states if <see cref="AllowNullChecked"/> is
+    ///     <see langword="true"/> or between two states if <see cref="AllowNullChecked"/> is <see langword="false"/>.
+    /// </summary>
+    public void ToggleChecked ()
+    {
+        if (_checkType != MenuItemCheckStyle.Checked)
+        {
+            throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!");
+        }
+
+        bool? previousChecked = Checked;
+
+        if (AllowNullChecked)
+        {
+            Checked = previousChecked switch
+                      {
+                          null => true,
+                          true => false,
+                          false => null
+                      };
+        }
+        else
+        {
+            Checked = !Checked;
+        }
+    }
+
+    private static int GetMenuBarItemLength (string title)
+    {
+        return title.EnumerateRunes ()
+                    .Where (ch => ch != MenuBar.HotKeySpecifier)
+                    .Sum (ch => Math.Max (ch.GetColumns (), 1));
+    }
+
+    #region Keyboard Handling
+
+    // TODO: Update to use Key instead of Rune
+    /// <summary>
+    ///     The HotKey is used to activate a <see cref="MenuItem"/> with the keyboard. HotKeys are defined by prefixing the
+    ///     <see cref="Title"/> of a MenuItem with an underscore ('_').
+    ///     <para>
+    ///         Pressing Alt-Hotkey for a <see cref="MenuBarItem"/> (menu items on the menu bar) works even if the menu is
+    ///         not active). Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem.
+    ///     </para>
+    ///     <para>
+    ///         For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the
+    ///         File menu. Pressing the N key will then activate the New MenuItem.
+    ///     </para>
+    ///     <para>See also <see cref="Shortcut"/> which enable global key-bindings to menu items.</para>
+    /// </summary>
+    public Rune HotKey { get; set; }
+    private void GetHotKey ()
+    {
+        var nextIsHot = false;
+
+        foreach (char x in _title)
+        {
+            if (x == MenuBar.HotKeySpecifier.Value)
+            {
+                nextIsHot = true;
+            }
+            else
+            {
+                if (nextIsHot)
+                {
+                    HotKey = (Rune)char.ToUpper (x);
+
+                    break;
+                }
+
+                nextIsHot = false;
+                HotKey = default (Rune);
+            }
+        }
+    }
+
+    // TODO: Update to use Key instead of KeyCode
+    /// <summary>
+    ///     Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the
+    ///     <see cref="View"/> that is the parent of the <see cref="MenuBar"/> or <see cref="ContextMenu"/> this
+    ///     <see cref="MenuItem"/>.
+    ///     <para>
+    ///         The <see cref="KeyCode"/> will be drawn on the MenuItem to the right of the <see cref="Title"/> and
+    ///         <see cref="Help"/> text. See <see cref="ShortcutTag"/>.
+    ///     </para>
+    /// </summary>
+    public KeyCode Shortcut
+    {
+        get => _shortcutHelper.Shortcut;
+        set
+        {
+            if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null))
+            {
+                _shortcutHelper.Shortcut = value;
+            }
+        }
+    }
+
+    /// <summary>Gets the text describing the keystroke combination defined by <see cref="Shortcut"/>.</summary>
+    public string ShortcutTag => _shortcutHelper.Shortcut == KeyCode.Null
+                                     ? string.Empty
+                                     : Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter);
+
+    #endregion Keyboard Handling
+}

+ 15 - 0
Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs

@@ -0,0 +1,15 @@
+namespace Terminal.Gui;
+
+/// <summary>Specifies how a <see cref="MenuItem"/> shows selection state.</summary>
+[Flags]
+public enum MenuItemCheckStyle
+{
+    /// <summary>The menu item will be shown normally, with no check indicator. The default.</summary>
+    NoCheck = 0b_0000_0000,
+
+    /// <summary>The menu item will indicate checked/un-checked state (see <see cref="Checked"/>).</summary>
+    Checked = 0b_0000_0001,
+
+    /// <summary>The menu item is part of a menu radio group (see <see cref="Checked"/>) and will indicate selected state.</summary>
+    Radio = 0b_0000_0010
+}

+ 20 - 0
Terminal.Gui/Views/Menu/MenuOpenedEventArgs.cs

@@ -0,0 +1,20 @@
+namespace Terminal.Gui;
+
+/// <summary>Defines arguments for the <see cref="MenuBar.MenuOpened"/> event</summary>
+public class MenuOpenedEventArgs : EventArgs
+{
+    /// <summary>Creates a new instance of the <see cref="MenuOpenedEventArgs"/> class</summary>
+    /// <param name="parent"></param>
+    /// <param name="menuItem"></param>
+    public MenuOpenedEventArgs (MenuBarItem parent, MenuItem menuItem)
+    {
+        Parent = parent;
+        MenuItem = menuItem;
+    }
+
+    /// <summary>Gets the <see cref="MenuItem"/> being opened.</summary>
+    public MenuItem MenuItem { get; }
+
+    /// <summary>The parent of <see cref="MenuItem"/>. Will be null if menu opening is the root.</summary>
+    public MenuBarItem Parent { get; }
+}

+ 24 - 0
Terminal.Gui/Views/Menu/MenuOpeningEventArgs.cs

@@ -0,0 +1,24 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     An <see cref="EventArgs"/> which allows passing a cancelable menu opening event or replacing with a new
+///     <see cref="MenuBarItem"/>.
+/// </summary>
+public class MenuOpeningEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="MenuOpeningEventArgs"/>.</summary>
+    /// <param name="currentMenu">The current <see cref="MenuBarItem"/> parent.</param>
+    public MenuOpeningEventArgs (MenuBarItem currentMenu) { CurrentMenu = currentMenu; }
+
+    /// <summary>
+    ///     Flag that allows the cancellation of the event. If set to <see langword="true"/> in the event handler, the
+    ///     event will be canceled.
+    /// </summary>
+    public bool Cancel { get; set; }
+
+    /// <summary>The current <see cref="MenuBarItem"/> parent.</summary>
+    public MenuBarItem CurrentMenu { get; }
+
+    /// <summary>The new <see cref="MenuBarItem"/> to be replaced.</summary>
+    public MenuBarItem NewMenuBarItem { get; set; }
+}

+ 112 - 94
Terminal.Gui/Views/MessageBox.cs

@@ -25,27 +25,42 @@
 public static class MessageBox
 {
     /// <summary>
-    ///     The index of the selected button, or -1 if the user pressed ESC to close the dialog. This is useful for web
-    ///     based console where by default there is no SynchronizationContext or TaskScheduler.
+    ///     Defines the default border styling for <see cref="MessageBox"/>. Can be configured via
+    ///     <see cref="ConfigurationManager"/>.
     /// </summary>
-    public static int Clicked { get; private set; } = -1;
+    [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+    public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
 
     /// <summary>
-    ///     Defines the default border styling for <see cref="Dialog"/>. Can be configured via
+    ///     Defines the default minimum MessageBox width, as a percentage of the container width. Can be configured via
     ///     <see cref="ConfigurationManager"/>.
     /// </summary>
     [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
-    public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
+    public static int DefaultMinimumWidth { get; set; } = 60;
 
     /// <summary>
-    ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
-    ///     to the user.
+    ///     Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via
+    ///     <see cref="ConfigurationManager"/>.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
-    /// <param name="width">Width for the window.</param>
-    /// <param name="height">Height for the window.</param>
-    /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+    public static int DefaultMinimumHeight { get; set; } = 5;
+    /// <summary>
+    ///     The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox. This is useful for web
+    ///     based console where there is no SynchronizationContext or TaskScheduler.
+    /// </summary>
+    /// <remarks>
+    ///     Warning: This is a global variable and should be used with caution. It is not thread safe.
+    /// </remarks>
+    public static int Clicked { get; private set; } = -1;
+
+    /// <summary>
+    ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons.
+    /// </summary>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
+    /// <param name="width">Width for the MessageBox.</param>
+    /// <param name="height">Height for the MessageBox.</param>
+    /// <param name="title">Title for the MessageBox.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
     ///     Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
@@ -60,9 +75,9 @@ public static class MessageBox
     ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
     ///     to the user.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
     /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
     ///     The message box will be vertically and horizontally centered in the container and the size will be
@@ -71,14 +86,13 @@ public static class MessageBox
     public static int ErrorQuery (string title, string message, params string [] buttons) { return QueryFull (true, 0, 0, title, message, 0, true, buttons); }
 
     /// <summary>
-    ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
-    ///     to the user.
+    ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
-    /// <param name="width">Width for the window.</param>
-    /// <param name="height">Height for the window.</param>
-    /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
+    /// <param name="width">Width for the MessageBox.</param>
+    /// <param name="height">Height for the MessageBox.</param>
+    /// <param name="title">Title for the MessageBox.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="defaultButton">Index of the default button.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
@@ -101,9 +115,9 @@ public static class MessageBox
     ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
     ///     to the user.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
-    /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
+    /// <param name="title">Title for the MessageBox.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="defaultButton">Index of the default button.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
@@ -119,13 +133,13 @@ public static class MessageBox
     ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
     ///     to the user.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
     /// <param name="width">Width for the window.</param>
     /// <param name="height">Height for the window.</param>
     /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="defaultButton">Index of the default button.</param>
-    /// <param name="wrapMessagge">If wrap the message or not.</param>
+    /// <param name="wrapMessage">If wrap the message or not.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
     ///     Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
@@ -137,22 +151,22 @@ public static class MessageBox
         string title,
         string message,
         int defaultButton = 0,
-        bool wrapMessagge = true,
+        bool wrapMessage = true,
         params string [] buttons
     )
     {
-        return QueryFull (true, width, height, title, message, defaultButton, wrapMessagge, buttons);
+        return QueryFull (true, width, height, title, message, defaultButton, wrapMessage, buttons);
     }
 
     /// <summary>
     ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
     ///     to the user.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
     /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="defaultButton">Index of the default button.</param>
-    /// <param name="wrapMessagge">If wrap the message or not.</param>
+    /// <param name="wrapMessage">If wrap the message or not. The default is <see langword="true"/></param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
     ///     The message box will be vertically and horizontally centered in the container and the size will be
@@ -162,26 +176,25 @@ public static class MessageBox
         string title,
         string message,
         int defaultButton = 0,
-        bool wrapMessagge = true,
+        bool wrapMessage = true,
         params string [] buttons
     )
     {
-        return QueryFull (true, 0, 0, title, message, defaultButton, wrapMessagge, buttons);
+        return QueryFull (true, 0, 0, title, message, defaultButton, wrapMessage, buttons);
     }
 
     /// <summary>
-    ///     Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
-    ///     to the user.
+    ///     Presents a <see cref="MessageBox"/> with the specified title and message and a list of buttons.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
-    /// <param name="width">Width for the window.</param>
-    /// <param name="height">Height for the window.</param>
-    /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
+    /// <param name="width">Width for the MessageBox.</param>
+    /// <param name="height">Height for the MessageBox.</param>
+    /// <param name="title">Title for the MessageBox.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
-    ///     Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
-    ///     contents.
+    ///     Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
+    ///     the contents.
     /// </remarks>
     public static int Query (int width, int height, string title, string message, params string [] buttons)
     {
@@ -189,33 +202,43 @@ public static class MessageBox
     }
 
     /// <summary>
-    ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
-    ///     to the user.
+    ///     Presents a <see cref="MessageBox"/> with the specified title and message and a list of buttons.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
-    /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
+    /// <param name="title">Title for the MessageBox.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
+    /// <para>
     ///     The message box will be vertically and horizontally centered in the container and the size will be
-    ///     automatically determined from the size of the message and buttons.
+    ///     automatically determined from the size of the title, message. and buttons.
+    /// </para>
+    /// <para>
+    ///     Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
+    ///     the contents.
+    /// </para>
     /// </remarks>
     public static int Query (string title, string message, params string [] buttons) { return QueryFull (false, 0, 0, title, message, 0, true, buttons); }
 
     /// <summary>
-    ///     Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
-    ///     to the user.
+    ///     Presents a <see cref="MessageBox"/> with the specified title and message and a list of buttons.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
     /// <param name="width">Width for the window.</param>
     /// <param name="height">Height for the window.</param>
-    /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <param name="title">Title for the MessageBox.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="defaultButton">Index of the default button.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
-    ///     Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
-    ///     contents.
+    /// <para>
+    ///     The message box will be vertically and horizontally centered in the container and the size will be
+    ///     automatically determined from the size of the title, message. and buttons.
+    /// </para>
+    /// <para>
+    ///     Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
+    ///     the contents.
+    /// </para>
     /// </remarks>
     public static int Query (
         int width,
@@ -230,12 +253,11 @@ public static class MessageBox
     }
 
     /// <summary>
-    ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
-    ///     to the user.
+    ///     Presents a <see cref="MessageBox"/> with the specified title and message and a list of buttons.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
-    /// <param name="title">Title for the query.</param>
-    /// <param name="message">Message to display, might contain multiple lines.</param>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
+    /// <param name="title">Title for the MessageBox.</param>
+    /// <param name="message">Message to display; might contain multiple lines. The message will be word=wrapped by default.</param>
     /// <param name="defaultButton">Index of the default button.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
@@ -248,16 +270,16 @@ public static class MessageBox
     }
 
     /// <summary>
-    ///     Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
+    ///     Presents a <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
     ///     to the user.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
     /// <param name="width">Width for the window.</param>
     /// <param name="height">Height for the window.</param>
     /// <param name="title">Title for the query.</param>
     /// <param name="message">Message to display, might contain multiple lines.</param>
     /// <param name="defaultButton">Index of the default button.</param>
-    /// <param name="wrapMessagge">If wrap the message or not.</param>
+    /// <param name="wrapMessage">If wrap the message or not.</param>
     /// <param name="buttons">Array of buttons to add.</param>
     /// <remarks>
     ///     Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
@@ -269,27 +291,23 @@ public static class MessageBox
         string title,
         string message,
         int defaultButton = 0,
-        bool wrapMessagge = true,
+        bool wrapMessage = true,
         params string [] buttons
     )
     {
-        return QueryFull (false, width, height, title, message, defaultButton, wrapMessagge, buttons);
+        return QueryFull (false, width, height, title, message, defaultButton, wrapMessage, buttons);
     }
 
     /// <summary>
-    ///     Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
+    ///     Presents a <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
     ///     to the user.
     /// </summary>
-    /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
+    /// <returns>The index of the selected button, or -1 if the user pressed <see cref="Application.QuitKey"/> to close the MessageBox.</returns>
     /// <param name="title">Title for the query.</param>
     /// <param name="message">Message to display, might contain multiple lines.</param>
     /// <param name="defaultButton">Index of the default button.</param>
     /// <param name="wrapMessage">If wrap the message or not.</param>
     /// <param name="buttons">Array of buttons to add.</param>
-    /// <remarks>
-    ///     The message box will be vertically and horizontally centered in the container and the size will be
-    ///     automatically determined from the size of the message and buttons.
-    /// </remarks>
     public static int Query (
         string title,
         string message,
@@ -325,7 +343,10 @@ public static class MessageBox
 
             foreach (string s in buttons)
             {
-                var b = new Button { Text = s };
+                var b = new Button
+                {
+                    Text = s,
+                };
 
                 if (count == defaultButton)
                 {
@@ -337,15 +358,15 @@ public static class MessageBox
             }
         }
 
-        Dialog d;
-
-        d = new Dialog
+        var d = new Dialog
         {
-            Buttons = buttonList.ToArray (),
             Title = title,
+            Buttons = buttonList.ToArray (),
+            ButtonAlignment = Alignment.Center,
+            ButtonAlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems,
             BorderStyle = DefaultBorderStyle,
-            Width = Dim.Auto (DimAutoStyle.Content, minimumContentDim: Dim.Percent(60)),
-            Height = Dim.Auto (DimAutoStyle.Content),
+            Width = Dim.Auto (DimAutoStyle.Content, minimumContentDim: 1, Dim.Percent (90)),
+            Height = Dim.Auto (DimAutoStyle.Content, minimumContentDim: 1, Dim.Percent (90)),
         };
 
         if (width != 0)
@@ -358,22 +379,18 @@ public static class MessageBox
             d.Height = height;
         }
 
-        if (useErrorColors)
-        {
-            d.ColorScheme = Colors.ColorSchemes ["Error"];
-        }
-        else
-        {
-            d.ColorScheme = Colors.ColorSchemes ["Dialog"];
-        }
+        d.ColorScheme = useErrorColors ? Colors.ColorSchemes ["Error"] : Colors.ColorSchemes ["Dialog"];
 
         var messageLabel = new Label
         {
+            HotKeySpecifier = new Rune ('\xFFFF'),
+            Width = Dim.Auto (DimAutoStyle.Text),
+            Height = Dim.Auto (DimAutoStyle.Text),
             Text = message,
-            TextAlignment = TextAlignment.Centered,
+            TextAlignment = Alignment.Center,
             X = Pos.Center (),
             Y = 0,
-           // ColorScheme = Colors.ColorSchemes ["Error"]
+            //ColorScheme = Colors.ColorSchemes ["Error"],
         };
 
         messageLabel.TextFormatter.WordWrap = wrapMessage;
@@ -381,15 +398,16 @@ public static class MessageBox
 
         if (wrapMessage)
         {
+            int buttonHeight = buttonList.Count > 0 ? buttonList [0].Frame.Height : 0;
+
             messageLabel.Width = Dim.Fill ();
-            messageLabel.Height = Dim.Fill (1);
-            int GetWrapSize ()
+            messageLabel.Height = Dim.Func (() => GetWrapSize ().Height);
+            Size GetWrapSize ()
             {
                 // A bit of a hack to get the height of the wrapped text.
-                messageLabel.TextFormatter.Size = new (d.ContentSize.Width, 1000);
-                return messageLabel.TextFormatter.FormatAndGetSize ().Height;
+                messageLabel.TextFormatter.Size = d.GetContentSize () with { Height = 1000 };
+                return messageLabel.TextFormatter.FormatAndGetSize ();
             }
-            d.Height = Dim.Auto (DimAutoStyle.Content, minimumContentDim: Dim.Func (GetWrapSize) + 1);
         }
 
         d.Add (messageLabel);

+ 19 - 0
Terminal.Gui/Views/OrientationEventArgs.cs

@@ -0,0 +1,19 @@
+namespace Terminal.Gui;
+
+/// <summary><see cref="EventArgs"/> for <see cref="Orientation"/> events.</summary>
+public class OrientationEventArgs : EventArgs
+{
+    /// <summary>Constructs a new instance.</summary>
+    /// <param name="orientation">the new orientation</param>
+    public OrientationEventArgs (Orientation orientation)
+    {
+        Orientation = orientation;
+        Cancel = false;
+    }
+
+    /// <summary>If set to true, the orientation change operation will be canceled, if applicable.</summary>
+    public bool Cancel { get; set; }
+
+    /// <summary>The new orientation.</summary>
+    public Orientation Orientation { get; set; }
+}

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

@@ -175,7 +175,7 @@ public class ProgressBar : View
 
         if (ProgressBarFormat != ProgressBarFormat.Simple && !_isActivity)
         {
-            var tf = new TextFormatter { Alignment = TextAlignment.Centered, Text = Text, AutoSize = true };
+            var tf = new TextFormatter { Alignment = Alignment.Center, Text = Text, AutoSize = true };
             var attr = new Attribute (ColorScheme.HotNormal.Foreground, ColorScheme.HotNormal.Background);
 
             if (_fraction > .5)

+ 190 - 199
Terminal.Gui/Views/RadioGroup.cs

@@ -11,8 +11,7 @@ public class RadioGroup : View
     private int _selected;
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Computed"/>
-    ///     layout.
+    ///     Initializes a new instance of the <see cref="RadioGroup"/> class.
     /// </summary>
     public RadioGroup ()
     {
@@ -66,16 +65,32 @@ public class RadioGroup : View
                     Command.Accept,
                     () =>
                     {
-                        SelectItem ();
+                        SelectedItem = _cursor;
+
                         return !OnAccept ();
                     }
                    );
 
+        AddCommand (
+                    Command.HotKey,
+                    ctx =>
+                    {
+                        SetFocus ();
+                        if (ctx.KeyBinding?.Context is { } && (int)ctx.KeyBinding?.Context! < _radioLabels.Count)
+                        {
+                            SelectedItem = (int)ctx.KeyBinding?.Context!;
+
+                            return !OnAccept();
+                        }
+
+                        return true;
+                    });
+
         SetupKeyBindings ();
 
         LayoutStarted += RadioGroup_LayoutStarted;
 
-        HighlightStyle = Gui.HighlightStyle.PressedOutside | Gui.HighlightStyle.Pressed;
+        HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
 
         MouseClick += RadioGroup_MouseClick;
     }
@@ -85,6 +100,7 @@ public class RadioGroup : View
     private void SetupKeyBindings ()
     {
         KeyBindings.Clear ();
+
         // Default keybindings for this view
         if (Orientation == Orientation.Vertical)
         {
@@ -96,6 +112,7 @@ public class RadioGroup : View
             KeyBindings.Add (Key.CursorLeft, Command.LineUp);
             KeyBindings.Add (Key.CursorRight, Command.LineDown);
         }
+
         KeyBindings.Add (Key.Home, Command.TopHome);
         KeyBindings.Add (Key.End, Command.BottomEnd);
         KeyBindings.Add (Key.Space, Command.Accept);
@@ -180,11 +197,13 @@ public class RadioGroup : View
             int prevCount = _radioLabels.Count;
             _radioLabels = value.ToList ();
 
-            foreach (string label in _radioLabels)
+            for (var index = 0; index < _radioLabels.Count; index++)
             {
+                string label = _radioLabels [index];
+
                 if (TextFormatter.FindHotKey (label, HotKeySpecifier, out _, out Key hotKey))
                 {
-                    AddKeyBindingsForHotKey (Key.Empty, hotKey);
+                    AddKeyBindingsForHotKey (Key.Empty, hotKey, index);
                 }
             }
 
@@ -193,7 +212,7 @@ public class RadioGroup : View
         }
     }
 
-    /// <inheritdoc />
+    /// <inheritdoc/>
     public override string Text
     {
         get
@@ -202,6 +221,7 @@ public class RadioGroup : View
             {
                 return string.Empty;
             }
+
             // Return labels as a CSV string
             return string.Join (",", _radioLabels);
         }
@@ -221,73 +241,51 @@ public class RadioGroup : View
     /// <summary>The currently selected item from the list of radio labels</summary>
     /// <value>The selected.</value>
     public int SelectedItem
-{
-    get => _selected;
-    set
     {
-        OnSelectedItemChanged (value, SelectedItem);
-        _cursor = Math.Max (_selected, 0);
-        SetNeedsDisplay ();
+        get => _selected;
+        set
+        {
+            OnSelectedItemChanged (value, SelectedItem);
+            _cursor = Math.Max (_selected, 0);
+            SetNeedsDisplay ();
+        }
     }
-}
 
-/// <inheritdoc/>
-public override void OnDrawContent (Rectangle viewport)
-{
-    base.OnDrawContent (viewport);
+    /// <inheritdoc/>
+    public override void OnDrawContent (Rectangle viewport)
+    {
+        base.OnDrawContent (viewport);
 
-    Driver.SetAttribute (GetNormalColor ());
+        Driver.SetAttribute (GetNormalColor ());
 
-    for (var i = 0; i < _radioLabels.Count; i++)
-    {
-        switch (Orientation)
+        for (var i = 0; i < _radioLabels.Count; i++)
         {
-            case Orientation.Vertical:
-                Move (0, i);
+            switch (Orientation)
+            {
+                case Orientation.Vertical:
+                    Move (0, i);
 
-                break;
-            case Orientation.Horizontal:
-                Move (_horizontal [i].pos, 0);
+                    break;
+                case Orientation.Horizontal:
+                    Move (_horizontal [i].pos, 0);
 
-                break;
-        }
-
-        string rl = _radioLabels [i];
-        Driver.SetAttribute (GetNormalColor ());
-        Driver.AddStr ($"{(i == _selected ? Glyphs.Selected : Glyphs.UnSelected)} ");
-        TextFormatter.FindHotKey (rl, HotKeySpecifier, out int hotPos, out Key hotKey);
+                    break;
+            }
 
-        if (hotPos != -1 && hotKey != Key.Empty)
-        {
-            Rune [] rlRunes = rl.ToRunes ();
+            string rl = _radioLabels [i];
+            Driver.SetAttribute (GetNormalColor ());
+            Driver.AddStr ($"{(i == _selected ? Glyphs.Selected : Glyphs.UnSelected)} ");
+            TextFormatter.FindHotKey (rl, HotKeySpecifier, out int hotPos, out Key hotKey);
 
-            for (var j = 0; j < rlRunes.Length; j++)
+            if (hotPos != -1 && hotKey != Key.Empty)
             {
-                Rune rune = rlRunes [j];
+                Rune [] rlRunes = rl.ToRunes ();
 
-                if (j == hotPos && i == _cursor)
-                {
-                    Application.Driver.SetAttribute (
-                                                     HasFocus
-                                                         ? ColorScheme.HotFocus
-                                                         : GetHotNormalColor ()
-                                                    );
-                }
-                else if (j == hotPos && i != _cursor)
+                for (var j = 0; j < rlRunes.Length; j++)
                 {
-                    Application.Driver.SetAttribute (GetHotNormalColor ());
-                }
-                else if (HasFocus && i == _cursor)
-                {
-                    Application.Driver.SetAttribute (ColorScheme.Focus);
-                }
+                    Rune rune = rlRunes [j];
 
-                if (rune == HotKeySpecifier && j + 1 < rlRunes.Length)
-                {
-                    j++;
-                    rune = rlRunes [j];
-
-                    if (i == _cursor)
+                    if (j == hotPos && i == _cursor)
                     {
                         Application.Driver.SetAttribute (
                                                          HasFocus
@@ -295,184 +293,177 @@ public override void OnDrawContent (Rectangle viewport)
                                                              : GetHotNormalColor ()
                                                         );
                     }
-                    else if (i != _cursor)
+                    else if (j == hotPos && i != _cursor)
                     {
                         Application.Driver.SetAttribute (GetHotNormalColor ());
                     }
-                }
+                    else if (HasFocus && i == _cursor)
+                    {
+                        Application.Driver.SetAttribute (ColorScheme.Focus);
+                    }
+
+                    if (rune == HotKeySpecifier && j + 1 < rlRunes.Length)
+                    {
+                        j++;
+                        rune = rlRunes [j];
+
+                        if (i == _cursor)
+                        {
+                            Application.Driver.SetAttribute (
+                                                             HasFocus
+                                                                 ? ColorScheme.HotFocus
+                                                                 : GetHotNormalColor ()
+                                                            );
+                        }
+                        else if (i != _cursor)
+                        {
+                            Application.Driver.SetAttribute (GetHotNormalColor ());
+                        }
+                    }
 
-                Application.Driver.AddRune (rune);
-                Driver.SetAttribute (GetNormalColor ());
+                    Application.Driver.AddRune (rune);
+                    Driver.SetAttribute (GetNormalColor ());
+                }
+            }
+            else
+            {
+                DrawHotString (rl, HasFocus && i == _cursor, ColorScheme);
             }
-        }
-        else
-        {
-            DrawHotString (rl, HasFocus && i == _cursor, ColorScheme);
         }
     }
-}
 
-/// <inheritdoc/>
-public override bool? OnInvokingKeyBindings (Key keyEvent)
-{
-    // This is a bit of a hack. We want to handle the key bindings for the radio group but
-    // InvokeKeyBindings doesn't pass any context so we can't tell if the key binding is for
-    // the radio group or for one of the radio buttons. So before we call the base class
-    // we set SelectedItem appropriately.
-
-    Key key = keyEvent;
-
-    if (KeyBindings.TryGet (key, out _))
+    /// <summary>Called when the view orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
+    /// <param name="newOrientation"></param>
+    /// <returns>True of the event was cancelled.</returns>
+    public virtual bool OnOrientationChanged (Orientation newOrientation)
     {
-        // Search RadioLabels 
-        for (var i = 0; i < _radioLabels.Count; i++)
+        var args = new OrientationEventArgs (newOrientation);
+        OrientationChanged?.Invoke (this, args);
+
+        if (!args.Cancel)
         {
-            if (TextFormatter.FindHotKey (
-                                          _radioLabels [i],
-                                          HotKeySpecifier,
-                                          out _,
-                                          out Key hotKey,
-                                          true
-                                         )
-                && key.NoAlt.NoCtrl.NoShift == hotKey)
-            {
-                SelectedItem = i;
-                break;
-            }
+            _orientation = newOrientation;
+            SetupKeyBindings ();
+            SetContentSize ();
         }
-    }
-
-    return base.OnInvokingKeyBindings (keyEvent);
-}
 
-/// <summary>Called when the view orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
-/// <param name="newOrientation"></param>
-/// <returns>True of the event was cancelled.</returns>
-public virtual bool OnOrientationChanged (Orientation newOrientation)
-{
-    var args = new OrientationEventArgs (newOrientation);
-    OrientationChanged?.Invoke (this, args);
+        return args.Cancel;
+    }
 
-    if (!args.Cancel)
+    // TODO: This should be cancelable
+    /// <summary>Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.</summary>
+    /// <param name="selectedItem"></param>
+    /// <param name="previousSelectedItem"></param>
+    public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
     {
-        _orientation = newOrientation;
-        SetupKeyBindings ();
-        SetContentSize ();
+        _selected = selectedItem;
+        SelectedItemChanged?.Invoke (this, new (selectedItem, previousSelectedItem));
     }
 
-    return args.Cancel;
-}
+    /// <summary>
+    ///     Fired when the view orientation has changed. Can be cancelled by setting
+    ///     <see cref="OrientationEventArgs.Cancel"/> to true.
+    /// </summary>
+    public event EventHandler<OrientationEventArgs> OrientationChanged;
 
-// TODO: This should be cancelable
-/// <summary>Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.</summary>
-/// <param name="selectedItem"></param>
-/// <param name="previousSelectedItem"></param>
-public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
-{
-    _selected = selectedItem;
-    SelectedItemChanged?.Invoke (this, new SelectedItemChangedArgs (selectedItem, previousSelectedItem));
-}
+    /// <inheritdoc/>
+    public override Point? PositionCursor ()
+    {
+        var x = 0;
+        var y = 0;
 
-/// <summary>
-///     Fired when the view orientation has changed. Can be cancelled by setting
-///     <see cref="OrientationEventArgs.Cancel"/> to true.
-/// </summary>
-public event EventHandler<OrientationEventArgs> OrientationChanged;
+        switch (Orientation)
+        {
+            case Orientation.Vertical:
+                y = _cursor;
 
-/// <inheritdoc/>
-public override Point? PositionCursor ()
-{
-    int x = 0;
-    int y = 0;
-    switch (Orientation)
-    {
-        case Orientation.Vertical:
-            y = _cursor;
+                break;
+            case Orientation.Horizontal:
+                x = _horizontal [_cursor].pos;
 
-            break;
-        case Orientation.Horizontal:
-            x = _horizontal [_cursor].pos;
+                break;
 
-            break;
+            default:
+                return null;
+        }
 
-        default:
-            return null;
-    }
+        Move (x, y);
 
-    Move (x, y);
-    return null; // Don't show the cursor
-}
+        return null; // Don't show the cursor
+    }
 
-/// <summary>Allow to invoke the <see cref="SelectedItemChanged"/> after their creation.</summary>
-public void Refresh () { OnSelectedItemChanged (_selected, -1); }
+    /// <summary>Allow to invoke the <see cref="SelectedItemChanged"/> after their creation.</summary>
+    public void Refresh () { OnSelectedItemChanged (_selected, -1); }
 
-// TODO: This should use StateEventArgs<int> and should be cancelable.
-/// <summary>Invoked when the selected radio label has changed.</summary>
-public event EventHandler<SelectedItemChangedArgs> SelectedItemChanged;
+    // TODO: This should use StateEventArgs<int> and should be cancelable.
+    /// <summary>Invoked when the selected radio label has changed.</summary>
+    public event EventHandler<SelectedItemChangedArgs> SelectedItemChanged;
 
-private void MoveDownRight ()
-{
-    if (_cursor + 1 < _radioLabels.Count)
+    private void MoveDownRight ()
     {
-        _cursor++;
-        SetNeedsDisplay ();
-    }
-    else if (_cursor > 0)
-    {
-        _cursor = 0;
-        SetNeedsDisplay ();
+        if (_cursor + 1 < _radioLabels.Count)
+        {
+            _cursor++;
+            SetNeedsDisplay ();
+        }
+        else if (_cursor > 0)
+        {
+            _cursor = 0;
+            SetNeedsDisplay ();
+        }
     }
-}
 
-private void MoveEnd () { _cursor = Math.Max (_radioLabels.Count - 1, 0); }
-private void MoveHome () { _cursor = 0; }
+    private void MoveEnd () { _cursor = Math.Max (_radioLabels.Count - 1, 0); }
+    private void MoveHome () { _cursor = 0; }
 
-private void MoveUpLeft ()
-{
-    if (_cursor > 0)
+    private void MoveUpLeft ()
     {
-        _cursor--;
-        SetNeedsDisplay ();
-    }
-    else if (_radioLabels.Count - 1 > 0)
-    {
-        _cursor = _radioLabels.Count - 1;
-        SetNeedsDisplay ();
+        if (_cursor > 0)
+        {
+            _cursor--;
+            SetNeedsDisplay ();
+        }
+        else if (_radioLabels.Count - 1 > 0)
+        {
+            _cursor = _radioLabels.Count - 1;
+            SetNeedsDisplay ();
+        }
     }
-}
 
-private void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetContentSize (); }
-private void SelectItem () { SelectedItem = _cursor; }
+    private void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetContentSize (); }
 
-private void SetContentSize ()
-{
-    switch (_orientation)
+    private void SetContentSize ()
     {
-        case Orientation.Vertical:
-            var width = 0;
+        switch (_orientation)
+        {
+            case Orientation.Vertical:
+                var width = 0;
 
-            foreach (string s in _radioLabels)
-            {
-                width = Math.Max (s.GetColumns () + 2, width);
-            }
+                foreach (string s in _radioLabels)
+                {
+                    width = Math.Max (s.GetColumns () + 2, width);
+                }
 
-            SetContentSize (new (width, _radioLabels.Count));
-            break;
+                SetContentSize (new (width, _radioLabels.Count));
 
-        case Orientation.Horizontal:
-            _horizontal = new List<(int pos, int length)> ();
-            var start = 0;
-            var length = 0;
+                break;
 
-            for (var i = 0; i < _radioLabels.Count; i++)
-            {
-                start += length;
+            case Orientation.Horizontal:
+                _horizontal = new ();
+                var start = 0;
+                var length = 0;
 
-                length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0);
-                _horizontal.Add ((start, length));
-            }
-            SetContentSize (new (_horizontal.Sum (item => item.length), 1));
-            break;
+                for (var i = 0; i < _radioLabels.Count; i++)
+                {
+                    start += length;
+
+                    length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0);
+                    _horizontal.Add ((start, length));
+                }
+
+                SetContentSize (new (_horizontal.Sum (item => item.length), 1));
+
+                break;
+        }
     }
 }
-}

+ 2 - 4
Terminal.Gui/Views/ScrollBarView.cs

@@ -33,8 +33,7 @@ public class ScrollBarView : View
     private bool _vertical;
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using
-    ///     <see cref="LayoutStyle.Computed"/> layout.
+    ///     Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class.
     /// </summary>
     public ScrollBarView ()
     {
@@ -46,8 +45,7 @@ public class ScrollBarView : View
     }
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using
-    ///     <see cref="LayoutStyle.Computed"/> layout.
+    ///     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>

+ 20 - 21
Terminal.Gui/Views/ScrollView.cs

@@ -21,7 +21,7 @@ namespace Terminal.Gui;
 ///     <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.ContentSize"/>.
+///         <see cref="View.GetContentSize ()"/>.
 ///     </para>
 ///     <para>Use the</para>
 /// </remarks>
@@ -38,8 +38,7 @@ public class ScrollView : View
     private bool _showVerticalScrollIndicator;
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Gui.ScrollView"/> class using <see cref="LayoutStyle.Computed"/>
-    ///     positioning.
+    ///     Initializes a new instance of the <see cref="Gui.ScrollView"/> class.
     /// </summary>
     public ScrollView ()
     {
@@ -88,10 +87,10 @@ public class ScrollView : View
         AddCommand (Command.PageDown, () => ScrollDown (Viewport.Height));
         AddCommand (Command.PageLeft, () => ScrollLeft (Viewport.Width));
         AddCommand (Command.PageRight, () => ScrollRight (Viewport.Width));
-        AddCommand (Command.TopHome, () => ScrollUp (ContentSize.Height));
-        AddCommand (Command.BottomEnd, () => ScrollDown (ContentSize.Height));
-        AddCommand (Command.LeftHome, () => ScrollLeft (ContentSize.Width));
-        AddCommand (Command.RightEnd, () => ScrollRight (ContentSize.Width));
+        AddCommand (Command.TopHome, () => ScrollUp (GetContentSize ().Height));
+        AddCommand (Command.BottomEnd, () => ScrollDown (GetContentSize ().Height));
+        AddCommand (Command.LeftHome, () => ScrollLeft (GetContentSize ().Width));
+        AddCommand (Command.RightEnd, () => ScrollRight (GetContentSize ().Width));
 
         // Default keybindings for this view
         KeyBindings.Add (Key.CursorUp, Command.ScrollUp);
@@ -127,7 +126,7 @@ public class ScrollView : View
                            }
 
                            SetContentOffset (_contentOffset);
-                           _contentView.Frame = new Rectangle (ContentOffset, ContentSize);
+                           _contentView.Frame = new Rectangle (ContentOffset, GetContentSize ());
 
                            // PERF: How about calls to Point.Offset instead?
                            _vertical.ChangedPosition += delegate { ContentOffset = new Point (ContentOffset.X, _vertical.Position); };
@@ -220,12 +219,12 @@ public class ScrollView : View
     //    get => ContentSize;
     //    set
     //    {
-    //        if (ContentSize != value)
+    //        if (GetContentSize () != value)
     //        {
     //            ContentSize = value;
     //            _contentView.Frame = new Rectangle (_contentOffset, value);
-    //            _vertical.Size = ContentSize.Height;
-    //            _horizontal.Size = ContentSize.Width;
+    //            _vertical.Size = GetContentSize ().Height;
+    //            _horizontal.Size = GetContentSize ().Width;
     //            SetNeedsDisplay ();
     //        }
     //    }
@@ -244,26 +243,26 @@ public class ScrollView : View
                 _horizontal.OtherScrollBarView.KeepContentAlwaysInViewport = value;
                 Point p = default;
 
-                if (value && -_contentOffset.X + Viewport.Width > ContentSize.Width)
+                if (value && -_contentOffset.X + Viewport.Width > GetContentSize ().Width)
                 {
                     p = new Point (
-                                   ContentSize.Width - Viewport.Width + (_showVerticalScrollIndicator ? 1 : 0),
+                                   GetContentSize ().Width - Viewport.Width + (_showVerticalScrollIndicator ? 1 : 0),
                                    -_contentOffset.Y
                                   );
                 }
 
-                if (value && -_contentOffset.Y + Viewport.Height > ContentSize.Height)
+                if (value && -_contentOffset.Y + Viewport.Height > GetContentSize ().Height)
                 {
                     if (p == default (Point))
                     {
                         p = new Point (
                                        -_contentOffset.X,
-                                       ContentSize.Height - Viewport.Height + (_showHorizontalScrollIndicator ? 1 : 0)
+                                       GetContentSize ().Height - Viewport.Height + (_showHorizontalScrollIndicator ? 1 : 0)
                                       );
                     }
                     else
                     {
-                        p.Y = ContentSize.Height - Viewport.Height + (_showHorizontalScrollIndicator ? 1 : 0);
+                        p.Y = GetContentSize ().Height - Viewport.Height + (_showHorizontalScrollIndicator ? 1 : 0);
                     }
                 }
 
@@ -607,7 +606,7 @@ public class ScrollView : View
     {
         // 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, ContentSize);
+        _contentView.Frame = new Rectangle (_contentOffset, GetContentSize ());
         int p = Math.Max (0, -_contentOffset.Y);
 
         if (_vertical.Position != p)
@@ -638,7 +637,7 @@ public class ScrollView : View
         bool v = false, h = false;
         var p = false;
 
-        if (ContentSize is { } && (Viewport.Height == 0 || Viewport.Height > ContentSize.Height))
+        if (GetContentSize () is { } && (Viewport.Height == 0 || Viewport.Height > GetContentSize ().Height))
         {
             if (ShowVerticalScrollIndicator)
             {
@@ -647,7 +646,7 @@ public class ScrollView : View
 
             v = false;
         }
-        else if (ContentSize is { } && Viewport.Height > 0 && Viewport.Height == ContentSize.Height)
+        else if (GetContentSize () is { } && Viewport.Height > 0 && Viewport.Height == GetContentSize ().Height)
         {
             p = true;
         }
@@ -661,7 +660,7 @@ public class ScrollView : View
             v = true;
         }
 
-        if (ContentSize is { } && (Viewport.Width == 0 || Viewport.Width > ContentSize.Width))
+        if (GetContentSize () is { } && (Viewport.Width == 0 || Viewport.Width > GetContentSize ().Width))
         {
             if (ShowHorizontalScrollIndicator)
             {
@@ -670,7 +669,7 @@ public class ScrollView : View
 
             h = false;
         }
-        else if (ContentSize is { } && Viewport.Width > 0 && Viewport.Width == ContentSize.Width && p)
+        else if (GetContentSize () is { } && Viewport.Width > 0 && Viewport.Width == GetContentSize ().Width && p)
         {
             if (ShowHorizontalScrollIndicator)
             {

+ 8 - 219
Terminal.Gui/Views/Slider.cs

@@ -1,210 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary><see cref="EventArgs"/> for <see cref="Slider{T}"/> <see cref="SliderOption{T}"/> events.</summary>
-public class SliderOptionEventArgs : EventArgs
-{
-    /// <summary>Initializes a new instance of <see cref="SliderOptionEventArgs"/></summary>
-    /// <param name="isSet"> indicates whether the option is set</param>
-    public SliderOptionEventArgs (bool isSet) { IsSet = isSet; }
-
-    /// <summary>Gets whether the option is set or not.</summary>
-    public bool IsSet { get; }
-}
-
-/// <summary>Represents an option in a <see cref="Slider{T}"/> .</summary>
-/// <typeparam name="T">Data type of the option.</typeparam>
-public class SliderOption<T>
-{
-    /// <summary>Creates a new empty instance of the <see cref="SliderOption{T}"/> class.</summary>
-    public SliderOption () { }
-
-    /// <summary>Creates a new instance of the <see cref="SliderOption{T}"/> class with values for each property.</summary>
-    public SliderOption (string legend, Rune legendAbbr, T data)
-    {
-        Legend = legend;
-        LegendAbbr = legendAbbr;
-        Data = data;
-    }
-
-    /// <summary>Event fired when the an option has changed.</summary>
-    public event EventHandler<SliderOptionEventArgs> Changed;
-
-    /// <summary>Custom data of the option.</summary>
-    public T Data { get; set; }
-
-    /// <summary>Legend of the option.</summary>
-    public string Legend { get; set; }
-
-    /// <summary>
-    ///     Abbreviation of the Legend. When the <see cref="Slider{T}.MinimumInnerSpacing"/> too small to fit
-    ///     <see cref="Legend"/>.
-    /// </summary>
-    public Rune LegendAbbr { get; set; }
-
-    /// <summary>Event Raised when this option is set.</summary>
-    public event EventHandler<SliderOptionEventArgs> Set;
-
-    /// <summary>Creates a human-readable string that represents this <see cref="SliderOption{T}"/>.</summary>
-    public override string ToString () { return "{Legend=" + Legend + ", LegendAbbr=" + LegendAbbr + ", Data=" + Data + "}"; }
-
-    /// <summary>Event Raised when this option is unset.</summary>
-    public event EventHandler<SliderOptionEventArgs> UnSet;
-
-    /// <summary>To Raise the <see cref="Changed"/> event from the Slider.</summary>
-    internal void OnChanged (bool isSet) { Changed?.Invoke (this, new (isSet)); }
-
-    /// <summary>To Raise the <see cref="Set"/> event from the Slider.</summary>
-    internal void OnSet () { Set?.Invoke (this, new (true)); }
-
-    /// <summary>To Raise the <see cref="UnSet"/> event from the Slider.</summary>
-    internal void OnUnSet () { UnSet?.Invoke (this, new (false)); }
-}
-
-/// <summary><see cref="Slider{T}"/>  Types</summary>
-public enum SliderType
-{
-    /// <summary>
-    ///     <code>
-    /// ├─┼─┼─┼─┼─█─┼─┼─┼─┼─┼─┼─┤
-    /// </code>
-    /// </summary>
-    Single,
-
-    /// <summary>
-    ///     <code>
-    /// ├─┼─█─┼─┼─█─┼─┼─┼─┼─█─┼─┤
-    /// </code>
-    /// </summary>
-    Multiple,
-
-    /// <summary>
-    ///     <code>
-    /// ├▒▒▒▒▒▒▒▒▒█─┼─┼─┼─┼─┼─┼─┤
-    /// </code>
-    /// </summary>
-    LeftRange,
-
-    /// <summary>
-    ///     <code>
-    /// ├─┼─┼─┼─┼─█▒▒▒▒▒▒▒▒▒▒▒▒▒┤
-    /// </code>
-    /// </summary>
-    RightRange,
-
-    /// <summary>
-    ///     <code>
-    /// ├─┼─┼─┼─┼─█▒▒▒▒▒▒▒█─┼─┼─┤
-    /// </code>
-    /// </summary>
-    Range
-}
-
-/// <summary><see cref="Slider{T}"/> Legend Style</summary>
-public class SliderAttributes
-{
-    /// <summary>Attribute for the Legends Container.</summary>
-    public Attribute? EmptyAttribute { get; set; }
-
-    /// <summary>Attribute for when the respective Option is NOT Set.</summary>
-    public Attribute? NormalAttribute { get; set; }
-
-    /// <summary>Attribute for when the respective Option is Set.</summary>
-    public Attribute? SetAttribute { get; set; }
-}
-
-/// <summary><see cref="Slider{T}"/> Style</summary>
-public class SliderStyle
-{
-    /// <summary>Constructs a new instance.</summary>
-    public SliderStyle () { LegendAttributes = new (); }
-
-    /// <summary>The glyph and the attribute to indicate mouse dragging.</summary>
-    public Cell DragChar { get; set; }
-
-    /// <summary>The glyph and the attribute used for empty spaces on the slider.</summary>
-    public Cell EmptyChar { get; set; }
-
-    /// <summary>The glyph and the attribute used for the end of ranges on the slider.</summary>
-    public Cell EndRangeChar { get; set; }
-
-    /// <summary>Legend attributes</summary>
-    public SliderAttributes LegendAttributes { get; set; }
-
-    /// <summary>The glyph and the attribute used for each option (tick) on the slider.</summary>
-    public Cell OptionChar { get; set; }
-
-    /// <summary>The glyph and the attribute used for filling in ranges on the slider.</summary>
-    public Cell RangeChar { get; set; }
-
-    /// <summary>The glyph and the attribute used for options (ticks) that are set on the slider.</summary>
-    public Cell SetChar { get; set; }
-
-    /// <summary>The glyph and the attribute used for spaces between options (ticks) on the slider.</summary>
-    public Cell SpaceChar { get; set; }
-
-    /// <summary>The glyph and the attribute used for the start of ranges on the slider.</summary>
-    public Cell StartRangeChar { get; set; }
-}
-
-/// <summary>All <see cref="Slider{T}"/> configuration are grouped in this class.</summary>
-internal class SliderConfiguration
-{
-    internal bool _allowEmpty;
-    internal int _endSpacing;
-    internal int _minInnerSpacing = 1;
-    internal int _cachedInnerSpacing; // Currently calculated
-    internal Orientation _legendsOrientation = Orientation.Horizontal;
-    internal bool _rangeAllowSingle;
-    internal bool _showEndSpacing;
-    internal bool _showLegends;
-    internal bool _showLegendsAbbr;
-    internal Orientation _sliderOrientation = Orientation.Horizontal;
-    internal int _startSpacing;
-    internal SliderType _type = SliderType.Single;
-    internal bool _useMinimumSize;
-}
-
-/// <summary><see cref="EventArgs"/> for <see cref="Slider{T}"/> events.</summary>
-public class SliderEventArgs<T> : EventArgs
-{
-    /// <summary>Initializes a new instance of <see cref="SliderEventArgs{T}"/></summary>
-    /// <param name="options">The current options.</param>
-    /// <param name="focused">Index of the option that is focused. -1 if no option has the focus.</param>
-    public SliderEventArgs (Dictionary<int, SliderOption<T>> options, int focused = -1)
-    {
-        Options = options;
-        Focused = focused;
-        Cancel = false;
-    }
-
-    /// <summary>If set to true, the focus operation will be canceled, if applicable.</summary>
-    public bool Cancel { get; set; }
-
-    /// <summary>Gets or sets the index of the option that is focused.</summary>
-    public int Focused { get; set; }
-
-    /// <summary>Gets/sets whether the option is set or not.</summary>
-    public Dictionary<int, SliderOption<T>> Options { get; set; }
-}
-
-/// <summary><see cref="EventArgs"/> for <see cref="Orientation"/> events.</summary>
-public class OrientationEventArgs : EventArgs
-{
-    /// <summary>Constructs a new instance.</summary>
-    /// <param name="orientation">the new orientation</param>
-    public OrientationEventArgs (Orientation orientation)
-    {
-        Orientation = orientation;
-        Cancel = false;
-    }
-
-    /// <summary>If set to true, the orientation change operation will be canceled, if applicable.</summary>
-    public bool Cancel { get; set; }
-
-    /// <summary>The new orientation.</summary>
-    public Orientation Orientation { get; set; }
-}
-
 /// <summary>Slider control.</summary>
 public class Slider : Slider<object>
 {
@@ -1002,7 +797,7 @@ public class Slider<T> : View
         }
     }
 
-    private string AlignText (string text, int width, TextAlignment textAlignment)
+    private string AlignText (string text, int width, Alignment alignment)
     {
         if (text is null)
         {
@@ -1019,20 +814,20 @@ public class Slider<T> : View
         string s2 = new (' ', w % 2);
 
         // Note: The formatter doesn't handle all of this ???
-        switch (textAlignment)
+        switch (alignment)
         {
-            case TextAlignment.Justified:
+            case Alignment.Fill:
                 return TextFormatter.Justify (text, width);
-            case TextAlignment.Left:
+            case Alignment.Start:
                 return text + s1 + s1 + s2;
-            case TextAlignment.Centered:
+            case Alignment.Center:
                 if (text.Length % 2 != 0)
                 {
                     return s1 + text + s1 + s2;
                 }
 
                 return s1 + s2 + text + s1;
-            case TextAlignment.Right:
+            case Alignment.End:
                 return s1 + s1 + s2 + text;
             default:
                 return text;
@@ -1139,12 +934,6 @@ public class Slider<T> : View
                             }
 
                             break;
-                        case SliderType.Single:
-                            break;
-                        case SliderType.Multiple:
-                            break;
-                        default:
-                            throw new ArgumentOutOfRangeException ();
                     }
                 }
 
@@ -1365,7 +1154,7 @@ public class Slider<T> : View
                     switch (_config._legendsOrientation)
                     {
                         case Orientation.Horizontal:
-                            text = AlignText (text, _config._cachedInnerSpacing + 1, TextAlignment.Centered);
+                            text = AlignText (text, _config._cachedInnerSpacing + 1, Alignment.Center);
 
                             break;
                         case Orientation.Vertical:
@@ -1383,7 +1172,7 @@ public class Slider<T> : View
 
                             break;
                         case Orientation.Vertical:
-                            text = AlignText (text, _config._cachedInnerSpacing + 1, TextAlignment.Centered);
+                            text = AlignText (text, _config._cachedInnerSpacing + 1, Alignment.Center);
 
                             break;
                     }

+ 14 - 0
Terminal.Gui/Views/SliderAttributes.cs

@@ -0,0 +1,14 @@
+namespace Terminal.Gui;
+
+/// <summary><see cref="Slider{T}"/> Legend Style</summary>
+public class SliderAttributes
+{
+    /// <summary>Attribute for the Legends Container.</summary>
+    public Attribute? EmptyAttribute { get; set; }
+
+    /// <summary>Attribute for when the respective Option is NOT Set.</summary>
+    public Attribute? NormalAttribute { get; set; }
+
+    /// <summary>Attribute for when the respective Option is Set.</summary>
+    public Attribute? SetAttribute { get; set; }
+}

+ 19 - 0
Terminal.Gui/Views/SliderConfiguration.cs

@@ -0,0 +1,19 @@
+namespace Terminal.Gui;
+
+/// <summary>All <see cref="Slider{T}"/> configuration are grouped in this class.</summary>
+internal class SliderConfiguration
+{
+    internal bool _allowEmpty;
+    internal int _endSpacing;
+    internal int _minInnerSpacing = 1;
+    internal int _cachedInnerSpacing; // Currently calculated
+    internal Orientation _legendsOrientation = Orientation.Horizontal;
+    internal bool _rangeAllowSingle;
+    internal bool _showEndSpacing;
+    internal bool _showLegends;
+    internal bool _showLegendsAbbr;
+    internal Orientation _sliderOrientation = Orientation.Horizontal;
+    internal int _startSpacing;
+    internal SliderType _type = SliderType.Single;
+    internal bool _useMinimumSize;
+}

+ 24 - 0
Terminal.Gui/Views/SliderEventArgs.cs

@@ -0,0 +1,24 @@
+namespace Terminal.Gui;
+
+/// <summary><see cref="EventArgs"/> for <see cref="Slider{T}"/> events.</summary>
+public class SliderEventArgs<T> : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="SliderEventArgs{T}"/></summary>
+    /// <param name="options">The current options.</param>
+    /// <param name="focused">Index of the option that is focused. -1 if no option has the focus.</param>
+    public SliderEventArgs (Dictionary<int, SliderOption<T>> options, int focused = -1)
+    {
+        Options = options;
+        Focused = focused;
+        Cancel = false;
+    }
+
+    /// <summary>If set to true, the focus operation will be canceled, if applicable.</summary>
+    public bool Cancel { get; set; }
+
+    /// <summary>Gets or sets the index of the option that is focused.</summary>
+    public int Focused { get; set; }
+
+    /// <summary>Gets/sets whether the option is set or not.</summary>
+    public Dictionary<int, SliderOption<T>> Options { get; set; }
+}

+ 50 - 0
Terminal.Gui/Views/SliderOption.cs

@@ -0,0 +1,50 @@
+namespace Terminal.Gui;
+
+/// <summary>Represents an option in a <see cref="Slider{T}"/> .</summary>
+/// <typeparam name="T">Data type of the option.</typeparam>
+public class SliderOption<T>
+{
+    /// <summary>Creates a new empty instance of the <see cref="SliderOption{T}"/> class.</summary>
+    public SliderOption () { }
+
+    /// <summary>Creates a new instance of the <see cref="SliderOption{T}"/> class with values for each property.</summary>
+    public SliderOption (string legend, Rune legendAbbr, T data)
+    {
+        Legend = legend;
+        LegendAbbr = legendAbbr;
+        Data = data;
+    }
+
+    /// <summary>Event fired when the an option has changed.</summary>
+    public event EventHandler<SliderOptionEventArgs> Changed;
+
+    /// <summary>Custom data of the option.</summary>
+    public T Data { get; set; }
+
+    /// <summary>Legend of the option.</summary>
+    public string Legend { get; set; }
+
+    /// <summary>
+    ///     Abbreviation of the Legend. When the <see cref="Slider{T}.MinimumInnerSpacing"/> too small to fit
+    ///     <see cref="Legend"/>.
+    /// </summary>
+    public Rune LegendAbbr { get; set; }
+
+    /// <summary>Event Raised when this option is set.</summary>
+    public event EventHandler<SliderOptionEventArgs> Set;
+
+    /// <summary>Creates a human-readable string that represents this <see cref="SliderOption{T}"/>.</summary>
+    public override string ToString () { return "{Legend=" + Legend + ", LegendAbbr=" + LegendAbbr + ", Data=" + Data + "}"; }
+
+    /// <summary>Event Raised when this option is unset.</summary>
+    public event EventHandler<SliderOptionEventArgs> UnSet;
+
+    /// <summary>To Raise the <see cref="Changed"/> event from the Slider.</summary>
+    internal void OnChanged (bool isSet) { Changed?.Invoke (this, new (isSet)); }
+
+    /// <summary>To Raise the <see cref="Set"/> event from the Slider.</summary>
+    internal void OnSet () { Set?.Invoke (this, new (true)); }
+
+    /// <summary>To Raise the <see cref="UnSet"/> event from the Slider.</summary>
+    internal void OnUnSet () { UnSet?.Invoke (this, new (false)); }
+}

+ 12 - 0
Terminal.Gui/Views/SliderOptionEventArgs.cs

@@ -0,0 +1,12 @@
+namespace Terminal.Gui;
+
+/// <summary><see cref="EventArgs"/> for <see cref="Slider{T}"/> <see cref="SliderOption{T}"/> events.</summary>
+public class SliderOptionEventArgs : EventArgs
+{
+    /// <summary>Initializes a new instance of <see cref="SliderOptionEventArgs"/></summary>
+    /// <param name="isSet"> indicates whether the option is set</param>
+    public SliderOptionEventArgs (bool isSet) { IsSet = isSet; }
+
+    /// <summary>Gets whether the option is set or not.</summary>
+    public bool IsSet { get; }
+}

+ 35 - 0
Terminal.Gui/Views/SliderStyle.cs

@@ -0,0 +1,35 @@
+namespace Terminal.Gui;
+
+/// <summary><see cref="Slider{T}"/> Style</summary>
+public class SliderStyle
+{
+    /// <summary>Constructs a new instance.</summary>
+    public SliderStyle () { LegendAttributes = new (); }
+
+    /// <summary>The glyph and the attribute to indicate mouse dragging.</summary>
+    public Cell DragChar { get; set; }
+
+    /// <summary>The glyph and the attribute used for empty spaces on the slider.</summary>
+    public Cell EmptyChar { get; set; }
+
+    /// <summary>The glyph and the attribute used for the end of ranges on the slider.</summary>
+    public Cell EndRangeChar { get; set; }
+
+    /// <summary>Legend attributes</summary>
+    public SliderAttributes LegendAttributes { get; set; }
+
+    /// <summary>The glyph and the attribute used for each option (tick) on the slider.</summary>
+    public Cell OptionChar { get; set; }
+
+    /// <summary>The glyph and the attribute used for filling in ranges on the slider.</summary>
+    public Cell RangeChar { get; set; }
+
+    /// <summary>The glyph and the attribute used for options (ticks) that are set on the slider.</summary>
+    public Cell SetChar { get; set; }
+
+    /// <summary>The glyph and the attribute used for spaces between options (ticks) on the slider.</summary>
+    public Cell SpaceChar { get; set; }
+
+    /// <summary>The glyph and the attribute used for the start of ranges on the slider.</summary>
+    public Cell StartRangeChar { get; set; }
+}

+ 40 - 0
Terminal.Gui/Views/SliderType.cs

@@ -0,0 +1,40 @@
+namespace Terminal.Gui;
+
+/// <summary><see cref="Slider{T}"/>  Types</summary>
+public enum SliderType
+{
+    /// <summary>
+    ///     <code>
+    /// ├─┼─┼─┼─┼─█─┼─┼─┼─┼─┼─┼─┤
+    /// </code>
+    /// </summary>
+    Single,
+
+    /// <summary>
+    ///     <code>
+    /// ├─┼─█─┼─┼─█─┼─┼─┼─┼─█─┼─┤
+    /// </code>
+    /// </summary>
+    Multiple,
+
+    /// <summary>
+    ///     <code>
+    /// ├▒▒▒▒▒▒▒▒▒█─┼─┼─┼─┼─┼─┼─┤
+    /// </code>
+    /// </summary>
+    LeftRange,
+
+    /// <summary>
+    ///     <code>
+    /// ├─┼─┼─┼─┼─█▒▒▒▒▒▒▒▒▒▒▒▒▒┤
+    /// </code>
+    /// </summary>
+    RightRange,
+
+    /// <summary>
+    ///     <code>
+    /// ├─┼─┼─┼─┼─█▒▒▒▒▒▒▒█─┼─┼─┤
+    /// </code>
+    /// </summary>
+    Range
+}

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

@@ -1,63 +1,5 @@
 namespace Terminal.Gui;
 
-/// <summary>
-///     <see cref="StatusItem"/> objects are contained by <see cref="StatusBar"/> <see cref="View"/>s. Each
-///     <see cref="StatusItem"/> has a title, a shortcut (hotkey), and an <see cref="Command"/> that will be invoked when
-///     the <see cref="StatusItem.Shortcut"/> is pressed. The <see cref="StatusItem.Shortcut"/> will be a global hotkey for
-///     the application in the current context of the screen. The color of the <see cref="StatusItem.Title"/> will be
-///     changed after each ~. A <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using
-///     <see cref="ColorScheme.HotNormal"/> and *Help* as <see cref="ColorScheme.HotNormal"/>.
-/// </summary>
-public class StatusItem
-{
-    /// <summary>Initializes a new <see cref="StatusItem"/>.</summary>
-    /// <param name="shortcut">Shortcut to activate the <see cref="StatusItem"/>.</param>
-    /// <param name="title">Title for the <see cref="StatusItem"/>.</param>
-    /// <param name="action">Action to invoke when the <see cref="StatusItem"/> is activated.</param>
-    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
-    public StatusItem (Key shortcut, string title, Action action, Func<bool> canExecute = null)
-    {
-        Title = title ?? "";
-        Shortcut = shortcut;
-        Action = action;
-        CanExecute = canExecute;
-    }
-
-    /// <summary>Gets or sets the action to be invoked when the statusbar item is triggered</summary>
-    /// <value>Action to invoke.</value>
-    public Action Action { get; set; }
-
-    /// <summary>
-    ///     Gets or sets the action to be invoked to determine if the <see cref="StatusItem"/> can be triggered. If
-    ///     <see cref="CanExecute"/> returns <see langword="true"/> the status item will be enabled. Otherwise, it will be
-    ///     disabled.
-    /// </summary>
-    /// <value>Function to determine if the action is can be executed or not.</value>
-    public Func<bool> CanExecute { get; set; }
-
-    /// <summary>Gets or sets arbitrary data for the status item.</summary>
-    /// <remarks>This property is not used internally.</remarks>
-    public object Data { get; set; }
-
-    /// <summary>Gets the global shortcut to invoke the action on the menu.</summary>
-    public Key Shortcut { get; set; }
-
-    /// <summary>Gets or sets the title.</summary>
-    /// <value>The title.</value>
-    /// <remarks>
-    ///     The colour of the <see cref="StatusItem.Title"/> will be changed after each ~. A
-    ///     <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using <see cref="ColorScheme.HotNormal"/> and
-    ///     *Help* as <see cref="ColorScheme.HotNormal"/>.
-    /// </remarks>
-    public string Title { get; set; }
-
-    /// <summary>
-    ///     Returns <see langword="true"/> if the status item is enabled. This method is a wrapper around
-    ///     <see cref="CanExecute"/>.
-    /// </summary>
-    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
-}
-
 /// <summary>
 ///     A status bar is a <see cref="View"/> that snaps to the bottom of a <see cref="Toplevel"/> displaying set of
 ///     <see cref="StatusItem"/>s. The <see cref="StatusBar"/> should be context sensitive. This means, if the main menu
@@ -69,8 +11,7 @@ public class StatusBar : View
 {
     private static Rune _shortcutDelimiter = (Rune)'=';
 
-    private StatusItem [] _items = { };
-    private StatusItem _itemToInvoke;
+    private StatusItem [] _items = [];
 
     /// <summary>Initializes a new instance of the <see cref="StatusBar"/> class.</summary>
     public StatusBar () : this (new StatusItem [] { }) { }
@@ -91,10 +32,11 @@ public class StatusBar : View
         CanFocus = false;
         ColorScheme = Colors.ColorSchemes ["Menu"];
         X = 0;
-        Y = Pos.AnchorEnd (1);
+        Y = Pos.AnchorEnd ();
         Width = Dim.Fill ();
-        Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == ContentSize
-        AddCommand (Command.Accept, InvokeItem);
+        Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
+
+        AddCommand (Command.Accept, ctx => InvokeItem ((StatusItem)ctx.KeyBinding?.Context));
     }
 
     /// <summary>The items that compose the <see cref="StatusBar"/></summary>
@@ -110,9 +52,10 @@ public class StatusBar : View
 
             _items = value;
 
-            foreach (StatusItem item in _items)
+            foreach (StatusItem item in _items.Where (i => i.Shortcut != Key.Empty))
             {
-                KeyBindings.Add (item.Shortcut, KeyBindingScope.HotKey, Command.Accept);
+                KeyBinding keyBinding = new (new [] { Command.Accept }, KeyBindingScope.HotKey, item);
+                KeyBindings.Add (item.Shortcut, keyBinding);
             }
         }
     }
@@ -142,7 +85,7 @@ public class StatusBar : View
     }
 
     ///<inheritdoc/>
-    protected internal override bool OnMouseEvent  (MouseEvent me)
+    protected internal override bool OnMouseEvent (MouseEvent me)
     {
         if (me.Flags != MouseFlags.Button1Clicked)
         {
@@ -215,32 +158,6 @@ public class StatusBar : View
         }
     }
 
-    /// <inheritdoc/>
-    public override bool? OnInvokingKeyBindings (Key keyEvent)
-    {
-        // This is a bit of a hack. We want to handle the key bindings for status bar but
-        // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for.
-        // So before we call the base class we set SelectedItem appropriately.
-        Key key = new (keyEvent);
-
-        if (KeyBindings.TryGet (key, out _))
-        {
-            // Search RadioLabels 
-            foreach (StatusItem item in Items)
-            {
-                if (item.Shortcut == key)
-                {
-                    _itemToInvoke = item;
-                    //keyEvent.Scope = KeyBindingScope.HotKey;
-
-                    break;
-                }
-            }
-        }
-
-        return base.OnInvokingKeyBindings (keyEvent);
-    }
-
     /// <summary>Removes a <see cref="StatusItem"/> at specified index of <see cref="Items"/>.</summary>
     /// <param name="index">The zero-based index of the item to remove.</param>
     /// <returns>The <see cref="StatusItem"/> removed.</returns>
@@ -287,11 +204,11 @@ public class StatusBar : View
         return len;
     }
 
-    private bool? InvokeItem ()
+    private bool? InvokeItem (StatusItem itemToInvoke)
     {
-        if (_itemToInvoke is { Action: { } })
+        if (itemToInvoke is { Action: { } })
         {
-            _itemToInvoke.Action.Invoke ();
+            itemToInvoke.Action.Invoke ();
 
             return true;
         }

+ 59 - 0
Terminal.Gui/Views/StatusItem.cs

@@ -0,0 +1,59 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     <see cref="StatusItem"/> objects are contained by <see cref="StatusBar"/> <see cref="View"/>s. Each
+///     <see cref="StatusItem"/> has a title, a shortcut (hotkey), and an <see cref="Command"/> that will be invoked when
+///     the <see cref="StatusItem.Shortcut"/> is pressed. The <see cref="StatusItem.Shortcut"/> will be a global hotkey for
+///     the application in the current context of the screen. The color of the <see cref="StatusItem.Title"/> will be
+///     changed after each ~. A <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using
+///     <see cref="ColorScheme.HotNormal"/> and *Help* as <see cref="ColorScheme.HotNormal"/>.
+/// </summary>
+public class StatusItem
+{
+    /// <summary>Initializes a new <see cref="StatusItem"/>.</summary>
+    /// <param name="shortcut">Shortcut to activate the <see cref="StatusItem"/>.</param>
+    /// <param name="title">Title for the <see cref="StatusItem"/>.</param>
+    /// <param name="action">Action to invoke when the <see cref="StatusItem"/> is activated.</param>
+    /// <param name="canExecute">Function to determine if the action can currently be executed.</param>
+    public StatusItem (Key shortcut, string title, Action action, Func<bool> canExecute = null)
+    {
+        Title = title ?? "";
+        Shortcut = shortcut;
+        Action = action;
+        CanExecute = canExecute;
+    }
+
+    /// <summary>Gets or sets the action to be invoked when the <see cref="StatusItem"/> is triggered</summary>
+    /// <value>Action to invoke.</value>
+    public Action Action { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the action to be invoked to determine if the <see cref="StatusItem"/> can be triggered. If
+    ///     <see cref="CanExecute"/> returns <see langword="true"/> the status item will be enabled. Otherwise, it will be
+    ///     disabled.
+    /// </summary>
+    /// <value>Function to determine if the action is can be executed or not.</value>
+    public Func<bool> CanExecute { get; set; }
+
+    /// <summary>Gets or sets arbitrary data for the status item.</summary>
+    /// <remarks>This property is not used internally.</remarks>
+    public object Data { get; set; }
+
+    /// <summary>Gets the global shortcut to invoke the action on the menu.</summary>
+    public Key Shortcut { get; set; }
+
+    /// <summary>Gets or sets the title.</summary>
+    /// <value>The title.</value>
+    /// <remarks>
+    ///     The colour of the <see cref="StatusItem.Title"/> will be changed after each ~. A
+    ///     <see cref="StatusItem.Title"/> set to `~F1~ Help` will render as *F1* using <see cref="ColorScheme.HotNormal"/> and
+    ///     *Help* as <see cref="ColorScheme.HotNormal"/>.
+    /// </remarks>
+    public string Title { get; set; }
+
+    /// <summary>
+    ///     Returns <see langword="true"/> if the status item is enabled. This method is a wrapper around
+    ///     <see cref="CanExecute"/>.
+    /// </summary>
+    public bool IsEnabled () { return CanExecute?.Invoke () ?? true; }
+}

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

@@ -21,7 +21,7 @@ public class TabView : View
     private TabToRender [] _tabLocations;
     private int _tabScrollOffset;
 
-    /// <summary>Initializes a <see cref="TabView"/> class using <see cref="LayoutStyle.Computed"/> layout.</summary>
+    /// <summary>Initializes a <see cref="TabView"/> class.</summary>
     public TabView ()
     {
         CanFocus = true;
@@ -564,7 +564,7 @@ public class TabView : View
             _host = host;
 
             CanFocus = true;
-            Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == ContentSize
+            Height = 1; // BUGBUG: Views should avoid setting Height as doing so implies Frame.Size == GetContentSize ().
             Width = Dim.Fill ();
 
             _rightScrollIndicator = new View

+ 13 - 13
Terminal.Gui/Views/TableView/ColumnStyle.cs

@@ -8,10 +8,10 @@
 public class ColumnStyle
 {
     /// <summary>
-    ///     Defines a delegate for returning custom alignment per cell based on cell values.  When specified this will
+    ///     Defines a delegate for returning custom alignment per cell based on cell values. When specified this will
     ///     override <see cref="Alignment"/>
     /// </summary>
-    public Func<object, TextAlignment> AlignmentGetter;
+    public Func<object, Alignment> AlignmentGetter;
 
     /// <summary>
     ///     Defines a delegate for returning a custom color scheme per cell based on cell values. Return null for the
@@ -20,26 +20,26 @@ public class ColumnStyle
     public CellColorGetterDelegate ColorGetter;
 
     /// <summary>
-    ///     Defines a delegate for returning custom representations of cell values.  If not set then
-    ///     <see cref="object.ToString()"/> is used.  Return values from your delegate may be truncated e.g. based on
+    ///     Defines a delegate for returning custom representations of cell values. If not set then
+    ///     <see cref="object.ToString()"/> is used. Return values from your delegate may be truncated e.g. based on
     ///     <see cref="MaxWidth"/>
     /// </summary>
     public Func<object, string> RepresentationGetter;
 
-    private bool visible = true;
+    private bool _visible = true;
 
     /// <summary>
-    ///     Defines the default alignment for all values rendered in this column.  For custom alignment based on cell
+    ///     Defines the default alignment for all values rendered in this column. For custom alignment based on cell
     ///     contents use <see cref="AlignmentGetter"/>.
     /// </summary>
-    public TextAlignment Alignment { get; set; }
+    public Alignment Alignment { get; set; }
 
     /// <summary>Defines the format for values e.g. "yyyy-MM-dd" for dates</summary>
     public string Format { get; set; }
 
     /// <summary>
-    ///     Set the maximum width of the column in characters.  This value will be ignored if more than the tables
-    ///     <see cref="TableView.MaxCellWidth"/>.  Defaults to <see cref="TableView.DefaultMaxCellWidth"/>
+    ///     Set the maximum width of the column in characters. This value will be ignored if more than the tables
+    ///     <see cref="TableView.MaxCellWidth"/>. Defaults to <see cref="TableView.DefaultMaxCellWidth"/>
     /// </summary>
     public int MaxWidth { get; set; } = TableView.DefaultMaxCellWidth;
 
@@ -47,7 +47,7 @@ public class ColumnStyle
     public int MinAcceptableWidth { get; set; } = TableView.DefaultMinAcceptableWidth;
 
     /// <summary>
-    ///     Set the minimum width of the column in characters.  Setting this will ensure that even when a column has short
+    ///     Set the minimum width of the column in characters. Setting this will ensure that even when a column has short
     ///     content/header it still fills a given width of the control.
     ///     <para>
     ///         This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/> or the
@@ -64,8 +64,8 @@ public class ColumnStyle
     /// <remarks>If <see cref="MaxWidth"/> is 0 then <see cref="Visible"/> will always return false.</remarks>
     public bool Visible
     {
-        get => MaxWidth >= 0 && visible;
-        set => visible = value;
+        get => MaxWidth >= 0 && _visible;
+        set => _visible = value;
     }
 
     /// <summary>
@@ -74,7 +74,7 @@ public class ColumnStyle
     /// </summary>
     /// <param name="cellValue"></param>
     /// <returns></returns>
-    public TextAlignment GetAlignment (object cellValue)
+    public Alignment GetAlignment (object cellValue)
     {
         if (AlignmentGetter is { })
         {

+ 2 - 2
Terminal.Gui/Views/TableView/TableStyle.cs

@@ -15,11 +15,11 @@ public class TableStyle
     /// </summary>
     public bool AlwaysUseNormalColorForVerticalCellLines { get; set; } = false;
 
-    /// <summary>Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc)</summary>
+    /// <summary>Collection of columns for which you want special rendering (e.g. custom column lengths, text justification, etc.)</summary>
     public Dictionary<int, ColumnStyle> ColumnStyles { get; set; } = new ();
 
     /// <summary>
-    ///     Determines rendering when the last column in the table is visible but it's content or
+    ///     Determines rendering when the last column in the table is visible, but it's content or
     ///     <see cref="ColumnStyle.MaxWidth"/> is less than the remaining space in the control.  True (the default) will expand
     ///     the column to fill the remaining bounds of the control.  False will draw a column ending line and leave a blank
     ///     column that cannot be selected in the remaining space.

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

@@ -37,12 +37,12 @@ public class TableView : View
     private TableStyle style = new ();
     private ITableSource table;
 
-    /// <summary>Initializes a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.</summary>
+    /// <summary>Initializes a <see cref="TableView"/> class.</summary>
     /// <param name="table">The table to display in the control</param>
     public TableView (ITableSource table) : this () { Table = table; }
 
     /// <summary>
-    ///     Initializes a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the
+    ///     Initializes a <see cref="TableView"/> class. Set the
     ///     <see cref="Table"/> property to begin editing
     /// </summary>
     public TableView ()
@@ -908,10 +908,10 @@ public class TableView : View
         // What columns to render at what X offset in viewport
         ColumnToRender [] columnsToRender = CalculateViewport (Viewport).ToArray ();
 
-        Driver.SetAttribute (GetNormalColor ());
+        Driver?.SetAttribute (GetNormalColor ());
 
         //invalidate current row (prevents scrolling around leaving old characters in the frame
-        Driver.AddStr (new string (' ', Viewport.Width));
+        Driver?.AddStr (new string (' ', Viewport.Width));
 
         var line = 0;
 
@@ -2014,7 +2014,7 @@ public class TableView : View
 
     /// <summary>
     ///     Returns true if the <see cref="Table"/> is not set or all the columns in the <see cref="Table"/> have an
-    ///     explicit <see cref="ColumnStyle"/> that marks them <see cref="ColumnStyle.visible"/> <see langword="false"/>.
+    ///     explicit <see cref="ColumnStyle"/> that marks them <see cref="ColumnStyle.Visible"/> <see langword="false"/>.
     /// </summary>
     /// <returns></returns>
     private bool TableIsNullOrInvisible ()
@@ -2116,16 +2116,16 @@ public class TableView : View
                         - (representation.EnumerateRunes ().Sum (c => c.GetColumns ())
                            + 1 /*leave 1 space for cell boundary*/);
 
-            switch (colStyle?.GetAlignment (originalCellValue) ?? TextAlignment.Left)
+            switch (colStyle?.GetAlignment (originalCellValue) ?? Alignment.Start)
             {
-                case TextAlignment.Left:
+                case Alignment.Start:
                     return representation + new string (' ', toPad);
-                case TextAlignment.Right:
+                case Alignment.End:
                     return new string (' ', toPad) + representation;
 
                 // TODO: With single line cells, centered and justified are the same right?
-                case TextAlignment.Centered:
-                case TextAlignment.Justified:
+                case Alignment.Center:
+                case Alignment.Fill:
                     return
                         new string (' ', (int)Math.Floor (toPad / 2.0))
                         + // round down

+ 1 - 2
Terminal.Gui/Views/TextField.cs

@@ -21,8 +21,7 @@ public class TextField : View
     private List<Rune> _text;
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="TextField"/> class using <see cref="LayoutStyle.Computed"/>
-    ///     positioning.
+    ///     Initializes a new instance of the <see cref="TextField"/> class.
     /// </summary>
     public TextField ()
     {

+ 8 - 9
Terminal.Gui/Views/TextValidateField.cs

@@ -392,8 +392,7 @@ namespace Terminal.Gui
         private ITextValidateProvider _provider;
 
         /// <summary>
-        ///     Initializes a new instance of the <see cref="TextValidateField"/> class using
-        ///     <see cref="LayoutStyle.Computed"/> positioning.
+        ///     Initializes a new instance of the <see cref="TextValidateField"/> class.
         /// </summary>
         public TextValidateField ()
         {
@@ -539,7 +538,7 @@ namespace Terminal.Gui
             {
                 int c = _provider.Cursor (mouseEvent.Position.X - GetMargins (Viewport.Width).left);
 
-                if (_provider.Fixed == false && TextAlignment == TextAlignment.Right && Text.Length > 0)
+                if (_provider.Fixed == false && TextAlignment == Alignment.End && Text.Length > 0)
                 {
                     c++;
                 }
@@ -633,7 +632,7 @@ namespace Terminal.Gui
             // When it's right-aligned and it's a normal input, the cursor behaves differently.
             int curPos;
 
-            if (_provider?.Fixed == false && TextAlignment == TextAlignment.Right)
+            if (_provider?.Fixed == false && TextAlignment == Alignment.End)
             {
                 curPos = _cursorPosition + left - 1;
             }
@@ -650,7 +649,7 @@ namespace Terminal.Gui
         /// <returns></returns>
         private bool BackspaceKeyHandler ()
         {
-            if (_provider.Fixed == false && TextAlignment == TextAlignment.Right && _cursorPosition <= 1)
+            if (_provider.Fixed == false && TextAlignment == Alignment.End && _cursorPosition <= 1)
             {
                 return false;
             }
@@ -688,7 +687,7 @@ namespace Terminal.Gui
         /// <returns></returns>
         private bool DeleteKeyHandler ()
         {
-            if (_provider.Fixed == false && TextAlignment == TextAlignment.Right)
+            if (_provider.Fixed == false && TextAlignment == Alignment.End)
             {
                 _cursorPosition = _provider.CursorLeft (_cursorPosition);
             }
@@ -719,11 +718,11 @@ namespace Terminal.Gui
 
             switch (TextAlignment)
             {
-                case TextAlignment.Left:
+                case Alignment.Start:
                     return (0, total);
-                case TextAlignment.Centered:
+                case Alignment.Center:
                     return (total / 2, total / 2 + total % 2);
-                case TextAlignment.Right:
+                case Alignment.End:
                     return (total, 0);
                 default:
                     return (0, total);

+ 64 - 99
Terminal.Gui/Views/TextView.cs

@@ -1784,7 +1784,7 @@ internal class WordWrapManager
                                                             TextFormatter.Format (
                                                                                   TextModel.ToString (line),
                                                                                   width,
-                                                                                  TextAlignment.Left,
+                                                                                  Alignment.Start,
                                                                                   true,
                                                                                   preserveTrailingSpaces,
                                                                                   tabWidth
@@ -2588,25 +2588,6 @@ public class TextView : View
     /// </summary>
     public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete ();
 
-    /// <summary>
-    ///     The bottom offset needed to use a horizontal scrollbar or for another reason. This is only needed with the
-    ///     keyboard navigation.
-    /// </summary>
-    public int BottomOffset
-    {
-        get => _bottomOffset;
-        set
-        {
-            if (CurrentRow == Lines - 1 && _bottomOffset > 0 && value == 0)
-            {
-                _topRow = Math.Max (_topRow - _bottomOffset, 0);
-            }
-
-            _bottomOffset = value;
-            Adjust ();
-        }
-    }
-
     /// <summary>Get the <see cref="ContextMenu"/> for this view.</summary>
     public ContextMenu? ContextMenu { get; }
 
@@ -2678,7 +2659,7 @@ public class TextView : View
     public int Lines => _model.Count;
 
     /// <summary>Gets the maximum visible length line.</summary>
-    public int Maxlength => _model.GetMaxVisibleLine (_topRow, _topRow + Frame.Height, TabWidth);
+    public int Maxlength => _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth);
 
     /// <summary>Gets or sets a value indicating whether this <see cref="TextView"/> is a multiline text view.</summary>
     public bool Multiline
@@ -2736,30 +2717,12 @@ public class TextView : View
                 _isReadOnly = value;
 
                 SetNeedsDisplay ();
+                WrapTextModel ();
                 Adjust ();
             }
         }
     }
 
-    /// <summary>
-    ///     The right offset needed to use a vertical scrollbar or for another reason. This is only needed with the
-    ///     keyboard navigation.
-    /// </summary>
-    public int RightOffset
-    {
-        get => _rightOffset;
-        set
-        {
-            if (!_wordWrap && CurrentColumn == GetCurrentLine ().Count && _rightOffset > 0 && value == 0)
-            {
-                _leftColumn = Math.Max (_leftColumn - _rightOffset, 0);
-            }
-
-            _rightOffset = value;
-            Adjust ();
-        }
-    }
-
     /// <summary>Length of the selected text.</summary>
     public int SelectedLength => GetSelectedLength ();
 
@@ -2852,7 +2815,7 @@ public class TextView : View
             if (_wordWrap)
             {
                 _wrapManager = new (_model);
-                _model = _wrapManager.WrapModel (_frameWidth, out _, out _, out _, out _);
+                _model = _wrapManager.WrapModel (Viewport.Width, out _, out _, out _, out _);
             }
 
             OnTextChanged (old, Text);
@@ -2897,7 +2860,7 @@ public class TextView : View
             if (_wordWrap)
             {
                 _wrapManager = new (_model);
-                _model = _wrapManager.WrapModel (_frameWidth, out _, out _, out _, out _);
+                WrapTextModel ();
             }
             else if (!_wordWrap && _wrapManager is { })
             {
@@ -2908,7 +2871,6 @@ public class TextView : View
         }
     }
 
-    private int _frameWidth => Math.Max (Frame.Width - (RightOffset != 0 ? 2 : 1), 0);
 
     /// <summary>Allows clearing the <see cref="HistoryText.HistoryTextItem"/> items updating the original text.</summary>
     public void ClearHistoryChanges () { _historyText?.Clear (Text); }
@@ -3389,34 +3351,34 @@ public class TextView : View
 
             if (_model.Count > 0 && _shiftSelecting && Selecting)
             {
-                if (CurrentRow - _topRow + BottomOffset >= Frame.Height - 1 && _model.Count + BottomOffset > _topRow + CurrentRow)
+                if (CurrentRow - _topRow >= Viewport.Height - 1 && _model.Count > _topRow + CurrentRow)
                 {
-                    ScrollTo (_topRow + Frame.Height);
+                    ScrollTo (_topRow + Viewport.Height);
                 }
                 else if (_topRow > 0 && CurrentRow <= _topRow)
                 {
-                    ScrollTo (_topRow - Frame.Height);
+                    ScrollTo (_topRow - Viewport.Height);
                 }
-                else if (ev.Position.Y >= Frame.Height)
+                else if (ev.Position.Y >= Viewport.Height)
                 {
-                    ScrollTo (_model.Count + BottomOffset);
+                    ScrollTo (_model.Count);
                 }
                 else if (ev.Position.Y < 0 && _topRow > 0)
                 {
                     ScrollTo (0);
                 }
 
-                if (CurrentColumn - _leftColumn + RightOffset >= Frame.Width - 1 && line.Count + RightOffset > _leftColumn + CurrentColumn)
+                if (CurrentColumn - _leftColumn >= Viewport.Width - 1 && line.Count > _leftColumn + CurrentColumn)
                 {
-                    ScrollTo (_leftColumn + Frame.Width, false);
+                    ScrollTo (_leftColumn + Viewport.Width, false);
                 }
                 else if (_leftColumn > 0 && CurrentColumn <= _leftColumn)
                 {
-                    ScrollTo (_leftColumn - Frame.Width, false);
+                    ScrollTo (_leftColumn - Viewport.Width, false);
                 }
-                else if (ev.Position.X >= Frame.Width)
+                else if (ev.Position.X >= Viewport.Width)
                 {
-                    ScrollTo (line.Count + RightOffset, false);
+                    ScrollTo (line.Count, false);
                 }
                 else if (ev.Position.X < 0 && _leftColumn > 0)
                 {
@@ -3585,8 +3547,8 @@ public class TextView : View
         SetNormalColor ();
 
         (int width, int height) offB = OffSetBackground ();
-        int right = Frame.Width + offB.width + RightOffset;
-        int bottom = Frame.Height + offB.height + BottomOffset;
+        int right = Viewport.Width + offB.width;
+        int bottom = Viewport.Height + offB.height;
         var row = 0;
 
         for (int idxRow = _topRow; idxRow < _model.Count; idxRow++)
@@ -3826,9 +3788,9 @@ public class TextView : View
         if (Application.MouseGrabView == this && Selecting)
         {
             // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
-            //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Frame.Height);
-            //var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Frame.Height);
-            //SetNeedsDisplay (new (0, minRow, Frame.Width, maxRow));
+            //var minRow = Math.Min (Math.Max (Math.Min (selectionStartRow, currentRow) - topRow, 0), Viewport.Height);
+            //var maxRow = Math.Min (Math.Max (Math.Max (selectionStartRow, currentRow) - topRow, 0), Viewport.Height);
+            //SetNeedsDisplay (new (0, minRow, Viewport.Width, maxRow));
             SetNeedsDisplay ();
         }
 
@@ -3851,7 +3813,7 @@ public class TextView : View
                     cols += TabWidth + 1;
                 }
 
-                if (!TextModel.SetCol (ref col, Frame.Width, cols))
+                if (!TextModel.SetCol (ref col, Viewport.Width, cols))
                 {
                     col = CurrentColumn;
 
@@ -3863,7 +3825,7 @@ public class TextView : View
         int posX = CurrentColumn - _leftColumn;
         int posY = CurrentRow - _topRow;
 
-        if (posX > -1 && col >= posX && posX < Frame.Width - RightOffset && _topRow <= CurrentRow && posY < Frame.Height - BottomOffset)
+        if (posX > -1 && col >= posX && posX < Viewport.Width && _topRow <= CurrentRow && posY < Viewport.Height)
         {
             Move (col, CurrentRow - _topRow);
             return new (col, CurrentRow - _topRow);
@@ -3934,7 +3896,7 @@ public class TextView : View
         else if (!_wordWrap)
         {
             int maxlength =
-                _model.GetMaxVisibleLine (_topRow, _topRow + Frame.Height + RightOffset, TabWidth);
+                _model.GetMaxVisibleLine (_topRow, _topRow + Viewport.Height, TabWidth);
             _leftColumn = Math.Max (!_wordWrap && idx > maxlength - 1 ? maxlength - 1 : idx, 0);
         }
 
@@ -4113,18 +4075,18 @@ public class TextView : View
             need = true;
         }
         else if (!_wordWrap
-                 && (CurrentColumn - _leftColumn + RightOffset > Frame.Width + offB.width || dSize.size + RightOffset >= Frame.Width + offB.width))
+                 && (CurrentColumn - _leftColumn + 1 > Viewport.Width + offB.width || dSize.size + 1 >= Viewport.Width + offB.width))
         {
             _leftColumn = TextModel.CalculateLeftColumn (
                                                          line,
                                                          _leftColumn,
                                                          CurrentColumn,
-                                                         Frame.Width + offB.width - RightOffset,
+                                                         Viewport.Width + offB.width,
                                                          TabWidth
                                                         );
             need = true;
         }
-        else if ((_wordWrap && _leftColumn > 0) || (dSize.size + RightOffset < Frame.Width + offB.width && tSize.size + RightOffset < Frame.Width + offB.width))
+        else if ((_wordWrap && _leftColumn > 0) || (dSize.size < Viewport.Width + offB.width && tSize.size < Viewport.Width + offB.width))
         {
             if (_leftColumn > 0)
             {
@@ -4138,9 +4100,9 @@ public class TextView : View
             _topRow = CurrentRow;
             need = true;
         }
-        else if (CurrentRow - _topRow + BottomOffset >= Frame.Height + offB.height)
+        else if (CurrentRow - _topRow >= Viewport.Height + offB.height)
         {
-            _topRow = Math.Min (Math.Max (CurrentRow - Frame.Height + 1 + BottomOffset, 0), CurrentRow);
+            _topRow = Math.Min (Math.Max (CurrentRow - Viewport.Height + 1, 0), CurrentRow);
             need = true;
         }
         else if (_topRow > 0 && CurrentRow < _topRow)
@@ -4161,7 +4123,10 @@ public class TextView : View
         }
         else
         {
-            PositionCursor ();
+            if (IsInitialized)
+            {
+                PositionCursor ();
+            }
         }
 
         OnUnwrappedCursorPosition ();
@@ -4282,7 +4247,7 @@ public class TextView : View
             {
                 //QUESTION: Is the below comment still relevant?
                 // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
-                //SetNeedsDisplay (new (0, startRow - topRow, Frame.Width, startRow - topRow + 1));
+                //SetNeedsDisplay (new (0, startRow - topRow, Viewport.Width, startRow - topRow + 1));
                 SetNeedsDisplay ();
             }
 
@@ -4378,7 +4343,7 @@ public class TextView : View
             else
             {
                 // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
-                //SetNeedsDisplay (new (0, currentRow - topRow, 1, Frame.Width));
+                //SetNeedsDisplay (new (0, currentRow - topRow, 1, Viewport.Width));
                 SetNeedsDisplay ();
             }
         }
@@ -4470,7 +4435,7 @@ public class TextView : View
                 _wrapNeeded = true;
             }
 
-            DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Frame.Width, CurrentRow - _topRow + 1));
+            DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, CurrentRow - _topRow + 1));
         }
         else
         {
@@ -4493,8 +4458,8 @@ public class TextView : View
                                new (
                                     CurrentColumn - _leftColumn,
                                     CurrentRow - _topRow,
-                                    Frame.Width,
-                                    CurrentRow - _topRow + 1
+                                    Viewport.Width,
+                                    Math.Max (CurrentRow - _topRow + 1, 0)
                                    )
                               );
         }
@@ -4788,7 +4753,7 @@ public class TextView : View
         if (!_wrapNeeded)
         {
             // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
-            //SetNeedsDisplay (new (0, prow, Math.Max (Frame.Width, 0), Math.Max (prow + 1, 0)));
+            //SetNeedsDisplay (new (0, prow, Math.Max (Viewport.Width, 0), Math.Max (prow + 1, 0)));
             SetNeedsDisplay ();
         }
     }
@@ -4825,9 +4790,9 @@ public class TextView : View
                               HistoryText.LineStatus.Replaced
                              );
 
-            if (!_wordWrap && CurrentColumn - _leftColumn > Frame.Width)
+            if (!_wordWrap && CurrentColumn - _leftColumn > Viewport.Width)
             {
-                _leftColumn = Math.Max (CurrentColumn - Frame.Width + 1, 0);
+                _leftColumn = Math.Max (CurrentColumn - Viewport.Width + 1, 0);
             }
 
             if (_wordWrap)
@@ -4837,7 +4802,7 @@ public class TextView : View
             else
             {
                 // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
-                //SetNeedsDisplay (new (0, currentRow - topRow, Frame.Width, Math.Max (currentRow - topRow + 1, 0)));
+                //SetNeedsDisplay (new (0, currentRow - topRow, Viewport.Width, Math.Max (currentRow - topRow + 1, 0)));
                 SetNeedsDisplay ();
             }
 
@@ -4933,7 +4898,7 @@ public class TextView : View
                 Insert (new () { Rune = a.AsRune, ColorScheme = colorScheme });
                 CurrentColumn++;
 
-                if (CurrentColumn >= _leftColumn + Frame.Width)
+                if (CurrentColumn >= _leftColumn + Viewport.Width)
                 {
                     _leftColumn++;
                     SetNeedsDisplay ();
@@ -5051,7 +5016,7 @@ public class TextView : View
 
         UpdateWrapModel ();
 
-        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Frame.Width, Frame.Height));
+        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
 
         _lastWasKill = setLastWasKill;
         DoNeededAction ();
@@ -5158,7 +5123,7 @@ public class TextView : View
 
         UpdateWrapModel ();
 
-        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Frame.Width, Frame.Height));
+        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
 
         _lastWasKill = setLastWasKill;
         DoNeededAction ();
@@ -5228,7 +5193,7 @@ public class TextView : View
 
         UpdateWrapModel ();
 
-        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Frame.Width, Frame.Height));
+        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
         DoNeededAction ();
     }
 
@@ -5287,7 +5252,7 @@ public class TextView : View
 
         UpdateWrapModel ();
 
-        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Frame.Width, Frame.Height));
+        DoSetNeedsDisplay (new (0, CurrentRow - _topRow, Viewport.Width, Viewport.Height));
         DoNeededAction ();
     }
 
@@ -5303,7 +5268,7 @@ public class TextView : View
         if (!_multiline && !IsInitialized)
         {
             CurrentColumn = Text.GetRuneCount ();
-            _leftColumn = CurrentColumn > Frame.Width + 1 ? CurrentColumn - Frame.Width + 1 : 0;
+            _leftColumn = CurrentColumn > Viewport.Width + 1 ? CurrentColumn - Viewport.Width + 1 : 0;
         }
     }
 
@@ -5337,7 +5302,7 @@ public class TextView : View
 
             CurrentRow++;
 
-            if (CurrentRow + BottomOffset >= _topRow + Frame.Height)
+            if (CurrentRow >= _topRow + Viewport.Height)
             {
                 _topRow++;
                 SetNeedsDisplay ();
@@ -5346,7 +5311,7 @@ public class TextView : View
             TrackColumn ();
             PositionCursor ();
         }
-        else if (CurrentRow > Frame.Height)
+        else if (CurrentRow > Viewport.Height)
         {
             Adjust ();
         }
@@ -5357,7 +5322,7 @@ public class TextView : View
     private void MoveEndOfLine ()
     {
         List<RuneCell> currentLine = GetCurrentLine ();
-        CurrentColumn = currentLine.Count;
+        CurrentColumn = Math.Max (currentLine.Count - (ReadOnly ? 1 : 0), 0);
         Adjust ();
         DoNeededAction ();
     }
@@ -5381,7 +5346,7 @@ public class TextView : View
                 }
 
                 List<RuneCell> currentLine = GetCurrentLine ();
-                CurrentColumn = currentLine.Count;
+                CurrentColumn = Math.Max (currentLine.Count - (ReadOnly ? 1 : 0), 0);
             }
         }
 
@@ -5401,7 +5366,7 @@ public class TextView : View
 
     private void MovePageDown ()
     {
-        int nPageDnShift = Frame.Height - 1;
+        int nPageDnShift = Viewport.Height - 1;
 
         if (CurrentRow >= 0 && CurrentRow < _model.Count)
         {
@@ -5431,7 +5396,7 @@ public class TextView : View
 
     private void MovePageUp ()
     {
-        int nPageUpShift = Frame.Height - 1;
+        int nPageUpShift = Viewport.Height - 1;
 
         if (CurrentRow > 0)
         {
@@ -5469,7 +5434,7 @@ public class TextView : View
     {
         List<RuneCell> currentLine = GetCurrentLine ();
 
-        if (CurrentColumn < currentLine.Count)
+        if ((ReadOnly ? CurrentColumn + 1 : CurrentColumn) < currentLine.Count)
         {
             CurrentColumn++;
         }
@@ -5480,7 +5445,7 @@ public class TextView : View
                 CurrentRow++;
                 CurrentColumn = 0;
 
-                if (CurrentRow >= _topRow + Frame.Height)
+                if (CurrentRow >= _topRow + Viewport.Height)
                 {
                     _topRow++;
                     SetNeedsDisplay ();
@@ -5581,14 +5546,14 @@ public class TextView : View
         var w = 0;
         var h = 0;
 
-        if (SuperView?.Frame.Right - Frame.Right < 0)
+        if (SuperView?.Viewport.Right - Viewport.Right < 0)
         {
-            w = SuperView!.Frame.Right - Frame.Right - 1;
+            w = SuperView!.Viewport.Right - Viewport.Right - 1;
         }
 
-        if (SuperView?.Frame.Bottom - Frame.Bottom < 0)
+        if (SuperView?.Viewport.Bottom - Viewport.Bottom < 0)
         {
-            h = SuperView!.Frame.Bottom - Frame.Bottom - 1;
+            h = SuperView!.Viewport.Bottom - Viewport.Bottom - 1;
         }
 
         return (w, h);
@@ -5835,9 +5800,9 @@ public class TextView : View
             r = GetCurrentLine ();
             int idx = TextModel.GetColFromX (r, _leftColumn, Math.Max (ev.Position.X, 0), TabWidth);
 
-            if (idx - _leftColumn >= r.Count + RightOffset)
+            if (idx - _leftColumn >= r.Count)
             {
-                CurrentColumn = Math.Max (r.Count - _leftColumn + RightOffset, 0);
+                CurrentColumn = Math.Max (r.Count - _leftColumn - (ReadOnly ? 1 : 0), 0);
             }
             else
             {
@@ -6123,7 +6088,7 @@ public class TextView : View
 
         var fullNeedsDisplay = false;
 
-        if (CurrentRow >= _topRow + Frame.Height)
+        if (CurrentRow >= _topRow + Viewport.Height)
         {
             _topRow++;
             fullNeedsDisplay = true;
@@ -6150,7 +6115,7 @@ public class TextView : View
         else
         {
             // BUGBUG: customized rect aren't supported now because the Redraw isn't using the Intersect method.
-            //SetNeedsDisplay (new (0, currentRow - topRow, 2, Frame.Height));
+            //SetNeedsDisplay (new (0, currentRow - topRow, 2, Viewport.Height));
             SetNeedsDisplay ();
         }
 
@@ -6481,7 +6446,7 @@ public class TextView : View
         if (_wordWrap && _wrapManager is { })
         {
             _model = _wrapManager.WrapModel (
-                                             _frameWidth,
+                                             Math.Max (Viewport.Width - (ReadOnly ? 0 : 1), 0), // For the cursor on the last column of a line
                                              out int nRow,
                                              out int nCol,
                                              out int nStartRow,

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

@@ -21,7 +21,7 @@ public class TimeField : TextField
     private bool _isShort;
     private TimeSpan _time;
 
-    /// <summary>Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.</summary>
+    /// <summary>Initializes a new instance of <see cref="TimeField"/>.</summary>
     public TimeField ()
     {
         CultureInfo cultureInfo = CultureInfo.CurrentCulture;

+ 10 - 9
Terminal.Gui/Views/Toplevel.cs

@@ -1,5 +1,3 @@
-using System.Net.Mime;
-
 namespace Terminal.Gui;
 
 /// <summary>
@@ -23,7 +21,7 @@ namespace Terminal.Gui;
 public partial class Toplevel : View
 {
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Toplevel"/> class with <see cref="LayoutStyle.Computed"/> layout,
+    ///     Initializes a new instance of the <see cref="Toplevel"/> class,
     ///     defaulting to full screen. The <see cref="View.Width"/> and <see cref="View.Height"/> properties will be set to the
     ///     dimensions of the terminal using <see cref="Dim.Fill"/>.
     /// </summary>
@@ -108,7 +106,7 @@ public partial class Toplevel : View
                    );
 
         // Default keybindings for this view
-        KeyBindings.Add (Application.QuitKey, Command.QuitToplevel);
+        KeyBindings.Add (Application.QuitKey, KeyBindingScope.Application, Command.QuitToplevel);
 
         KeyBindings.Add (Key.CursorRight, Command.NextView);
         KeyBindings.Add (Key.CursorDown, Command.NextView);
@@ -120,12 +118,17 @@ public partial class Toplevel : View
         KeyBindings.Add (Key.Tab.WithCtrl, Command.NextViewOrTop);
         KeyBindings.Add (Key.Tab.WithShift.WithCtrl, Command.PreviousViewOrTop);
 
-        KeyBindings.Add (Key.F5, Command.Refresh);
+        // TODO: Refresh Key should be configurable
+        KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh);
         KeyBindings.Add (Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix
         KeyBindings.Add (Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix
 
+        if (Environment.OSVersion.Platform == PlatformID.Unix)
+        {
+            KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
+        }
+
 #if UNIX_KEY_BINDINGS
-        KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
         KeyBindings.Add (Key.L.WithCtrl, Command.Refresh); // Unix
         KeyBindings.Add (Key.F.WithCtrl, Command.NextView); // Unix
         KeyBindings.Add (Key.I.WithCtrl, Command.NextView); // Unix
@@ -397,9 +400,7 @@ public partial class Toplevel : View
         }
 
         if ((superView != top || top?.SuperView is { } || (top != Application.Top && top.Modal) || (top?.SuperView is null && top.IsOverlapped))
-
-            // BUGBUG: Prevously PositionToplevel required LayotuStyle.Computed
-            && (top.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y) /*&& top.LayoutStyle == LayoutStyle.Computed*/)
+            && (top.Frame.X + top.Frame.Width > maxWidth || ny > top.Frame.Y))
         {
             if ((top.X is null || top.X is PosAbsolute) && top.Frame.X != nx)
             {

+ 1 - 2
Terminal.Gui/Views/Window.cs

@@ -15,8 +15,7 @@ namespace Terminal.Gui;
 public class Window : Toplevel
 {
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Window"/> class using <see cref="LayoutStyle.Computed"/>
-    ///     positioning.
+    ///     Initializes a new instance of the <see cref="Window"/> class.
     /// </summary>
     public Window ()
     {

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

@@ -54,28 +54,10 @@ public class Wizard : Dialog
     private readonly LinkedList<WizardStep> _steps = new ();
     private WizardStep _currentStep;
     private bool _finishedPressed;
-
-    ///// <summary>
-    ///// The title of the Wizard, shown at the top of the Wizard with " - currentStep.Title" appended.
-    ///// </summary>
-    ///// <remarks>
-    ///// The Title is only displayed when the <see cref="Wizard"/> <see cref="Wizard.Modal"/> is set to <c>false</c>.
-    ///// </remarks>
-    //public new string Title {
-    //	get {
-    //		// The base (Dialog) Title holds the full title ("Wizard Title - Step Title")
-    //		return base.Title;
-    //	}
-    //	set {
-    //		wizardTitle = value;
-    //		base.Title = $"{wizardTitle}{(steps.Count > 0 && currentStep is { } ? " - " + currentStep.Title : string.Empty)}";
-    //	}
-    //}
     private string _wizardTitle = string.Empty;
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/>
-    ///     positioning.
+    ///     Initializes a new instance of the <see cref="Wizard"/> class.
     /// </summary>
     /// <remarks>
     ///     The Wizard will be vertically and horizontally centered in the container. After initialization use <c>X</c>,
@@ -83,9 +65,9 @@ public class Wizard : Dialog
     /// </remarks>
     public Wizard ()
     {
-        // Using Justify causes the Back and Next buttons to be hard justified against
-        // the left and right edge
-        ButtonAlignment = ButtonAlignments.Justify;
+        // TODO: LastEndRestStart will enable a "Quit" button to always appear at the far left
+        ButtonAlignment = Alignment.Start;
+        ButtonAlignmentModes |= AlignmentModes.IgnoreFirstOrLast;
         BorderStyle = LineStyle.Double;
 
         //// Add a horiz separator

+ 1 - 2
Terminal.Gui/Views/Wizard/WizardStep.cs

@@ -40,8 +40,7 @@ public class WizardStep : FrameView
     private readonly TextView _helpTextView = new ();
 
     /// <summary>
-    ///     Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/>
-    ///     positioning.
+    ///     Initializes a new instance of the <see cref="Wizard"/> class.
     /// </summary>
     public WizardStep ()
     {

+ 2 - 0
Terminal.sln.DotSettings

@@ -391,6 +391,7 @@
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=53eecf85_002Dd821_002D40e8_002Dac97_002Dfdb734542b84/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=70345118_002D4b40_002D4ece_002D937c_002Dbbeb7a0b2e70/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=c873eafb_002Dd57f_002D481d_002D8c93_002D77f6863c2f88/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
+	<s:String x:Key="/Default/Environment/InlayHints/GeneralInlayHintsOptions/DefaultMode/@EntryValue">PushToShowHints</s:String>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
@@ -439,5 +440,6 @@
 	<s:String x:Key="/Default/PatternsAndTemplates/Todo/TodoPatterns/=B0C2F2A1AF61DA42BBF270980E3DCEF7/Name/@EntryValue">Concurrency Issue</s:String>
 	<s:String x:Key="/Default/PatternsAndTemplates/Todo/TodoPatterns/=B0C2F2A1AF61DA42BBF270980E3DCEF7/Pattern/@EntryValue">(?&lt;=\W|^)(?&lt;TAG&gt;CONCURRENCY:)(\W|$)(.*)</s:String>
 	<s:String x:Key="/Default/PatternsAndTemplates/Todo/TodoPatterns/=B0C2F2A1AF61DA42BBF270980E3DCEF7/TodoIconStyle/@EntryValue">Warning</s:String>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Justifier/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=unsynchronized/@EntryIndexedValue">True</s:Boolean>
 </wpf:ResourceDictionary>

+ 4 - 3
UICatalog/KeyBindingsDialog.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
 using Terminal.Gui;
 
@@ -10,7 +11,7 @@ internal class KeyBindingsDialog : Dialog
     // TODO: Update to use Key instead of KeyCode
     private static readonly Dictionary<Command, KeyCode> CurrentBindings = new ();
 
-    private readonly Command [] _commands;
+    private readonly ObservableCollection<Command> _commands;
     private readonly ListView _commandsListView;
     private readonly Label _keyLabel;
 
@@ -26,13 +27,13 @@ internal class KeyBindingsDialog : Dialog
         }
 
         // known commands that views can support
-        _commands = Enum.GetValues (typeof (Command)).Cast<Command> ().ToArray ();
+        _commands = new (Enum.GetValues (typeof (Command)).Cast<Command> ().ToArray ());
 
         _commandsListView = new ListView
         {
             Width = Dim.Percent (50),
             Height = Dim.Percent (100) - 1,
-            Source = new ListWrapper (_commands),
+            Source = new ListWrapper<Command> (_commands),
             SelectedItem = 0
         };
 

+ 1 - 0
UICatalog/Resources/config.json

@@ -32,6 +32,7 @@
   "Themes": [
     {
       "UI Catalog Theme": {
+        "Dialog.DefaultButtonAlignment": "Fill",
         "ColorSchemes": [
           {
             "UI Catalog Scheme": {

+ 21 - 18
UICatalog/Scenario.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
 using System.Linq;
 using Terminal.Gui;
 
@@ -97,7 +98,7 @@ public class Scenario : IDisposable
     ///     <see cref="ScenarioMetadata.Name"/>.
     ///     https://stackoverflow.com/questions/5411694/get-all-inherited-classes-of-an-abstract-class
     /// </summary>
-    public static List<Scenario> GetScenarios ()
+    public static ObservableCollection<Scenario> GetScenarios ()
     {
         List<Scenario> objects = new ();
 
@@ -113,7 +114,7 @@ public class Scenario : IDisposable
             _maxScenarioNameLen = Math.Max (_maxScenarioNameLen, scenario.GetName ().Length + 1);
         }
 
-        return objects.OrderBy (s => s.GetName ()).ToList ();
+        return new (objects.OrderBy (s => s.GetName ()).ToList ());
     }
 
     /// <summary>
@@ -239,24 +240,26 @@ public class Scenario : IDisposable
     #endregion IDispose
 
     /// <summary>Returns a list of all Categories set by all of the <see cref="Scenario"/>s defined in the project.</summary>
-    internal static List<string> GetAllCategories ()
+    internal static ObservableCollection<string> GetAllCategories ()
     {
-        List<string> categories = new ();
-
-        categories = typeof (Scenario).Assembly.GetTypes ()
-                                      .Where (
-                                              myType => myType.IsClass
-                                                        && !myType.IsAbstract
-                                                        && myType.IsSubclassOf (typeof (Scenario)))
-                                      .Select (type => System.Attribute.GetCustomAttributes (type).ToList ())
-                                      .Aggregate (
-                                                  categories,
-                                                  (current, attrs) => current
-                                                                      .Union (attrs.Where (a => a is ScenarioCategory).Select (a => ((ScenarioCategory)a).Name))
-                                                                      .ToList ());
+        List<string> aCategories = [];
+
+        aCategories = typeof (Scenario).Assembly.GetTypes ()
+                                       .Where (
+                                               myType => myType.IsClass
+                                                         && !myType.IsAbstract
+                                                         && myType.IsSubclassOf (typeof (Scenario)))
+                                       .Select (type => System.Attribute.GetCustomAttributes (type).ToList ())
+                                       .Aggregate (
+                                                   aCategories,
+                                                   (current, attrs) => current
+                                                                       .Union (
+                                                                               attrs.Where (a => a is ScenarioCategory)
+                                                                                    .Select (a => ((ScenarioCategory)a).Name))
+                                                                       .ToList ());
 
         // Sort
-        categories = categories.OrderBy (c => c).ToList ();
+        ObservableCollection<string> categories = new (aCategories.OrderBy (c => c).ToList ());
 
         // Put "All" at the top
         categories.Insert (0, "All Scenarios");
@@ -264,7 +267,7 @@ public class Scenario : IDisposable
         return categories;
     }
 
-    /// <summary>Defines the category names used to catagorize a <see cref="Scenario"/></summary>
+    /// <summary>Defines the category names used to categorize a <see cref="Scenario"/></summary>
     [AttributeUsage (AttributeTargets.Class, AllowMultiple = true)]
     public class ScenarioCategory (string Name) : System.Attribute
     {

+ 3 - 2
UICatalog/Scenarios/ASCIICustomButton.cs

@@ -235,6 +235,7 @@ public class ASCIICustomButtonTest : Scenario
                 pages++;
             }
 
+            // BUGBUG: set_ContentSize is supposed to be `protected`. 
             _scrollView.SetContentSize (new (25, pages * BUTTONS_ON_PAGE * BUTTON_HEIGHT));
 
             if (_smallerWindow)
@@ -269,7 +270,7 @@ public class ASCIICustomButtonTest : Scenario
                 case KeyCode.End:
                     _scrollView.ContentOffset = new Point (
                                                            _scrollView.ContentOffset.X,
-                                                           -(_scrollView.ContentSize.Height
+                                                           -(_scrollView.GetContentSize ().Height
                                                              - _scrollView.Frame.Height
                                                              + (_scrollView.ShowHorizontalScrollIndicator ? 1 : 0))
                                                           );
@@ -287,7 +288,7 @@ public class ASCIICustomButtonTest : Scenario
                                                            Math.Max (
                                                                      _scrollView.ContentOffset.Y
                                                                      - _scrollView.Frame.Height,
-                                                                     -(_scrollView.ContentSize.Height
+                                                                     -(_scrollView.GetContentSize ().Height
                                                                        - _scrollView.Frame.Height
                                                                        + (_scrollView.ShowHorizontalScrollIndicator
                                                                               ? 1

+ 236 - 0
UICatalog/Scenarios/AdornmentEditor.cs

@@ -0,0 +1,236 @@
+using System;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     Provides a composable UI for editing the settings of an Adornment.
+/// </summary>
+public class AdornmentEditor : View
+{
+    private readonly ColorPicker _backgroundColorPicker = new ()
+    {
+        Title = "_BG",
+        BoxWidth = 1,
+        BoxHeight = 1,
+        BorderStyle = LineStyle.Single,
+        SuperViewRendersLineCanvas = true,
+        Enabled = false
+    };
+
+    private readonly ColorPicker _foregroundColorPicker = new ()
+    {
+        Title = "_FG",
+        BoxWidth = 1,
+        BoxHeight = 1,
+        BorderStyle = LineStyle.Single,
+        SuperViewRendersLineCanvas = true,
+        Enabled = false
+    };
+
+    private Adornment _adornment;
+    public Adornment AdornmentToEdit
+    {
+        get => _adornment;
+        set
+        {
+            if (value == _adornment)
+            {
+                return;
+            }
+
+            _adornment = value;
+
+            foreach (var subview in Subviews)
+            {
+                subview.Enabled = _adornment is { };
+            }
+
+            if (_adornment is null)
+            {
+                return;
+            }
+
+            if (IsInitialized)
+            {
+                _topEdit.Value = _adornment.Thickness.Top;
+                _leftEdit.Value = _adornment.Thickness.Left;
+                _bottomEdit.Value = _adornment.Thickness.Bottom;
+                _rightEdit.Value = _adornment.Thickness.Right;
+
+                _adornment.Initialized += (sender, args) =>
+                                          {
+                                              var cs = _adornment.ColorScheme;
+                                              _foregroundColorPicker.SelectedColor = cs.Normal.Foreground.GetClosestNamedColor ();
+                                              _backgroundColorPicker.SelectedColor = cs.Normal.Background.GetClosestNamedColor ();
+
+                                          };
+            }
+
+            OnAdornmentChanged ();
+        }
+    }
+
+    public event EventHandler<EventArgs> AdornmentChanged;
+
+    public void OnAdornmentChanged ()
+    {
+        AdornmentChanged?.Invoke (this, EventArgs.Empty);
+    }
+
+    private Buttons.NumericUpDown<int> _topEdit;
+    private Buttons.NumericUpDown<int> _leftEdit;
+    private Buttons.NumericUpDown<int> _bottomEdit;
+    private Buttons.NumericUpDown<int> _rightEdit;
+
+    public AdornmentEditor ()
+    {
+        Width = Dim.Auto (DimAutoStyle.Content);
+        Height = Dim.Auto (DimAutoStyle.Content);
+
+        BorderStyle = LineStyle.Dashed;
+        Initialized += AdornmentEditor_Initialized;
+    }
+
+    private void AdornmentEditor_Initialized (object sender, EventArgs e)
+    {
+        ExpanderButton expandButton;
+        Border.Add (expandButton = new ExpanderButton ());
+
+        _topEdit = new ()
+        {
+            X = Pos.Center (), Y = 0,
+            Enabled = false
+        };
+
+        _topEdit.ValueChanging += Top_ValueChanging;
+        Add (_topEdit);
+
+        _leftEdit = new ()
+        {
+            X = Pos.Left (_topEdit) - Pos.Func (() => _topEdit.Digits) - 2, Y = Pos.Bottom (_topEdit),
+            Enabled = false
+        };
+
+        _leftEdit.ValueChanging += Left_ValueChanging;
+        Add (_leftEdit);
+
+        _rightEdit = new ()
+        {
+            X = Pos.Right (_leftEdit) + 5, Y = Pos.Bottom (_topEdit),
+            Enabled = false
+        };
+
+        _rightEdit.ValueChanging += Right_ValueChanging;
+        Add (_rightEdit);
+
+        _bottomEdit = new ()
+        {
+            X = Pos.Center (), Y = Pos.Bottom (_leftEdit),
+            Enabled = false
+        };
+
+        _bottomEdit.ValueChanging += Bottom_ValueChanging;
+        Add (_bottomEdit);
+
+        var copyTop = new Button
+        {
+            X = Pos.Center (), Y = Pos.Bottom (_bottomEdit), Text = "Cop_y Top",
+            Enabled = false
+        };
+
+        copyTop.Accept += (s, e) =>
+                          {
+                              AdornmentToEdit.Thickness = new (_topEdit.Value);
+                              _leftEdit.Value = _rightEdit.Value = _bottomEdit.Value = _topEdit.Value;
+                          };
+        Add (copyTop);
+
+        // Foreground ColorPicker.
+        _foregroundColorPicker.X = 0;
+        _foregroundColorPicker.Y = Pos.Bottom (copyTop);
+
+        _foregroundColorPicker.ColorChanged += ColorPickerColorChanged ();
+        Add (_foregroundColorPicker);
+
+        // Background ColorPicker.
+        _backgroundColorPicker.X = Pos.Right (_foregroundColorPicker) - 1;
+        _backgroundColorPicker.Y = Pos.Top (_foregroundColorPicker);
+
+        _backgroundColorPicker.ColorChanged += ColorPickerColorChanged ();
+        Add (_backgroundColorPicker);
+
+        _topEdit.Value = AdornmentToEdit?.Thickness.Top ?? 0;
+        _leftEdit.Value = AdornmentToEdit?.Thickness.Left ?? 0;
+        _rightEdit.Value = AdornmentToEdit?.Thickness.Right ?? 0;
+        _bottomEdit.Value = AdornmentToEdit?.Thickness.Bottom ?? 0;
+
+        foreach (var subview in Subviews)
+        {
+            subview.Enabled = AdornmentToEdit is { };
+        }
+    }
+
+    private EventHandler<ColorEventArgs> ColorPickerColorChanged ()
+    {
+        return (o, a) =>
+               {
+                   if (AdornmentToEdit is null)
+                   {
+                       return;
+                   }
+                   AdornmentToEdit.ColorScheme = new (AdornmentToEdit.ColorScheme)
+                   {
+                       Normal = new (_foregroundColorPicker.SelectedColor, _backgroundColorPicker.SelectedColor)
+                   };
+               };
+    }
+
+    private void Top_ValueChanging (object sender, StateEventArgs<int> e)
+    {
+        if (e.NewValue < 0 || AdornmentToEdit is null)
+        {
+            e.Cancel = true;
+
+            return;
+        }
+
+        AdornmentToEdit.Thickness = new (AdornmentToEdit.Thickness.Left, e.NewValue, AdornmentToEdit.Thickness.Right, AdornmentToEdit.Thickness.Bottom);
+    }
+
+    private void Left_ValueChanging (object sender, StateEventArgs<int> e)
+    {
+        if (e.NewValue < 0 || AdornmentToEdit is null)
+        {
+            e.Cancel = true;
+
+            return;
+        }
+
+        AdornmentToEdit.Thickness = new (e.NewValue, AdornmentToEdit.Thickness.Top, AdornmentToEdit.Thickness.Right, AdornmentToEdit.Thickness.Bottom);
+    }
+
+    private void Right_ValueChanging (object sender, StateEventArgs<int> e)
+    {
+        if (e.NewValue < 0 || AdornmentToEdit is null)
+        {
+            e.Cancel = true;
+
+            return;
+        }
+
+        AdornmentToEdit.Thickness = new (AdornmentToEdit.Thickness.Left, AdornmentToEdit.Thickness.Top, e.NewValue, AdornmentToEdit.Thickness.Bottom);
+    }
+
+    private void Bottom_ValueChanging (object sender, StateEventArgs<int> e)
+    {
+        if (e.NewValue < 0 || AdornmentToEdit is null)
+        {
+            e.Cancel = true;
+
+            return;
+        }
+
+        AdornmentToEdit.Thickness = new (AdornmentToEdit.Thickness.Left, AdornmentToEdit.Thickness.Top, AdornmentToEdit.Thickness.Right, e.NewValue);
+    }
+}

+ 12 - 443
UICatalog/Scenarios/Adornments.cs

@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Terminal.Gui;
+using Terminal.Gui;
 
 namespace UICatalog.Scenarios;
 
@@ -10,27 +7,32 @@ namespace UICatalog.Scenarios;
 [ScenarioCategory ("Borders")]
 public class Adornments : Scenario
 {
-    private ViewDiagnosticFlags _diagnosticFlags;
-
     public override void Main ()
     {
         Application.Init ();
 
-        _diagnosticFlags = View.Diagnostics;
-
         Window app = new ()
         {
             Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"
         };
 
-        var editor = new AdornmentsEditor ();
+        var editor = new AdornmentsEditor
+        {
+            AutoSelectViewToEdit = true,
+            // This is for giggles, to show that the editor can be moved around.
+            Arrangement = ViewArrangement.Movable,
+            X = Pos.AnchorEnd()
+
+        };
+        editor.Border.Thickness = new Thickness (1, 3, 1, 1);
+
         app.Add (editor);
 
         var window = new Window
         {
             Title = "The _Window",
             Arrangement = ViewArrangement.Movable,
-            X = Pos.Right (editor),
+           // X = Pos.Center (),
             Width = Dim.Percent (60),
             Height = Dim.Percent (80)
         };
@@ -126,442 +128,9 @@ public class Adornments : Scenario
 #endif
                               };
 
-        app.Closed += (s, e) => View.Diagnostics = _diagnosticFlags;
-
         Application.Run (app);
         app.Dispose ();
 
         Application.Shutdown ();
     }
-
-    /// <summary>
-    ///     Provides a composable UI for editing the settings of an Adornment.
-    /// </summary>
-    public class AdornmentEditor : View
-    {
-        private readonly ColorPicker _backgroundColorPicker = new ()
-        {
-            Title = "_BG",
-            BoxWidth = 1,
-            BoxHeight = 1,
-            BorderStyle = LineStyle.Single,
-            SuperViewRendersLineCanvas = true
-        };
-
-        private readonly ColorPicker _foregroundColorPicker = new ()
-        {
-            Title = "_FG",
-            BoxWidth = 1,
-            BoxHeight = 1,
-            BorderStyle = LineStyle.Single,
-            SuperViewRendersLineCanvas = true
-        };
-
-        private Buttons.NumericUpDown<int> _topEdit;
-        private Buttons.NumericUpDown<int> _leftEdit;
-        private Buttons.NumericUpDown<int> _bottomEdit;
-        private Buttons.NumericUpDown<int> _rightEdit;
-        private Thickness _thickness;
-        private bool _isUpdating;
-
-        public AdornmentEditor ()
-        {
-            Margin.Thickness = new (0);
-            BorderStyle = LineStyle.Double;
-            Initialized += AdornmentEditor_Initialized;
-        }
-
-        public Attribute Color
-        {
-            get => new (_foregroundColorPicker.SelectedColor, _backgroundColorPicker.SelectedColor);
-            set
-            {
-                _foregroundColorPicker.SelectedColor = value.Foreground.GetClosestNamedColor ();
-                _backgroundColorPicker.SelectedColor = value.Background.GetClosestNamedColor ();
-            }
-        }
-
-        public Thickness Thickness
-        {
-            get => _thickness;
-            set
-            {
-                if (_isUpdating)
-                {
-                    return;
-                }
-
-                _thickness = value;
-                ThicknessChanged?.Invoke (this, new () { Thickness = Thickness });
-
-                if (IsInitialized)
-                {
-                    _isUpdating = true;
-                    _topEdit.Value = _thickness.Top;
-                    _leftEdit.Value = _thickness.Left;
-                    _rightEdit.Value = _thickness.Right;
-                    _bottomEdit.Value = _thickness.Bottom;
-
-                    _isUpdating = false;
-                }
-            }
-        }
-
-        public event EventHandler<Attribute> AttributeChanged;
-        public event EventHandler<ThicknessEventArgs> ThicknessChanged;
-
-        private void AdornmentEditor_Initialized (object sender, EventArgs e)
-        {
-            SuperViewRendersLineCanvas = true;
-
-            _topEdit = new ()
-            {
-                X = Pos.Center (), Y = 0
-            };
-
-            _topEdit.ValueChanging += Top_ValueChanging;
-            Add (_topEdit);
-
-            _leftEdit = new ()
-            {
-                X = Pos.Left (_topEdit) - Pos.Func (() => _topEdit.Digits) - 2, Y = Pos.Bottom (_topEdit)
-            };
-
-            _leftEdit.ValueChanging += Left_ValueChanging;
-            Add (_leftEdit);
-
-            _rightEdit = new () { X = Pos.Right (_leftEdit) + 5, Y = Pos.Bottom (_topEdit) };
-
-            _rightEdit.ValueChanging += Right_ValueChanging;
-            Add (_rightEdit);
-
-            _bottomEdit = new () { X = Pos.Center (), Y = Pos.Bottom (_leftEdit) };
-
-            _bottomEdit.ValueChanging += Bottom_ValueChanging;
-            Add (_bottomEdit);
-
-            var copyTop = new Button { X = Pos.Center (), Y = Pos.Bottom (_bottomEdit), Text = "Cop_y Top" };
-
-            copyTop.Accept += (s, e) =>
-                              {
-                                  Thickness = new (Thickness.Top);
-                                  _leftEdit.Value = _rightEdit.Value = _bottomEdit.Value = _topEdit.Value;
-                              };
-            Add (copyTop);
-
-            // Foreground ColorPicker.
-            _foregroundColorPicker.X = -1;
-            _foregroundColorPicker.Y = Pos.Bottom (copyTop);
-            _foregroundColorPicker.SelectedColor = Color.Foreground.GetClosestNamedColor ();
-
-            _foregroundColorPicker.ColorChanged += (o, a) =>
-                                                       AttributeChanged?.Invoke (
-                                                                                 this,
-                                                                                 new (
-                                                                                      _foregroundColorPicker.SelectedColor,
-                                                                                      _backgroundColorPicker.SelectedColor
-                                                                                     )
-                                                                                );
-            Add (_foregroundColorPicker);
-
-            // Background ColorPicker.
-            _backgroundColorPicker.X = Pos.Right (_foregroundColorPicker) - 1;
-            _backgroundColorPicker.Y = Pos.Top (_foregroundColorPicker);
-            _backgroundColorPicker.SelectedColor = Color.Background.GetClosestNamedColor ();
-
-            _backgroundColorPicker.ColorChanged += (o, a) =>
-                                                       AttributeChanged?.Invoke (
-                                                                                 this,
-                                                                                 new (
-                                                                                      _foregroundColorPicker.SelectedColor,
-                                                                                      _backgroundColorPicker.SelectedColor
-                                                                                     )
-                                                                                );
-            Add (_backgroundColorPicker);
-
-            _topEdit.Value = Thickness.Top;
-            _leftEdit.Value = Thickness.Left;
-            _rightEdit.Value = Thickness.Right;
-            _bottomEdit.Value = Thickness.Bottom;
-
-            Width = Dim.Auto () - 1;
-            Height = Dim.Auto () - 1;
-            LayoutSubviews ();
-        }
-
-        private void Top_ValueChanging (object sender, StateEventArgs<int> e)
-        {
-            if (e.NewValue < 0)
-            {
-                e.Cancel = true;
-
-                return;
-            }
-
-            Thickness.Top = e.NewValue;
-        }
-
-        private void Left_ValueChanging (object sender, StateEventArgs<int> e)
-        {
-            if (e.NewValue < 0)
-            {
-                e.Cancel = true;
-
-                return;
-            }
-
-            Thickness.Left = e.NewValue;
-        }
-
-        private void Right_ValueChanging (object sender, StateEventArgs<int> e)
-        {
-            if (e.NewValue < 0)
-            {
-                e.Cancel = true;
-
-                return;
-            }
-
-            Thickness.Right = e.NewValue;
-        }
-
-        private void Bottom_ValueChanging (object sender, StateEventArgs<int> e)
-        {
-            if (e.NewValue < 0)
-            {
-                e.Cancel = true;
-
-                return;
-            }
-
-            Thickness.Bottom = e.NewValue;
-        }
-    }
-
-    /// <summary>
-    ///     Provides an editor UI for the Margin, Border, and Padding of a View.
-    /// </summary>
-    public class AdornmentsEditor : View
-    {
-        private AdornmentEditor _borderEditor;
-        private CheckBox _diagCheckBox;
-        private AdornmentEditor _marginEditor;
-        private string _origTitle = string.Empty;
-        private AdornmentEditor _paddingEditor;
-        private View _viewToEdit;
-
-        public AdornmentsEditor ()
-        {
-            ColorScheme = Colors.ColorSchemes ["Dialog"];
-
-            // TOOD: Use Dim.Auto
-            Width = 36;
-            Height = Dim.Fill ();
-        }
-
-        public View ViewToEdit
-        {
-            get => _viewToEdit;
-            set
-            {
-                _origTitle = value.Title;
-                _viewToEdit = value;
-
-                _marginEditor = new ()
-                {
-                    X = 0,
-                    Y = 0,
-                    Title = "_Margin",
-                    Thickness = _viewToEdit.Margin.Thickness,
-                    Color = new (_viewToEdit.Margin.ColorScheme?.Normal ?? ColorScheme.Normal),
-                    SuperViewRendersLineCanvas = true
-                };
-                _marginEditor.ThicknessChanged += Editor_ThicknessChanged;
-                _marginEditor.AttributeChanged += Editor_AttributeChanged;
-                Add (_marginEditor);
-
-                _borderEditor = new ()
-                {
-                    X = Pos.Left (_marginEditor),
-                    Y = Pos.Bottom (_marginEditor),
-                    Title = "B_order",
-                    Thickness = _viewToEdit.Border.Thickness,
-                    Color = new (_viewToEdit.Border.ColorScheme?.Normal ?? ColorScheme.Normal),
-                    SuperViewRendersLineCanvas = true
-                };
-                _borderEditor.ThicknessChanged += Editor_ThicknessChanged;
-                _borderEditor.AttributeChanged += Editor_AttributeChanged;
-                Add (_borderEditor);
-
-                List<LineStyle> borderStyleEnum = Enum.GetValues (typeof (LineStyle)).Cast<LineStyle> ().ToList ();
-
-                var rbBorderStyle = new RadioGroup
-                {
-                    X = Pos.Right (_borderEditor) - 1,
-                    Y = Pos.Top (_borderEditor),
-                    SelectedItem = (int)_viewToEdit.Border.LineStyle,
-                    BorderStyle = LineStyle.Double,
-                    Title = "Border St_yle",
-                    SuperViewRendersLineCanvas = true,
-                    RadioLabels = borderStyleEnum.Select (
-                                                          e => e.ToString ()
-                                                         )
-                                                 .ToArray ()
-                };
-                Add (rbBorderStyle);
-
-                rbBorderStyle.SelectedItemChanged += (s, e) =>
-                                                     {
-                                                         LineStyle prevBorderStyle = _viewToEdit.BorderStyle;
-                                                         _viewToEdit.Border.LineStyle = (LineStyle)e.SelectedItem;
-
-                                                         if (_viewToEdit.Border.LineStyle == LineStyle.None)
-                                                         {
-                                                             _viewToEdit.Border.Thickness = new (0);
-                                                         }
-                                                         else if (prevBorderStyle == LineStyle.None && _viewToEdit.Border.LineStyle != LineStyle.None)
-                                                         {
-                                                             _viewToEdit.Border.Thickness = new (1);
-                                                         }
-
-                                                         _borderEditor.Thickness = new (
-                                                                                        _viewToEdit.Border.Thickness.Left,
-                                                                                        _viewToEdit.Border.Thickness.Top,
-                                                                                        _viewToEdit.Border.Thickness.Right,
-                                                                                        _viewToEdit.Border.Thickness.Bottom
-                                                                                       );
-                                                         _viewToEdit.SetNeedsDisplay ();
-                                                         LayoutSubviews ();
-                                                     };
-
-                var ckbTitle = new CheckBox
-                {
-                    BorderStyle = LineStyle.Double,
-                    X = Pos.Left (_borderEditor),
-                    Y = Pos.Bottom (_borderEditor) - 1,
-
-                    //Width = Dim.Width (_borderEditor),
-                    Checked = true,
-                    SuperViewRendersLineCanvas = true,
-                    Text = "Show Title"
-                };
-
-                ckbTitle.Toggled += (sender, args) =>
-                                    {
-                                        if (ckbTitle.Checked == true)
-                                        {
-                                            //_viewToEdit.Title = _origTitle;
-                                        }
-                                        else
-                                        {
-                                            _viewToEdit.Title = string.Empty;
-                                        }
-                                    };
-                Add (ckbTitle);
-
-                _paddingEditor = new ()
-                {
-                    X = Pos.Left (_borderEditor),
-                    Y = Pos.Bottom (rbBorderStyle),
-                    Title = "_Padding",
-                    Thickness = _viewToEdit.Padding.Thickness,
-                    Color = new (_viewToEdit.Padding.ColorScheme?.Normal ?? ColorScheme.Normal),
-                    SuperViewRendersLineCanvas = true
-                };
-                _paddingEditor.ThicknessChanged += Editor_ThicknessChanged;
-                _paddingEditor.AttributeChanged += Editor_AttributeChanged;
-                Add (_paddingEditor);
-
-                _diagCheckBox = new () { Text = "_Diagnostics", Y = Pos.Bottom (_paddingEditor) };
-                _diagCheckBox.Checked = Diagnostics != ViewDiagnosticFlags.Off;
-
-                _diagCheckBox.Toggled += (s, e) =>
-                                         {
-                                             if (e.NewValue == true)
-                                             {
-                                                 Diagnostics =
-                                                     ViewDiagnosticFlags.Padding | ViewDiagnosticFlags.Ruler;
-                                             }
-                                             else
-                                             {
-                                                 Diagnostics = ViewDiagnosticFlags.Off;
-                                             }
-                                         };
-
-                Add (_diagCheckBox);
-
-                _viewToEdit.LayoutComplete += (s, e) =>
-                                              {
-                                                  if (ckbTitle.Checked == true)
-                                                  {
-                                                      _viewToEdit.Title = _origTitle;
-                                                  }
-                                                  else
-                                                  {
-                                                      _viewToEdit.Title = string.Empty;
-                                                  }
-                                              };
-            }
-        }
-
-        private void Editor_AttributeChanged (object sender, Attribute attr)
-        {
-            switch (sender.ToString ())
-            {
-                case var s when s == _marginEditor.ToString ():
-                    _viewToEdit.Margin.ColorScheme = new (_viewToEdit.Margin.ColorScheme) { Normal = attr };
-
-                    break;
-                case var s when s == _borderEditor.ToString ():
-                    _viewToEdit.Border.ColorScheme = new (_viewToEdit.Border.ColorScheme) { Normal = attr };
-
-                    break;
-                case var s when s == _paddingEditor.ToString ():
-                    _viewToEdit.Padding.ColorScheme =
-                        new (_viewToEdit.Padding.ColorScheme) { Normal = attr };
-
-                    break;
-            }
-        }
-
-        private void Editor_ThicknessChanged (object sender, ThicknessEventArgs e)
-        {
-            try
-            {
-                switch (sender.ToString ())
-                {
-                    case var s when s == _marginEditor.ToString ():
-                        _viewToEdit.Margin.Thickness = e.Thickness;
-
-                        break;
-                    case var s when s == _borderEditor.ToString ():
-                        _viewToEdit.Border.Thickness = e.Thickness;
-
-                        break;
-                    case var s when s == _paddingEditor.ToString ():
-                        _viewToEdit.Padding.Thickness = e.Thickness;
-
-                        break;
-                }
-            }
-            catch
-            {
-                switch (sender.ToString ())
-                {
-                    case var s when s == _marginEditor.ToString ():
-                        _viewToEdit.Margin.Thickness = e.PreviousThickness;
-
-                        break;
-                    case var s when s == _borderEditor.ToString ():
-                        _viewToEdit.Border.Thickness = e.PreviousThickness;
-
-                        break;
-                    case var s when s == _paddingEditor.ToString ():
-                        _viewToEdit.Padding.Thickness = e.PreviousThickness;
-
-                        break;
-                }
-            }
-        }
-    }
 }

Some files were not shown because too many files changed in this diff