Browse Source

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

Tig 1 day ago
parent
commit
d89b85231e

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

@@ -347,6 +347,8 @@ public class Menus : Scenario
             };
 
             ContextMenu.EnableForDesign (ref host);
+            Application.Popover.Register (ContextMenu);
+
             ContextMenu.Visible = false;
 
             // Demo of PopoverMenu as a context menu

+ 38 - 28
Terminal.Gui/App/IPopover.cs

@@ -8,54 +8,64 @@ namespace Terminal.Gui.App;
 ///     <para>
 ///         A popover is a transient UI element that appears above other content to display contextual information or UI,
 ///         such as menus, tooltips, or dialogs.
-///         Popovers are managed by <see cref="ApplicationPopover"/> and are typically shown using
-///         <see cref="ApplicationPopover.Show"/>.
 ///     </para>
 ///     <para>
-///         Popovers are not modal; they do not block input to the rest of the application, but they do receive focus and
-///         input events while visible.
-///         When a popover is shown, it is responsible for handling its own layout and content.
+///         <b>IMPORTANT:</b> Popovers must be registered with <see cref="Application.Popover"/> using
+///         <see cref="ApplicationPopover.Register"/> before they can be shown with <see cref="ApplicationPopover.Show"/>.
 ///     </para>
 ///     <para>
+///         <b>Lifecycle:</b><br/>
+///         When registered, the popover's lifetime is managed by the application. Registered popovers are
+///         automatically disposed when <see cref="Application.Shutdown"/> is called. Call
+///         <see cref="ApplicationPopover.DeRegister"/> to manage the lifetime directly.
+///     </para>
+///     <para>
+///         <b>Visibility and Hiding:</b><br/>
 ///         Popovers are automatically hidden when:
-///         <list type="bullet">
-///             <item>The user clicks outside the popover (unless occluded by a subview of the popover).</item>
-///             <item>The user presses <see cref="Application.QuitKey"/> (typically <c>Esc</c>).</item>
-///             <item>Another popover is shown.</item>
-///         </list>
 ///     </para>
+///     <list type="bullet">
+///         <item>The user clicks outside the popover (unless clicking on a subview).</item>
+///         <item>The user presses <see cref="Application.QuitKey"/> (typically <c>Esc</c>).</item>
+///         <item>Another popover is shown.</item>
+///         <item><see cref="View.Visible"/> is set to <see langword="false"/>.</item>
+///     </list>
 ///     <para>
 ///         <b>Focus and Input:</b><br/>
-///         When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a
-///         subview),
-///         presses <see cref="Application.QuitKey"/>, or another popover is shown, the popover will be hidden
-///         automatically.
+///         Popovers are not modal but do receive focus and input events while visible.
+///         Registered popovers receive keyboard events even when not visible, enabling global hotkey support.
 ///     </para>
 ///     <para>
 ///         <b>Layout:</b><br/>
-///         When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override
-///         this behavior
-///         by setting <see cref="View.Width"/> and <see cref="View.Height"/> in your derived class.
+///         When becoming visible, popovers are automatically laid out to fill the screen by default.
+///         Override <see cref="View.Width"/> and <see cref="View.Height"/> to customize size.
 ///     </para>
 ///     <para>
-///         <b>Mouse:</b><br/>
-///         Popovers are transparent to mouse events (see <see cref="ViewportSettingsFlags.TransparentMouse"/>),
-///         meaning mouse events in a popover that are not also within a subview of the popover will not be captured.
+///         <b>Mouse Events:</b><br/>
+///         Popovers use <see cref="ViewportSettingsFlags.TransparentMouse"/>, meaning mouse events
+///         outside subviews are not captured.
 ///     </para>
 ///     <para>
-///         <b>Custom Popovers:</b><br/>
-///         To create a custom popover, inherit from <see cref="PopoverBaseImpl"/> and add your own content and logic.
+///         <b>Creating Custom Popovers:</b><br/>
+///         Inherit from <see cref="PopoverBaseImpl"/> and add your own content and logic.
 ///     </para>
 /// </remarks>
 public interface IPopover
 {
     /// <summary>
-    ///     Gets or sets the <see cref="Current"/> that this Popover is associated with. If null, it is not associated with
-    ///     any Runnable and will receive all keyboard
-    ///     events from the <see cref="IApplication"/>. If set, it will only receive keyboard events the Runnable would normally
-    ///     receive.
-    ///     When <see cref="ApplicationPopover.Register"/> is called, the <see cref="Current"/> is set to the current
-    ///     <see cref="IApplication.TopRunnableView"/> if not already set.
+    ///     Gets or sets the <see cref="IRunnable"/> that this popover is associated with.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         If <see langword="null"/>, the popover is not associated with any runnable and will receive all keyboard
+    ///         events from the application.
+    ///     </para>
+    ///     <para>
+    ///         If set, the popover will only receive keyboard events when the associated runnable is active.
+    ///     </para>
+    ///     <para>
+    ///         When <see cref="ApplicationPopover.Register"/> is called, this property is automatically set to
+    ///         <see cref="IApplication.TopRunnableView"/> if not already set.
+    ///     </para>
+    /// </remarks>
     IRunnable? Current { get; set; }
 }

+ 39 - 27
Terminal.Gui/App/PopoverBaseImpl.cs

@@ -2,36 +2,36 @@
 namespace Terminal.Gui.App;
 
 /// <summary>
-///     Abstract base class for popover views in Terminal.Gui.
+///     Abstract base class for popover views in Terminal.Gui. Implements <see cref="IPopover"/>.
 /// </summary>
 /// <remarks>
 ///     <para>
-///         <b>Popover Lifecycle:</b><br/>
-///         To display a popover, use <see cref="ApplicationPopover.Show"/>. To hide a popover, either call
-///         <see cref="ApplicationPopover.Hide"/>,
-///         set <see cref="View.Visible"/> to <see langword="false"/>, or show another popover.
+///         <b>IMPORTANT:</b> Popovers must be registered with <see cref="Application.Popover"/> using
+///         <see cref="ApplicationPopover.Register"/> before they can be shown.
 ///     </para>
 ///     <para>
-///         <b>Focus and Input:</b><br/>
-///         When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a
-///         subview),
-///         presses <see cref="Application.QuitKey"/>, or another popover is shown, the popover will be hidden
-///         automatically.
+///         <b>Requirements:</b><br/>
+///         Derived classes must:
 ///     </para>
+///     <list type="bullet">
+///         <item>Set <see cref="View.ViewportSettings"/> to include <see cref="ViewportSettingsFlags.Transparent"/> and <see cref="ViewportSettingsFlags.TransparentMouse"/>.</item>
+///         <item>Add a key binding for <see cref="Command.Quit"/> (typically bound to <see cref="Application.QuitKey"/>).</item>
+///     </list>
 ///     <para>
-///         <b>Layout:</b><br/>
-///         When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override
-///         this behavior
-///         by setting <see cref="View.Width"/> and <see cref="View.Height"/> in your derived class.
+///         <b>Default Behavior:</b><br/>
+///         This base class provides:
 ///     </para>
+///     <list type="bullet">
+///         <item>Fills the screen by default (<see cref="View.Width"/> = <see cref="Dim.Fill"/>, <see cref="View.Height"/> = <see cref="Dim.Fill"/>).</item>
+///         <item>Transparent viewport settings for proper mouse event handling.</item>
+///         <item>Automatic layout when becoming visible.</item>
+///         <item>Focus restoration when hidden.</item>
+///         <item>Default <see cref="Command.Quit"/> implementation that hides the popover.</item>
+///     </list>
 ///     <para>
-///         <b>Mouse:</b><br/>
-///         Popovers are transparent to mouse events (see <see cref="ViewportSettingsFlags.TransparentMouse"/>),
-///         meaning mouse events in a popover that are not also within a subview of the popover will not be captured.
-///     </para>
-///     <para>
-///         <b>Custom Popovers:</b><br/>
-///         To create a custom popover, inherit from <see cref="PopoverBaseImpl"/> and add your own content and logic.
+///         <b>Lifecycle:</b><br/>
+///         Use <see cref="ApplicationPopover.Show"/> to display and <see cref="ApplicationPopover.Hide"/> or
+///         set <see cref="View.Visible"/> to <see langword="false"/> to hide.
 ///     </para>
 /// </remarks>
 public abstract class PopoverBaseImpl : View, IPopover
@@ -40,7 +40,15 @@ public abstract class PopoverBaseImpl : View, IPopover
     ///     Initializes a new instance of the <see cref="PopoverBaseImpl"/> class.
     /// </summary>
     /// <remarks>
-    ///     By default, the popover fills the available screen area and is focusable.
+    ///     <para>
+    ///         Sets up default popover behavior:
+    ///     </para>
+    ///     <list type="bullet">
+    ///         <item>Fills the screen (<see cref="View.Width"/> = <see cref="Dim.Fill"/>, <see cref="View.Height"/> = <see cref="Dim.Fill"/>).</item>
+    ///         <item>Sets <see cref="View.CanFocus"/> to <see langword="true"/>.</item>
+    ///         <item>Configures <see cref="View.ViewportSettings"/> with <see cref="ViewportSettingsFlags.Transparent"/> and <see cref="ViewportSettingsFlags.TransparentMouse"/>.</item>
+    ///         <item>Adds <see cref="Command.Quit"/> bound to <see cref="Application.QuitKey"/> which hides the popover when invoked.</item>
+    ///     </list>
     /// </remarks>
     protected PopoverBaseImpl ()
     {
@@ -87,15 +95,19 @@ public abstract class PopoverBaseImpl : View, IPopover
     }
 
     /// <summary>
-    ///     Called when the <see cref="View.Visible"/> property is changing.
+    ///     Called when the <see cref="View.Visible"/> property is changing. Handles layout and focus management.
     /// </summary>
-    /// <remarks>
-    ///     When becoming visible, the popover is laid out to fit the screen.
-    ///     When becoming hidden, focus is restored to the previous view.
-    /// </remarks>
     /// <returns>
     ///     <see langword="true"/> to cancel the visibility change; otherwise, <see langword="false"/>.
     /// </returns>
+    /// <remarks>
+    ///     <para>
+    ///         <b>When becoming visible:</b> Lays out the popover to fit the screen.
+    ///     </para>
+    ///     <para>
+    ///         <b>When becoming hidden:</b> Restores focus to the previously focused view in the view hierarchy.
+    ///     </para>
+    /// </remarks>
     protected override bool OnVisibleChanging ()
     {
         bool ret = base.OnVisibleChanging ();

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

@@ -123,7 +123,6 @@ public record struct Thickness
             driver?.FillRect (rect with { Height = Math.Min (rect.Height, Top) }, topChar);
         }
 
-        // Draw the Left side
         // Draw the Left side
         if (Left > 0)
         {

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

@@ -96,7 +96,7 @@ public class Margin : Adornment
                 margin.ClearCachedClip ();
             }
 
-            foreach (View subview in view.SubViews)
+            foreach (View subview in view.SubViews.OrderBy (v => v.HasFocus && v.ShadowStyle != ShadowStyle.None).Reverse ())
             {
                 stack.Push (subview);
             }

+ 160 - 41
Terminal.Gui/Views/Menu/PopoverMenu.cs

@@ -1,16 +1,29 @@
-
-
 namespace Terminal.Gui.Views;
 
 /// <summary>
-///     Provides a cascading menu that pops over all other content. Can be used as a context menu or a drop-down
-///     all other content. Can be used as a context menu or a drop-down
-///     menu as part of <see cref="MenuBar"/> as part of <see cref="MenuBar"/>.
+///     A <see cref="PopoverBaseImpl"/>-derived view that provides a cascading menu.
+///     Can be used as a context menu or a drop-down menu as part of <see cref="MenuBar"/>.
 /// </summary>
 /// <remarks>
 ///     <para>
-///         To use as a context menu, register the popover menu with <see cref="IApplication.Popover"/> and call
-///         <see cref="MakeVisible"/>.
+///         <b>IMPORTANT:</b> Must be registered with <see cref="Application.Popover"/> via
+///         <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/> or
+///         <see cref="ApplicationPopover.Show"/>.
+///     </para>
+///     <para>
+///         <b>Usage Example:</b>
+///     </para>
+///     <code>
+///         var menu = new PopoverMenu ([
+///             new MenuItem ("Cut", Command.Cut),
+///             new MenuItem ("Copy", Command.Copy),
+///             new MenuItem ("Paste", Command.Paste)
+///         ]);
+///         Application.Popover?.Register (menu);
+///         menu.MakeVisible (); // or Application.Popover?.Show (menu);
+///     </code>
+///     <para>
+///         See <see cref="PopoverBaseImpl"/> and <see cref="IPopover"/> for lifecycle, focus, and keyboard handling details.
 ///     </para>
 /// </remarks>
 public class PopoverMenu : PopoverBaseImpl, IDesignable
@@ -22,9 +35,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="PopoverMenu"/> class. If any of the elements of
-    ///     <paramref name="menuItems"/> is <see langword="null"/>,
-    ///     a see <see cref="Line"/> will be created instead.
+    ///     <paramref name="menuItems"/> is <see langword="null"/>, a <see cref="Line"/> will be created instead.
     /// </summary>
+    /// <param name="menuItems">The views to use as menu items. Null elements become separator lines.</param>
+    /// <remarks>
+    ///     Remember to call <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/>.
+    /// </remarks>
     public PopoverMenu (IEnumerable<View>? menuItems) : this (
                                                               new Menu (menuItems?.Select (item => item ?? new Line ()))
                                                               {
@@ -32,17 +48,27 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
                                                               })
     { }
 
-    /// <inheritdoc/>
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="PopoverMenu"/> class with the specified menu items.
+    /// </summary>
+    /// <param name="menuItems">The menu items to display in the popover.</param>
+    /// <remarks>
+    ///     Remember to call <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/>.
+    /// </remarks>
     public PopoverMenu (IEnumerable<MenuItem>? menuItems) : this (
-                                                                    new Menu (menuItems)
-                                                                    {
-                                                                        Title = "Popover Root"
-                                                                    })
+                                                                  new Menu (menuItems)
+                                                                  {
+                                                                      Title = "Popover Root"
+                                                                  })
     { }
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="PopoverMenu"/> class with the specified root <see cref="Menu"/>.
     /// </summary>
+    /// <param name="root">The root menu that contains the top-level menu items.</param>
+    /// <remarks>
+    ///     Remember to call <see cref="ApplicationPopover.Register"/> before calling <see cref="MakeVisible"/>.
+    /// </remarks>
     public PopoverMenu (Menu? root)
     {
         // Do this to support debugging traces where Title gets set
@@ -132,7 +158,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
 
     private Key _key = DefaultKey;
 
-    /// <summary>Specifies the key that will activate the context menu.</summary>
+    /// <summary>
+    ///     Gets or sets the key that will activate the popover menu when it is registered but not visible.
+    /// </summary>
+    /// <remarks>
+    ///     This key binding works as a global hotkey when the popover is registered with
+    ///     <see cref="Application.Popover"/>. The default value is <see cref="DefaultKey"/> (<see cref="Key.F10"/> with
+    ///     Shift).
+    /// </remarks>
     public Key Key
     {
         get => _key;
@@ -144,10 +177,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
         }
     }
 
-    /// <summary>Raised when <see cref="Key"/> is changed.</summary>
+    /// <summary>
+    ///     Raised when the <see cref="Key"/> property is changed.
+    /// </summary>
     public event EventHandler<KeyChangedEventArgs>? KeyChanged;
 
-    /// <summary>The default key for activating popover menus.</summary>
+    /// <summary>
+    ///     Gets or sets the default key for activating popover menus. The default value is <see cref="Key.F10"/> with Shift.
+    /// </summary>
+    /// <remarks>
+    ///     This is a configuration property that affects all new <see cref="PopoverMenu"/> instances.
+    /// </remarks>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key DefaultKey { get; set; } = Key.F10.WithShift;
 
@@ -159,12 +199,25 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
 
     /// <summary>
     ///     Makes the popover menu visible and locates it at <paramref name="idealScreenPosition"/>. The actual position of the
-    ///     menu
-    ///     will be adjusted to
-    ///     ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the
-    ///     first MenuItem.
+    ///     menu will be adjusted to ensure the menu fully fits on the screen, with the mouse cursor positioned over
+    ///     the first cell of the first <see cref="MenuItem"/>.
     /// </summary>
-    /// <param name="idealScreenPosition">If <see langword="null"/>, the current mouse position will be used.</param>
+    /// <param name="idealScreenPosition">
+    ///     The ideal screen-relative position for the menu. If <see langword="null"/>, the current mouse position will be
+    ///     used.
+    /// </param>
+    /// <remarks>
+    ///     <para>
+    ///         IMPORTANT: The popover must be registered with <see cref="Application.Popover"/> before calling this
+    ///         method.
+    ///         Call <see cref="ApplicationPopover.Register"/> first.
+    ///     </para>
+    ///     <para>
+    ///         This method internally calls <see cref="ApplicationPopover.Show"/>, which will throw
+    ///         <see cref="InvalidOperationException"/> if the popover is not registered.
+    ///     </para>
+    /// </remarks>
+    /// <exception cref="InvalidOperationException">Thrown if the popover has not been registered.</exception>
     public void MakeVisible (Point? idealScreenPosition = null)
     {
         if (Visible)
@@ -180,12 +233,18 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Locates the popover menu at <paramref name="idealScreenPosition"/>. The actual position of the menu will be
-    ///     adjusted to
-    ///     ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the
-    ///     first MenuItem (if possible).
+    ///     Sets the position of the popover menu at <paramref name="idealScreenPosition"/>. The actual position will be
+    ///     adjusted to ensure the menu fully fits on the screen, with the mouse cursor positioned over the first cell of
+    ///     the first <see cref="MenuItem"/> (if possible).
     /// </summary>
-    /// <param name="idealScreenPosition">If <see langword="null"/>, the current mouse position will be used.</param>
+    /// <param name="idealScreenPosition">
+    ///     The ideal screen-relative position for the menu. If <see langword="null"/>, the current mouse position will be
+    ///     used.
+    /// </param>
+    /// <remarks>
+    ///     This method only sets the position; it does not make the popover visible. Use <see cref="MakeVisible"/> to
+    ///     both position and show the popover.
+    /// </remarks>
     public void SetPosition (Point? idealScreenPosition = null)
     {
         idealScreenPosition ??= App?.Mouse.LastMousePosition;
@@ -212,6 +271,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     When becoming visible, the root menu is added and shown. When becoming hidden, the root menu is removed
+    ///     and the popover is hidden via <see cref="ApplicationPopover.Hide"/>.
+    /// </remarks>
     protected override void OnVisibleChanged ()
     {
         // Logging.Debug ($"{Title} - Visible: {Visible}");
@@ -231,8 +294,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     private Menu? _root;
 
     /// <summary>
-    ///     Gets or sets the <see cref="Menu"/> that is the root of the Popover Menu.
+    ///     Gets or sets the <see cref="Menu"/> that is the root of the popover menu hierarchy.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         The root menu contains the top-level menu items. Setting this property updates key bindings and
+    ///         event subscriptions for all menus in the hierarchy.
+    ///     </para>
+    ///     <para>
+    ///         When set, all submenus are configured with appropriate event handlers for selection and acceptance.
+    ///     </para>
+    /// </remarks>
     public Menu? Root
     {
         get => _root;
@@ -306,6 +378,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     This method checks all menu items in the hierarchy for a matching key binding and invokes the
+    ///     appropriate menu item if found.
+    /// </remarks>
     protected override bool OnKeyDownNotHandled (Key key)
     {
         // See if any of our MenuItems have this key as Key
@@ -325,9 +401,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Gets all the submenus in the PopoverMenu.
+    ///     Gets all the submenus in the popover menu hierarchy, including the root menu.
     /// </summary>
-    /// <returns></returns>
+    /// <returns>An enumerable collection of all <see cref="Menu"/> instances in the hierarchy.</returns>
+    /// <remarks>
+    ///     This method performs a depth-first traversal of the menu tree, starting from <see cref="Root"/>.
+    /// </remarks>
     public IEnumerable<Menu> GetAllSubMenus ()
     {
         List<Menu> result = [];
@@ -358,9 +437,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Gets all the MenuItems in the PopoverMenu.
+    ///     Gets all the menu items in the popover menu hierarchy.
     /// </summary>
-    /// <returns></returns>
+    /// <returns>An enumerable collection of all <see cref="MenuItem"/> instances across all menus in the hierarchy.</returns>
+    /// <remarks>
+    ///     This method traverses all menus returned by <see cref="GetAllSubMenus"/> and collects their menu items.
+    /// </remarks>
     internal IEnumerable<MenuItem> GetMenuItemsOfAllSubMenus ()
     {
         List<MenuItem> result = [];
@@ -380,9 +462,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Pops up the submenu of the specified MenuItem, if there is one.
+    ///     Shows the submenu of the specified <see cref="MenuItem"/>, if it has one.
     /// </summary>
-    /// <param name="menuItem"></param>
+    /// <param name="menuItem">The menu item whose submenu should be shown.</param>
+    /// <remarks>
+    ///     <para>
+    ///         If another submenu is currently visible at the same level, it will be hidden before showing the new one.
+    ///     </para>
+    ///     <para>
+    ///         The submenu is positioned to the right of the menu item, adjusted to ensure full visibility on screen.
+    ///     </para>
+    /// </remarks>
     internal void ShowSubMenu (MenuItem? menuItem)
     {
         var menu = menuItem?.SuperView as Menu;
@@ -416,11 +506,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <summary>
-    ///     Gets the most visible screen-relative location for <paramref name="menu"/>.
+    ///     Calculates the most visible screen-relative location for the specified <paramref name="menu"/>.
     /// </summary>
-    /// <param name="menu">The menu to locate.</param>
-    /// <param name="idealLocation">Ideal screen-relative location.</param>
-    /// <returns></returns>
+    /// <param name="menu">The menu to position.</param>
+    /// <param name="idealLocation">The ideal screen-relative location.</param>
+    /// <returns>The adjusted screen-relative position that ensures maximum visibility of the menu.</returns>
+    /// <remarks>
+    ///     This method adjusts the position to keep the menu fully visible on screen, considering screen boundaries.
+    /// </remarks>
     internal Point GetMostVisibleLocationForSubMenu (Menu menu, Point idealLocation)
     {
         var pos = Point.Empty;
@@ -489,6 +582,7 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     private void MenuOnAccepting (object? sender, CommandEventArgs e)
     {
         var senderView = sender as View;
+
         // Logging.Debug ($"{Title} ({e.Context?.Source?.Title}) Command: {e.Context?.Command} - Sender: {senderView?.GetType ().Name}");
 
         if (e.Context?.Command != Command.HotKey)
@@ -524,6 +618,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     <para>
+    ///         When the popover is not visible, only hotkey commands are processed.
+    ///     </para>
+    ///     <para>
+    ///         This method raises <see cref="View.Accepted"/> for commands that originate from menu items in the hierarchy.
+    ///     </para>
+    /// </remarks>
     protected override bool OnAccepting (CommandEventArgs args)
     {
         // Logging.Debug ($"{Title} ({args.Context?.Source?.Title}) Command: {args.Context?.Command}");
@@ -560,8 +662,6 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
         return false;
     }
 
-
-
     private void MenuOnSelectedMenuItemChanged (object? sender, MenuItem? e)
     {
         // Logging.Debug ($"{Title} - e.Title: {e?.Title}");
@@ -569,6 +669,13 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <exception cref="InvalidOperationException">
+    ///     Thrown if attempting to add a <see cref="Menu"/> or <see cref="MenuItem"/> directly to the popover.
+    /// </exception>
+    /// <remarks>
+    ///     Do not add <see cref="MenuItem"/> or <see cref="Menu"/> views directly to the popover.
+    ///     Use the <see cref="Root"/> property instead.
+    /// </remarks>
     protected override void OnSubViewAdded (View view)
     {
         if (Root is null && (view is Menu || view is MenuItem))
@@ -580,6 +687,9 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
     }
 
     /// <inheritdoc/>
+    /// <remarks>
+    ///     This method unsubscribes from all menu events and disposes the root menu.
+    /// </remarks>
     protected override void Dispose (bool disposing)
     {
         if (disposing)
@@ -600,7 +710,16 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
         base.Dispose (disposing);
     }
 
-    /// <inheritdoc/>
+    /// <summary>
+    ///     Enables the popover menu for use in design-time scenarios.
+    /// </summary>
+    /// <typeparam name="TContext">The type of the target view context.</typeparam>
+    /// <param name="targetView">The target view to associate with the menu commands.</param>
+    /// <returns><see langword="true"/> if successfully enabled for design; otherwise, <see langword="false"/>.</returns>
+    /// <remarks>
+    ///     This method creates a default set of menu items (Cut, Copy, Paste, Select All, Quit) for design-time use.
+    ///     It is primarily used for demonstration and testing purposes.
+    /// </remarks>
     public bool EnableForDesign<TContext> (ref TContext targetView) where TContext : notnull
     {
         // Note: This menu is used by unit tests. If you modify it, you'll likely have to update

+ 48 - 53
Terminal.Gui/Views/MessageBox.cs

@@ -11,8 +11,8 @@ namespace Terminal.Gui.Views;
 ///         or <see langword="null"/> if the user pressed <see cref="Application.QuitKey"/> (typically Esc).
 ///     </para>
 ///     <para>
-///         <see cref="Query(IApplication?, string, string, string[])"/> uses the default Dialog color scheme.
-///         <see cref="ErrorQuery(IApplication?, string, string, string[])"/> uses the Error color scheme.
+///         <see cref="Query(IApplication, string, string, string[])"/> uses the default Dialog color scheme.
+///         <see cref="ErrorQuery(IApplication, string, string, string[])"/> uses the Error color scheme.
 ///     </para>
 ///     <para>
 ///         <b>Important:</b> All MessageBox methods require an <see cref="IApplication"/> instance to be passed.
@@ -126,11 +126,11 @@ public static class MessageBox
     /// </returns>
     /// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
     /// <remarks>
-    ///     Consider using <see cref="ErrorQuery(IApplication?, string, string, string[])"/> which automatically sizes the
+    ///     Consider using <see cref="ErrorQuery(IApplication, string, string, string[])"/> which automatically sizes the
     ///     MessageBox.
     /// </remarks>
     public static int? ErrorQuery (
-        IApplication? app,
+        IApplication app,
         int width,
         int height,
         string title,
@@ -165,7 +165,7 @@ public static class MessageBox
     /// <remarks>
     ///     The MessageBox is centered and auto-sized based on title, message, and buttons.
     /// </remarks>
-    public static int? ErrorQuery (IApplication? app, string title, string message, params string [] buttons)
+    public static int? ErrorQuery (IApplication app, string title, string message, params string [] buttons)
     {
         return QueryFull (
                           app,
@@ -195,11 +195,11 @@ public static class MessageBox
     /// </returns>
     /// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
     /// <remarks>
-    ///     Consider using <see cref="ErrorQuery(IApplication?, string, string, int, string[])"/> which automatically sizes the
+    ///     Consider using <see cref="ErrorQuery(IApplication, string, string, int, string[])"/> which automatically sizes the
     ///     MessageBox.
     /// </remarks>
     public static int? ErrorQuery (
-        IApplication? app,
+        IApplication app,
         int width,
         int height,
         string title,
@@ -236,7 +236,7 @@ public static class MessageBox
     /// <remarks>
     ///     The MessageBox is centered and auto-sized based on title, message, and buttons.
     /// </remarks>
-    public static int? ErrorQuery (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons)
+    public static int? ErrorQuery (IApplication app, string title, string message, int defaultButton = 0, params string [] buttons)
     {
         return QueryFull (
                           app,
@@ -270,11 +270,11 @@ public static class MessageBox
     /// </returns>
     /// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
     /// <remarks>
-    ///     Consider using <see cref="ErrorQuery(IApplication?, string, string, int, bool, string[])"/> which automatically
+    ///     Consider using <see cref="ErrorQuery(IApplication, string, string, int, bool, string[])"/> which automatically
     ///     sizes the MessageBox.
     /// </remarks>
     public static int? ErrorQuery (
-        IApplication? app,
+        IApplication app,
         int width,
         int height,
         string title,
@@ -317,7 +317,7 @@ public static class MessageBox
     ///     The MessageBox is centered and auto-sized based on title, message, and buttons.
     /// </remarks>
     public static int? ErrorQuery (
-        IApplication? app,
+        IApplication app,
         string title,
         string message,
         int defaultButton = 0,
@@ -352,10 +352,10 @@ public static class MessageBox
     /// </returns>
     /// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
     /// <remarks>
-    ///     Consider using <see cref="Query(IApplication?, string, string, string[])"/> which automatically sizes the
+    ///     Consider using <see cref="Query(IApplication, string, string, string[])"/> which automatically sizes the
     ///     MessageBox.
     /// </remarks>
-    public static int? Query (IApplication? app, int width, int height, string title, string message, params string [] buttons)
+    public static int? Query (IApplication app, int width, int height, string title, string message, params string [] buttons)
     {
         return QueryFull (
                           app,
@@ -384,7 +384,7 @@ public static class MessageBox
     /// <remarks>
     ///     The MessageBox is centered and auto-sized based on title, message, and buttons.
     /// </remarks>
-    public static int? Query (IApplication? app, string title, string message, params string [] buttons)
+    public static int? Query (IApplication app, string title, string message, params string [] buttons)
     {
         return QueryFull (
                           app,
@@ -414,11 +414,11 @@ public static class MessageBox
     /// </returns>
     /// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
     /// <remarks>
-    ///     Consider using <see cref="Query(IApplication?, string, string, int, string[])"/> which automatically sizes the
+    ///     Consider using <see cref="Query(IApplication, string, string, int, string[])"/> which automatically sizes the
     ///     MessageBox.
     /// </remarks>
     public static int? Query (
-        IApplication? app,
+        IApplication app,
         int width,
         int height,
         string title,
@@ -455,7 +455,7 @@ public static class MessageBox
     /// <remarks>
     ///     The MessageBox is centered and auto-sized based on title, message, and buttons.
     /// </remarks>
-    public static int? Query (IApplication? app, string title, string message, int defaultButton = 0, params string [] buttons)
+    public static int? Query (IApplication app, string title, string message, int defaultButton = 0, params string [] buttons)
     {
         return QueryFull (
                           app,
@@ -489,11 +489,11 @@ public static class MessageBox
     /// </returns>
     /// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is <see langword="null"/>.</exception>
     /// <remarks>
-    ///     Consider using <see cref="Query(IApplication?, string, string, int, bool, string[])"/> which automatically sizes
+    ///     Consider using <see cref="Query(IApplication, string, string, int, bool, string[])"/> which automatically sizes
     ///     the MessageBox.
     /// </remarks>
     public static int? Query (
-        IApplication? app,
+        IApplication app,
         int width,
         int height,
         string title,
@@ -536,7 +536,7 @@ public static class MessageBox
     ///     The MessageBox is centered and auto-sized based on title, message, and buttons.
     /// </remarks>
     public static int? Query (
-        IApplication? app,
+        IApplication app,
         string title,
         string message,
         int defaultButton = 0,
@@ -557,7 +557,7 @@ public static class MessageBox
     }
 
     private static int? QueryFull (
-        IApplication? app,
+        IApplication app,
         bool useErrorColors,
         int width,
         int height,
@@ -568,25 +568,22 @@ public static class MessageBox
         params string [] buttons
     )
     {
-        ArgumentNullException.ThrowIfNull (app);
-
-        // Create button array for Dialog
         var count = 0;
-        List<Button> buttonList = new ();
+        List<Button> buttonList = [];
         Clicked = null;
 
-        if (buttons is { })
+        if (buttons.Length > 0)
         {
             if (defaultButton > buttons.Length - 1)
             {
                 defaultButton = buttons.Length - 1;
             }
 
-            foreach (string s in buttons)
+            foreach (string buttonText in buttons)
             {
                 var b = new Button
                 {
-                    Text = s,
+                    Text = buttonText,
                     Data = count
                 };
 
@@ -596,7 +593,7 @@ public static class MessageBox
 
                     b.Accepting += (s, e) =>
                                    {
-                                       if (e?.Context?.Source is Button button)
+                                       if (e.Context?.Source is Button button)
                                        {
                                            Clicked = (int)button.Data!;
                                        }
@@ -605,10 +602,7 @@ public static class MessageBox
                                            Clicked = defaultButton;
                                        }
 
-                                       if (e is { })
-                                       {
-                                           e.Handled = true;
-                                       }
+                                       e.Handled = true;
 
                                        (s as View)?.App?.RequestStop ();
                                    };
@@ -619,7 +613,7 @@ public static class MessageBox
             }
         }
 
-        var d = new Dialog
+        Dialog dialog = new ()
         {
             Title = title,
             ButtonAlignment = DefaultButtonAlignment,
@@ -628,38 +622,39 @@ public static class MessageBox
             Buttons = buttonList.ToArray ()
         };
 
-        d.Width = Dim.Auto (
-                            DimAutoStyle.Auto,
-                            Dim.Func (_ => (int)((app.Screen.Width - d.GetAdornmentsThickness ().Horizontal) * (DefaultMinimumWidth / 100f))),
-                            Dim.Func (_ => (int)((app.Screen.Width - d.GetAdornmentsThickness ().Horizontal) * 0.9f)));
+        // ReSharper disable AccessToDisposedClosure
+        dialog.Width = Dim.Auto (
+                                 DimAutoStyle.Auto,
+                                 Dim.Func (_ => (int)((app.Screen.Width - dialog.GetAdornmentsThickness ().Horizontal) * (DefaultMinimumWidth / 100f))),
+                                 Dim.Func (_ => (int)((app.Screen.Width - dialog.GetAdornmentsThickness ().Horizontal) * 0.9f)));
 
-        d.Height = Dim.Auto (
+        dialog.Height = Dim.Auto (
                              DimAutoStyle.Auto,
-                             Dim.Func (_ => (int)((app.Screen.Height - d.GetAdornmentsThickness ().Vertical) * (DefaultMinimumHeight / 100f))),
-                             Dim.Func (_ => (int)((app.Screen.Height - d.GetAdornmentsThickness ().Vertical) * 0.9f)));
+                             Dim.Func (_ => (int)((app.Screen.Height - dialog.GetAdornmentsThickness ().Vertical) * (DefaultMinimumHeight / 100f))),
+                             Dim.Func (_ => (int)((app.Screen.Height - dialog.GetAdornmentsThickness ().Vertical) * 0.9f)));
 
         if (width != 0)
         {
-            d.Width = width;
+            dialog.Width = width;
         }
 
         if (height != 0)
         {
-            d.Height = height;
+            dialog.Height = height;
         }
 
-        d.SchemeName = useErrorColors ? SchemeManager.SchemesToSchemeName (Schemes.Error) : SchemeManager.SchemesToSchemeName (Schemes.Dialog);
+        dialog.SchemeName = useErrorColors ? SchemeManager.SchemesToSchemeName (Schemes.Error) : SchemeManager.SchemesToSchemeName (Schemes.Dialog);
 
-        d.HotKeySpecifier = new ('\xFFFF');
-        d.Text = message;
-        d.TextAlignment = Alignment.Center;
-        d.VerticalTextAlignment = Alignment.Start;
-        d.TextFormatter.WordWrap = wrapMessage;
-        d.TextFormatter.MultiLine = !wrapMessage;
+        dialog.HotKeySpecifier = new ('\xFFFF');
+        dialog.Text = message;
+        dialog.TextAlignment = Alignment.Center;
+        dialog.VerticalTextAlignment = Alignment.Start;
+        dialog.TextFormatter.WordWrap = wrapMessage;
+        dialog.TextFormatter.MultiLine = !wrapMessage;
 
-        // Run the modal; do not shut down the mainloop driver when done
-        app.Run (d);
-        d.Dispose ();
+        // Run the modal
+        app.Run (dialog);
+        dialog.Dispose ();
 
         return Clicked;
     }

+ 1 - 1
Tests/UnitTests/View/Adornment/AdornmentTests.cs

@@ -63,7 +63,7 @@ public class AdornmentTests (ITestOutputHelper output)
         Assert.Equal (6, view.Width);
         Assert.Equal (3, view.Height);
 
-        view.SetClipToScreen ();
+        view.App.LayoutAndDraw (true);
         view.Draw ();
 
         DriverAssert.AssertDriverContentsWithFrameAre (

+ 87 - 8
Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs

@@ -15,6 +15,9 @@ public class BorderArrangementTests (ITestOutputHelper output)
         app.Init ("fake");
 
         app.Driver?.SetScreenSize (6, 5);
+
+        // Using a replacement char to make sure wide glyphs are handled correctly
+        // in the shadow area, to not confusing with a space char.
         app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
 
         Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
@@ -101,9 +104,24 @@ public class BorderArrangementTests (ITestOutputHelper output)
         app.Init ("fake");
 
         app.Driver?.SetScreenSize (8, 7);
+
+        // Using a replacement char to make sure wide glyphs are handled correctly
+        // in the shadow area, to not confusing with a space char.
         app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
 
+        // Don't remove this array even if it seems unused, it is used to map the attributes indexes in the DriverAssert
+        // Otherwise the test won't detect issues with attributes not visibly by the naked eye
+        Attribute [] attributes =
+        [
+            new (ColorName16.Blue, ColorName16.BrightBlue, TextStyle.None),
+            new (ColorName16.BrightBlue, ColorName16.Blue, TextStyle.None),
+            new (ColorName16.Green, ColorName16.BrightGreen, TextStyle.None),
+            new (ColorName16.Magenta, ColorName16.BrightMagenta, TextStyle.None),
+            new (ColorName16.BrightMagenta, ColorName16.Magenta, TextStyle.None)
+        ];
+
         Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+        superview.SetScheme (new () { Normal = attributes [0], Focus = attributes [1] });
 
         superview.Text = """
                          🍎🍎🍎🍎
@@ -115,17 +133,22 @@ public class BorderArrangementTests (ITestOutputHelper output)
                          🍎🍎🍎🍎
                          """;
 
-        View view = new ()
+        View view = new () { X = 6, Width = 2, Height = 1, Text = "🦮" };
+        view.SetScheme (new () { Normal = attributes [2] });
+
+        View view2 = new ()
         {
             X = 2, Width = 6, Height = 6, Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, CanFocus = true
         };
-        view.Border!.Thickness = new (1);
-        view.Border.Add (new View { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Hi" });
-        superview.Add (view);
+        view2.Border!.Thickness = new (1);
+        view2.Border.Add (new View { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Hi" });
+        view2.SetScheme (new () { Normal = attributes [3], HotNormal = attributes [4] });
+
+        superview.Add (view, view2);
 
         app.Begin (superview);
 
-        Assert.Equal ("Absolute(2)", view.X.ToString ());
+        Assert.Equal ("Absolute(2)", view2.X.ToString ());
 
         DriverAssert.AssertDriverContentsAre (
                                               """
@@ -140,6 +163,20 @@ public class BorderArrangementTests (ITestOutputHelper output)
                                               output,
                                               app.Driver);
 
+        DriverAssert.AssertDriverAttributesAre (
+                                                """
+                                                11333333
+                                                11333333
+                                                11333333
+                                                11333333
+                                                11333333
+                                                11333333
+                                                11111111
+                                                """,
+                                                output,
+                                                app.Driver,
+                                                attributes);
+
         Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
         app.LayoutAndDraw ();
 
@@ -156,8 +193,22 @@ public class BorderArrangementTests (ITestOutputHelper output)
                                               output,
                                               app.Driver);
 
+        DriverAssert.AssertDriverAttributesAre (
+                                                """
+                                                11433333
+                                                11333333
+                                                11333333
+                                                11333333
+                                                11333333
+                                                11333333
+                                                11111111
+                                                """,
+                                                output,
+                                                app.Driver,
+                                                attributes);
+
         Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
-        Assert.Equal ("Absolute(1)", view.X.ToString ());
+        Assert.Equal ("Absolute(1)", view2.X.ToString ());
         app.LayoutAndDraw ();
 
         DriverAssert.AssertDriverContentsAre (
@@ -173,13 +224,27 @@ public class BorderArrangementTests (ITestOutputHelper output)
                                               output,
                                               app.Driver);
 
+        DriverAssert.AssertDriverAttributesAre (
+                                                """
+                                                14333332
+                                                13333330
+                                                13333330
+                                                13333330
+                                                13333330
+                                                13333330
+                                                11111111
+                                                """,
+                                                output,
+                                                app.Driver,
+                                                attributes);
+
         Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
-        Assert.Equal ("Absolute(0)", view.X.ToString ());
+        Assert.Equal ("Absolute(0)", view2.X.ToString ());
         app.LayoutAndDraw ();
 
         DriverAssert.AssertDriverContentsAre (
                                               """
-                                              ◊i    🍎
+                                              ◊i    🦮
                                                     🍎
                                                     🍎
                                                     🍎
@@ -189,5 +254,19 @@ public class BorderArrangementTests (ITestOutputHelper output)
                                               """,
                                               output,
                                               app.Driver);
+
+        DriverAssert.AssertDriverAttributesAre (
+                                                """
+                                                43333322
+                                                33333311
+                                                33333311
+                                                33333311
+                                                33333311
+                                                33333311
+                                                11111111
+                                                """,
+                                                output,
+                                                app.Driver,
+                                                attributes);
     }
 }

+ 107 - 11
Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs

@@ -80,20 +80,37 @@ public class ShadowTests (ITestOutputHelper output)
     }
 
     [Theory]
+    [InlineData (ShadowStyle.None)]
     [InlineData (ShadowStyle.Opaque)]
     [InlineData (ShadowStyle.Transparent)]
-    public void ShadowWidth_ShadowHeight_Defaults_To_One (ShadowStyle style)
+    public void ShadowWidth_ShadowHeight_Defaults (ShadowStyle style)
     {
         View view = new () { ShadowStyle = style };
 
-        Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
+        if (view.ShadowStyle == ShadowStyle.None)
+        {
+            Assert.Equal (new (0, 0), view.Margin!.ShadowSize);
+        }
+        else
+        {
+            Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
+        }
+    }
+
+    [Fact]
+    public void ShadowStyle_Opaque_Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Different_Of_One ()
+    {
+        View view = new () { ShadowStyle = ShadowStyle.Opaque };
+        view.Margin!.ShadowSize = new (3, 4);
+        Assert.Equal (1, view.Margin.ShadowSize.Width);
+        Assert.Equal (1, view.Margin.ShadowSize.Height);
     }
 
     [Theory]
     [InlineData (ShadowStyle.None, 0)]
     [InlineData (ShadowStyle.Opaque, 1)]
     [InlineData (ShadowStyle.Transparent, 1)]
-    public void Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Less_Than_One (ShadowStyle style, int expectedLength)
+    public void Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Less_Than_Zero (ShadowStyle style, int expectedLength)
     {
         View view = new () { ShadowStyle = style };
         view.Margin!.ShadowSize = new (-1, -1);
@@ -119,6 +136,58 @@ public class ShadowTests (ITestOutputHelper output)
         Assert.Equal (new (0, 0, 1, 1), view.Margin.Thickness);
     }
 
+    [Theory]
+    [InlineData (ShadowStyle.None, 2, 1, 3, 0, 0, 0)]
+    [InlineData (ShadowStyle.Opaque, 1, 1, 1, 1, 1, 1)]
+    [InlineData (ShadowStyle.Transparent, 2, 1, 3, 2, 2, 3)]
+    public void Changing_ShadowWidth_ShadowHeight_Correctly_Set_Thickness (
+        ShadowStyle style,
+        int expectedLength1,
+        int expectedLength2,
+        int expectedLength3,
+        int expectedThickness1,
+        int expectedThickness2,
+        int expectedThickness3
+    )
+    {
+        View view = new () { ShadowStyle = style };
+        view.Margin!.ShadowSize = new (2, 2);
+        Assert.Equal (expectedLength1, view.Margin!.ShadowSize.Width);
+        Assert.Equal (expectedLength1, view.Margin.ShadowSize.Height);
+        Assert.Equal (new (0, 0, expectedThickness1, expectedThickness1), view.Margin.Thickness);
+
+        view.Margin!.ShadowSize = new (1, 1);
+        Assert.Equal (expectedLength2, view.Margin!.ShadowSize.Width);
+        Assert.Equal (expectedLength2, view.Margin.ShadowSize.Height);
+        Assert.Equal (new (0, 0, expectedThickness2, expectedThickness2), view.Margin.Thickness);
+
+        view.Margin!.ShadowSize = new (3, 3);
+        Assert.Equal (expectedLength3, view.Margin!.ShadowSize.Width);
+        Assert.Equal (expectedLength3, view.Margin.ShadowSize.Height);
+        Assert.Equal (new (0, 0, expectedThickness3, expectedThickness3), view.Margin.Thickness);
+
+        view.ShadowStyle = ShadowStyle.None;
+        Assert.Equal (expectedLength3, view.Margin!.ShadowSize.Width);
+        Assert.Equal (expectedLength3, view.Margin.ShadowSize.Height);
+        Assert.Equal (new (0, 0, 0, 0), view.Margin.Thickness);
+    }
+
+    [Theory]
+    [InlineData (ShadowStyle.None, 0, 1)]
+    [InlineData (ShadowStyle.Opaque, 1, 1)]
+    [InlineData (ShadowStyle.Transparent, 1, 1)]
+    public void Changing_Thickness_Correctly_Set_Thickness (ShadowStyle style, int expectedLength, int expectedThickness)
+    {
+        View view = new () { ShadowStyle = style };
+
+        Assert.Equal (new (0, 0, expectedLength, expectedLength), view.Margin!.Thickness);
+
+        view.Margin!.Thickness = new (0, 0, 1, 1);
+        Assert.Equal (expectedLength, view.Margin!.ShadowSize.Width);
+        Assert.Equal (expectedLength, view.Margin.ShadowSize.Height);
+        Assert.Equal (new (0, 0, expectedThickness, expectedThickness), view.Margin.Thickness);
+    }
+
     [Fact]
     public void ShadowStyle_Transparent_Handles_Wide_Glyphs_Correctly ()
     {
@@ -126,6 +195,9 @@ public class ShadowTests (ITestOutputHelper output)
         app.Init ("fake");
 
         app.Driver?.SetScreenSize (6, 5);
+
+        // Using a replacement char to make sure wide glyphs are handled correctly
+        // in the shadow area, to not confusing with a space char.
         app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
 
         Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
@@ -143,6 +215,8 @@ public class ShadowTests (ITestOutputHelper output)
         superview.Add (view);
 
         app.Begin (superview);
+        Assert.Equal (new (2, 1), view.Margin!.ShadowSize);
+        Assert.Equal (new (0, 0, 2, 1), view.Margin!.Thickness);
 
         DriverAssert.AssertDriverContentsAre (
                                               """
@@ -158,6 +232,8 @@ public class ShadowTests (ITestOutputHelper output)
         view.Margin!.ShadowSize = new (1, 2);
 
         app.LayoutAndDraw ();
+        Assert.Equal (new (1, 2), view.Margin!.ShadowSize);
+        Assert.Equal (new (0, 0, 2, 2), view.Margin!.Thickness);
 
         DriverAssert.AssertDriverContentsAre (
                                               """
@@ -169,6 +245,22 @@ public class ShadowTests (ITestOutputHelper output)
                                               """,
                                               output,
                                               app.Driver);
+
+        view.Width = Dim.Fill (1);
+        app.LayoutAndDraw ();
+        Assert.Equal (new (1, 2), view.Margin!.ShadowSize);
+        Assert.Equal (new (0, 0, 2, 2), view.Margin!.Thickness);
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              ┌─┐ 🍎
+                                              │ │ �
+                                              └─┘ �
+                                              � 🍎�
+                                              � 🍎�
+                                              """,
+                                              output,
+                                              app.Driver);
     }
 
     [Fact]
@@ -326,20 +418,20 @@ public class ShadowTests (ITestOutputHelper output)
     }
 
     [Theory]
-    [InlineData (ShadowStyle.None, 3)]
-    [InlineData (ShadowStyle.Opaque, 4)]
-    [InlineData (ShadowStyle.Transparent, 4)]
-    public void Margin_Thickness_Changes_Adjust_Correctly (ShadowStyle style, int expected)
+    [InlineData (ShadowStyle.None, 3, 4, 4)]
+    [InlineData (ShadowStyle.Opaque, 4, 5, 4)]
+    [InlineData (ShadowStyle.Transparent, 4, 5, 4)]
+    public void Margin_Thickness_Changes_Adjust_Correctly (ShadowStyle style, int expectedThickness, int expectedThicknessAdjust, int expectedThicknessNone)
     {
         var view = new View ();
         view.Margin!.Thickness = new (3);
         view.ShadowStyle = style;
-        Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
+        Assert.Equal (new (3, 3, expectedThickness, expectedThickness), view.Margin.Thickness);
 
-        view.Margin.Thickness = new (3, 3, expected + 1, expected + 1);
-        Assert.Equal (new (3, 3, expected + 1, expected + 1), view.Margin.Thickness);
+        view.Margin.Thickness = new (3, 3, expectedThickness + 1, expectedThickness + 1);
+        Assert.Equal (new (3, 3, expectedThicknessAdjust, expectedThicknessAdjust), view.Margin.Thickness);
         view.ShadowStyle = ShadowStyle.None;
-        Assert.Equal (new (3, 3, 4, 4), view.Margin.Thickness);
+        Assert.Equal (new (3, 3, expectedThicknessNone, expectedThicknessNone), view.Margin.Thickness);
         view.Dispose ();
     }
 
@@ -427,12 +519,16 @@ public class ShadowTests (ITestOutputHelper output)
         app.Driver.Refresh ();
 
         // Assert
+        Assert.Equal (new (0, 0, 2, 2), viewWithShadow.Frame);
+        Assert.Equal (new (0, 0, 1, 1), viewWithShadow.Viewport);
+
         _output.WriteLine ("Actual driver contents:");
         _output.WriteLine (app.Driver.ToString ());
         _output.WriteLine ("\nActual driver output:");
         string? output = app.Driver.GetOutput ().GetLastOutput ();
         _output.WriteLine (output);
 
+        // Printed with bright black (dark gray) text on bright black (dark gray) background making it invisible
         DriverAssert.AssertDriverOutputIs ("""
                                            \x1b[30m\x1b[107m*\x1b[90m\x1b[100mB
                                            """, _output, app.Driver);

+ 5 - 1
Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs

@@ -630,7 +630,11 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas
                                               output,
                                               driver);
 
-
+        // After a full redraw, all cells should be clean
+        foreach (Cell cell in driver.Contents!)
+        {
+            Assert.False (cell.IsDirty);
+        }
     }
 
     [Fact]

+ 117 - 0
Tests/UnitTestsParallelizable/ViewBase/Mouse/HighlightStatesTests.cs

@@ -0,0 +1,117 @@
+using UnitTests;
+using Xunit.Abstractions;
+
+namespace ViewBaseTests.Mouse;
+
+public class HighlightStatesTests (ITestOutputHelper output)
+{
+    [Fact]
+    public void HighlightStates_SubView_With_Single_Runnable_WorkAsExpected ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (6, 1);
+
+        Attribute focus = new (ColorName16.White, ColorName16.Black, TextStyle.None);
+        Attribute highlight = new (ColorName16.Blue, ColorName16.Black, TextStyle.Italic);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+        superview.SetScheme (new () { Focus = focus, Highlight = highlight });
+        View view = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "| Hi |", HighlightStates = MouseState.In };
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        for (var i = 0; i < app.Driver?.Cols; i++)
+        {
+            Assert.Equal (focus, app.Driver.Contents? [0, i].Attribute);
+        }
+
+        DriverAssert.AssertDriverContentsAre ("| Hi |", output, app.Driver);
+
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 0), Flags = MouseFlags.ReportMousePosition });
+        app.LayoutAndDraw ();
+
+        for (var i = 0; i < app.Driver?.Cols; i++)
+        {
+            Assert.Equal (highlight, app.Driver.Contents? [0, i].Attribute);
+        }
+
+        DriverAssert.AssertDriverContentsAre ("| Hi |", output, app.Driver);
+
+        app.Dispose ();
+    }
+
+    [Fact]
+    public void HighlightStates_SubView_With_Multiple_Runnable_WorkAsExpected ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (9, 5);
+
+        Attribute focus = new (ColorName16.White, ColorName16.Black, TextStyle.None);
+        Attribute highlight = new (ColorName16.Blue, ColorName16.Black, TextStyle.Italic);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+        superview.SetScheme (new () { Focus = focus, Highlight = highlight });
+        View view = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "| Hi |", HighlightStates = MouseState.In };
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        Attribute normal = new (ColorName16.Green, ColorName16.Magenta, TextStyle.None);
+        Attribute highlight2 = new (ColorName16.Red, ColorName16.Yellow, TextStyle.Italic);
+
+        Runnable modalSuperview = new () { Y = 1, Width = 9, Height = 4, BorderStyle = LineStyle.Single };
+        modalSuperview.SetScheme (new () { Normal = normal, Highlight = highlight2 });
+        View view2 = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "| Hey |", HighlightStates = MouseState.In };
+        modalSuperview.Add (view2);
+
+        app.Begin (modalSuperview);
+
+        for (var i = 0; i < app.Driver?.Cols; i++)
+        {
+            Assert.Equal (focus, app.Driver.Contents? [0, i].Attribute);
+        }
+
+        for (var i = 0; i < app.Driver?.Cols; i++)
+        {
+            Assert.Equal (normal, app.Driver.Contents? [2, i].Attribute);
+        }
+
+        DriverAssert.AssertDriverContentsAre ("""
+                                              | Hi |
+                                              ┌───────┐
+                                              │| Hey |│
+                                              │       │
+                                              └───────┘
+                                              """
+                                              , output, app.Driver);
+
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 2), Flags = MouseFlags.ReportMousePosition });
+        app.LayoutAndDraw ();
+
+        for (var i = 0; i < app.Driver?.Cols; i++)
+        {
+            Assert.Equal (focus, app.Driver.Contents? [0, i].Attribute);
+        }
+
+        for (var i = 1; i < app.Driver?.Cols - 1; i++)
+        {
+            Assert.Equal (highlight2, app.Driver?.Contents? [2, i].Attribute);
+        }
+
+        DriverAssert.AssertDriverContentsAre ("""
+                                              | Hi |
+                                              ┌───────┐
+                                              │| Hey |│
+                                              │       │
+                                              └───────┘
+                                              """,
+                                              output, app.Driver);
+
+        app.Dispose ();
+    }
+}

+ 361 - 11
docfx/docs/Popovers.md

@@ -1,18 +1,368 @@
 # Popovers Deep Dive
 
-Normally Views cannot draw outside of their `Viewport`. Options for influencing content outside of the `Viewport` include:
+Popovers are transient UI elements that appear above other content to display contextual information, such as menus, tooltips, autocomplete suggestions, and dialog boxes. Terminal.Gui's popover system provides a flexible, non-modal way to present temporary UI without blocking the rest of the application.
 
-1) Modifying the `Border` behavior
-2) Modifying the `Margin` behavior
-3) Using @Terminal.Gui.App.Application.Popover
+## Overview
 
-Popovers are useful for scenarios such as menus, autocomplete popups, and drop-down combo boxes.
+Normally, Views cannot draw outside of their `Viewport`. To display content that appears to "pop over" other views, Terminal.Gui provides the popover system via @Terminal.Gui.App.Application.Popover. Popovers differ from alternatives like modifying `Border` or `Margin` behavior because they:
 
-A `Popover` is any View that meets these characteristics:
+- Are managed centrally by the application
+- Support focus and keyboard event routing
+- Automatically hide in response to user actions
+- Can receive global hotkeys even when not visible
 
-- Implements the @Terminal.Gui.App.IPopover interface 
-- Is Focusable (`CetFocus = true`)
-- Is Transparent (`ViewportSettings = ViewportSettings.Transparent | ViewportSettings.TransparentMouse`
-- Sets `Visible = false` when it receives `Application.QuitKey`
+## Creating a Popover
 
[email protected] provides a sophisticated implementation that can be used as a context menu and is the basis for @Terminal.Gui.MenuBar.
+### Using PopoverMenu
+
+The easiest way to create a popover is to use @Terminal.Gui.Views.PopoverMenu, which provides a cascading menu implementation:
+
+```csharp
+// Create a popover menu with menu items
+PopoverMenu contextMenu = new ([
+    new MenuItem ("Cut", Command.Cut),
+    new MenuItem ("Copy", Command.Copy),
+    new MenuItem ("Paste", Command.Paste),
+    new MenuItem ("Select All", Command.SelectAll)
+]);
+
+// IMPORTANT: Register before showing
+Application.Popover?.Register (contextMenu);
+
+// Show at mouse position or specific location
+contextMenu.MakeVisible (); // Uses current mouse position
+// OR
+contextMenu.MakeVisible (new Point (10, 5)); // Specific location
+```
+
+### Creating a Custom Popover
+
+To create a custom popover, inherit from @Terminal.Gui.App.PopoverBaseImpl:
+
+```csharp
+public class MyCustomPopover : PopoverBaseImpl
+{
+    public MyCustomPopover ()
+    {
+        // PopoverBaseImpl already sets up required defaults:
+        // - ViewportSettings with Transparent and TransparentMouse flags
+        // - Command.Quit binding to hide the popover
+        // - Width/Height set to Dim.Fill()
+        
+        // Add your custom content
+        Label label = new () { Text = "Custom Popover Content" };
+        Add (label);
+        
+        // Optionally override size
+        Width = 40;
+        Height = 10;
+    }
+}
+
+// Usage:
+MyCustomPopover myPopover = new ();
+Application.Popover?.Register (myPopover);
+Application.Popover?.Show (myPopover);
+```
+
+## Popover Requirements
+
+A View qualifies as a popover if it:
+
+1. **Implements @Terminal.Gui.App.IPopover** - Provides the `Current` property for runnable association
+2. **Is Focusable** - `CanFocus = true` to receive keyboard input
+3. **Is Transparent** - `ViewportSettings` includes both:
+   - `ViewportSettings.Transparent` - Allows content beneath to show through
+   - `ViewportSettings.TransparentMouse` - Mouse clicks outside subviews pass through
+4. **Handles Quit** - Binds `Application.QuitKey` to `Command.Quit` and sets `Visible = false`
+
[email protected] provides all these requirements by default.
+
+## Registration and Lifecycle
+
+### Registration (REQUIRED)
+
+**All popovers must be registered before they can be shown:**
+
+```csharp
+PopoverMenu popover = new ([...]);
+
+// REQUIRED: Register with the application
+Application.Popover?.Register (popover);
+
+// Now you can show it
+Application.Popover?.Show (popover);
+// OR
+popover.MakeVisible (); // For PopoverMenu
+```
+
+**Why Registration is Required:**
+- Enables keyboard event routing to the popover
+- Allows global hotkeys to work even when popover is hidden
+- Manages popover lifecycle and disposal
+
+### Showing and Hiding
+
+**Show a popover:**
+```csharp
+Application.Popover?.Show (popover);
+```
+
+**Hide a popover:**
+```csharp
+// Method 1: Via ApplicationPopover
+Application.Popover?.Hide (popover);
+
+// Method 2: Set Visible property
+popover.Visible = false;
+
+// Automatic hiding occurs when:
+// - User presses Application.QuitKey (typically Esc)
+// - User clicks outside the popover (not on a subview)
+// - Another popover is shown
+```
+
+### Lifecycle Management
+
+**Registered popovers:**
+- Have their lifetime managed by the application
+- Are automatically disposed when `Application.Shutdown ()` is called
+- Receive keyboard events based on their associated runnable
+
+**To manage lifetime manually:**
+```csharp
+// Deregister to take ownership of disposal
+Application.Popover?.DeRegister (popover);
+
+// Now you're responsible for disposal
+popover.Dispose ();
+```
+
+## Keyboard Event Routing
+
+### Global Hotkeys
+
+Registered popovers receive keyboard events even when not visible, enabling global hotkey support:
+
+```csharp
+PopoverMenu menu = new ([...]);
+menu.Key = Key.F10.WithShift; // Default hotkey
+
+Application.Popover?.Register (menu);
+
+// Now pressing Shift+F10 anywhere in the app will show the menu
+```
+
+### Runnable Association
+
+The @Terminal.Gui.App.IPopover.Current property associates a popover with a specific @Terminal.Gui.IRunnable:
+
+- If `null`: Popover receives all keyboard events from the application
+- If set: Popover only receives events when the associated runnable is active
+- Automatically set to `Application.TopRunnableView` during registration
+
+```csharp
+// Associate with a specific runnable
+myPopover.Current = myWindow; // Only active when myWindow is the top runnable
+```
+
+## Focus and Input
+
+**When visible:**
+- Popovers receive focus automatically
+- All keyboard input goes to the popover until hidden
+- Mouse clicks on subviews are captured
+- Mouse clicks outside subviews pass through (due to `TransparentMouse`)
+
+**When hidden:**
+- Only registered hotkeys are processed
+- Other keyboard input is not captured
+
+## Layout and Positioning
+
+### Default Layout
+
[email protected] sets `Width = Dim.Fill ()` and `Height = Dim.Fill ()`, making the popover fill the screen by default. The transparent viewport settings allow content beneath to remain visible.
+
+### Custom Sizing
+
+Override `Width` and `Height` to customize size:
+
+```csharp
+public class MyPopover : PopoverBaseImpl
+{
+    public MyPopover ()
+    {
+        Width = 40;  // Fixed width
+        Height = Dim.Auto (); // Auto height based on content
+    }
+}
+```
+
+### Positioning with PopoverMenu
+
[email protected] provides positioning helpers:
+
+```csharp
+// Position at specific screen coordinates
+menu.SetPosition (new Point (10, 5));
+
+// Show and position in one call
+menu.MakeVisible (new Point (10, 5));
+
+// Uses mouse position if null
+menu.MakeVisible (); // Uses Application.Mouse.LastMousePosition
+```
+
+The menu automatically adjusts position to ensure it remains fully visible on screen.
+
+## Built-in Popover Types
+
+### PopoverMenu
+
[email protected] is a sophisticated cascading menu implementation used for:
+- Context menus
+- @Terminal.Gui.MenuBar drop-down menus
+- Custom menu scenarios
+
+**Key Features:**
+- Cascading submenus with automatic positioning
+- Keyboard navigation (arrow keys, hotkeys)
+- Automatic key binding from Commands
+- Mouse support
+- Separator lines via `new Line ()`
+
+**Example with submenus:**
+```csharp
+PopoverMenu fileMenu = new ([
+    new MenuItem ("New", Command.New),
+    new MenuItem ("Open", Command.Open),
+    new MenuItem {
+        Title = "Recent",
+        SubMenu = new Menu ([
+            new MenuItem ("File1.txt", Command.Open),
+            new MenuItem ("File2.txt", Command.Open)
+        ])
+    },
+    new Line (),
+    new MenuItem ("Exit", Command.Quit)
+]);
+
+Application.Popover?.Register (fileMenu);
+fileMenu.MakeVisible ();
+```
+
+## Mouse Event Handling
+
+Popovers use `ViewportSettings.TransparentMouse`, which means:
+
+- **Clicks on popover subviews**: Captured and handled normally
+- **Clicks outside subviews**: Pass through to views beneath
+- **Clicks on background**: Automatically hide the popover
+
+This creates the expected behavior where clicking outside a menu or dialog closes it.
+
+## Best Practices
+
+1. **Always Register First**
+   ```csharp
+   // WRONG - Will throw InvalidOperationException
+   PopoverMenu menu = new ([...]);
+   menu.MakeVisible ();
+   
+   // CORRECT
+   PopoverMenu menu = new ([...]);
+   Application.Popover?.Register (menu);
+   menu.MakeVisible ();
+   ```
+
+2. **Use PopoverMenu for Menus**
+   - Don't reinvent the wheel for standard menu scenarios
+   - Leverage built-in keyboard navigation and positioning
+
+3. **Manage Lifecycle Appropriately**
+   - Let the application manage disposal for long-lived popovers
+   - Deregister and manually dispose short-lived or conditional popovers
+
+4. **Test Global Hotkeys**
+   - Ensure hotkeys don't conflict with application-level keys
+   - Consider providing configuration for custom hotkeys
+
+5. **Handle Edge Cases**
+   - Test positioning near screen edges
+   - Verify behavior with multiple runnables
+   - Test with keyboard-only navigation
+
+## Common Scenarios
+
+### Context Menu on Right-Click
+
+```csharp
+PopoverMenu contextMenu = new ([...]);
+contextMenu.MouseFlags = MouseFlags.Button3Clicked; // Right-click
+Application.Popover?.Register (contextMenu);
+
+myView.MouseClick += (s, e) =>
+{
+    if (e.MouseEvent.Flags == MouseFlags.Button3Clicked)
+    {
+        contextMenu.MakeVisible (myView.ScreenToViewport (e.MouseEvent.Position));
+        e.Handled = true;
+    }
+};
+```
+
+### Autocomplete Popup
+
+```csharp
+public class AutocompletePopover : PopoverBaseImpl
+{
+    private ListView _listView;
+    
+    public AutocompletePopover ()
+    {
+        Width = 30;
+        Height = 10;
+        
+        _listView = new ListView
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill ()
+        };
+        Add (_listView);
+    }
+    
+    public void ShowSuggestions (IEnumerable<string> suggestions, Point position)
+    {
+        _listView.SetSource (suggestions.ToList ());
+        // Position below the text entry field
+        X = position.X;
+        Y = position.Y + 1;
+        Visible = true;
+    }
+}
+```
+
+### Global Command Palette
+
+```csharp
+PopoverMenu commandPalette = new (GetAllCommands ());
+commandPalette.Key = Key.P.WithCtrl; // Ctrl+P to show
+
+Application.Popover?.Register (commandPalette);
+
+// Now Ctrl+P anywhere in the app shows the command palette
+```
+
+## API Reference
+
+- @Terminal.Gui.App.IPopover - Interface for popover views
+- @Terminal.Gui.App.PopoverBaseImpl - Abstract base class for custom popovers
+- @Terminal.Gui.Views.PopoverMenu - Cascading menu implementation
+- @Terminal.Gui.App.ApplicationPopover - Popover manager (accessed via `Application.Popover`)
+
+## See Also
+
+- [Keyboard Deep Dive](keyboard.md) - Understanding keyboard event routing
+- [Mouse Deep Dive](mouse.md) - Mouse event handling
+- [MenuBar Overview](menubar.md) - Using PopoverMenu with MenuBar