diff --git a/Examples/UICatalog/Scenarios/Menus.cs b/Examples/UICatalog/Scenarios/Menus.cs
index 4796473ac..f044749ba 100644
--- a/Examples/UICatalog/Scenarios/Menus.cs
+++ b/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
diff --git a/Terminal.Gui/App/IPopover.cs b/Terminal.Gui/App/IPopover.cs
index be577b871..00ff8087b 100644
--- a/Terminal.Gui/App/IPopover.cs
+++ b/Terminal.Gui/App/IPopover.cs
@@ -8,54 +8,64 @@ namespace Terminal.Gui.App;
///
/// 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 and are typically shown using
-/// .
///
///
-/// 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.
+/// IMPORTANT: Popovers must be registered with using
+/// before they can be shown with .
///
///
+/// Lifecycle:
+/// When registered, the popover's lifetime is managed by the application. Registered popovers are
+/// automatically disposed when is called. Call
+/// to manage the lifetime directly.
+///
+///
+/// Visibility and Hiding:
/// Popovers are automatically hidden when:
-///
-/// - The user clicks outside the popover (unless occluded by a subview of the popover).
-/// - The user presses
(typically Esc ).
-/// - Another popover is shown.
-///
///
+///
+/// - The user clicks outside the popover (unless clicking on a subview).
+/// - The user presses
(typically Esc ).
+/// - Another popover is shown.
+/// is set to .
+///
///
/// Focus and Input:
-/// When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a
-/// subview),
-/// presses , 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.
///
///
/// Layout:
-/// When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override
-/// this behavior
-/// by setting and in your derived class.
+/// When becoming visible, popovers are automatically laid out to fill the screen by default.
+/// Override and to customize size.
///
///
-/// Mouse:
-/// Popovers are transparent to mouse events (see ),
-/// meaning mouse events in a popover that are not also within a subview of the popover will not be captured.
+/// Mouse Events:
+/// Popovers use , meaning mouse events
+/// outside subviews are not captured.
///
///
-/// Custom Popovers:
-/// To create a custom popover, inherit from and add your own content and logic.
+/// Creating Custom Popovers:
+/// Inherit from and add your own content and logic.
///
///
public interface IPopover
{
///
- /// Gets or sets the that this Popover is associated with. If null, it is not associated with
- /// any Runnable and will receive all keyboard
- /// events from the . If set, it will only receive keyboard events the Runnable would normally
- /// receive.
- /// When is called, the is set to the current
- /// if not already set.
+ /// Gets or sets the that this popover is associated with.
///
+ ///
+ ///
+ /// If , the popover is not associated with any runnable and will receive all keyboard
+ /// events from the application.
+ ///
+ ///
+ /// If set, the popover will only receive keyboard events when the associated runnable is active.
+ ///
+ ///
+ /// When is called, this property is automatically set to
+ /// if not already set.
+ ///
+ ///
IRunnable? Current { get; set; }
}
diff --git a/Terminal.Gui/App/PopoverBaseImpl.cs b/Terminal.Gui/App/PopoverBaseImpl.cs
index 70eeb4568..baf990a87 100644
--- a/Terminal.Gui/App/PopoverBaseImpl.cs
+++ b/Terminal.Gui/App/PopoverBaseImpl.cs
@@ -2,36 +2,36 @@
namespace Terminal.Gui.App;
///
-/// Abstract base class for popover views in Terminal.Gui.
+/// Abstract base class for popover views in Terminal.Gui. Implements .
///
///
///
-/// Popover Lifecycle:
-/// To display a popover, use . To hide a popover, either call
-/// ,
-/// set to , or show another popover.
+/// IMPORTANT: Popovers must be registered with using
+/// before they can be shown.
///
///
-/// Focus and Input:
-/// When visible, a popover receives focus and input events. If the user clicks outside the popover (and not on a
-/// subview),
-/// presses , or another popover is shown, the popover will be hidden
-/// automatically.
+/// Requirements:
+/// Derived classes must:
///
+///
+/// - Set
to include and .
+/// - Add a key binding for
(typically bound to ).
+///
///
-/// Layout:
-/// When the popover becomes visible, it is automatically laid out to fill the screen by default. You can override
-/// this behavior
-/// by setting and in your derived class.
+/// Default Behavior:
+/// This base class provides:
///
+///
+/// - Fills the screen by default (
= , = ).
+/// - Transparent viewport settings for proper mouse event handling.
+/// - Automatic layout when becoming visible.
+/// - Focus restoration when hidden.
+/// - Default
implementation that hides the popover.
+///
///
-/// Mouse:
-/// Popovers are transparent to mouse events (see ),
-/// meaning mouse events in a popover that are not also within a subview of the popover will not be captured.
-///
-///
-/// Custom Popovers:
-/// To create a custom popover, inherit from and add your own content and logic.
+/// Lifecycle:
+/// Use to display and or
+/// set to to hide.
///
///
public abstract class PopoverBaseImpl : View, IPopover
@@ -40,7 +40,15 @@ public abstract class PopoverBaseImpl : View, IPopover
/// Initializes a new instance of the class.
///
///
- /// By default, the popover fills the available screen area and is focusable.
+ ///
+ /// Sets up default popover behavior:
+ ///
+ ///
+ /// - Fills the screen (
= , = ).
+ /// - Sets
to .
+ /// - Configures
with and .
+ /// - Adds
bound to which hides the popover when invoked.
+ ///
///
protected PopoverBaseImpl ()
{
@@ -87,15 +95,19 @@ public abstract class PopoverBaseImpl : View, IPopover
}
///
- /// Called when the property is changing.
+ /// Called when the property is changing. Handles layout and focus management.
///
- ///
- /// When becoming visible, the popover is laid out to fit the screen.
- /// When becoming hidden, focus is restored to the previous view.
- ///
///
/// to cancel the visibility change; otherwise, .
///
+ ///
+ ///
+ /// When becoming visible: Lays out the popover to fit the screen.
+ ///
+ ///
+ /// When becoming hidden: Restores focus to the previously focused view in the view hierarchy.
+ ///
+ ///
protected override bool OnVisibleChanging ()
{
bool ret = base.OnVisibleChanging ();
diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs
index a15b49fd4..b2f4b0313 100644
--- a/Terminal.Gui/Drawing/Thickness.cs
+++ b/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)
{
diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs
index 998478b15..5c73bc6da 100644
--- a/Terminal.Gui/ViewBase/Adornment/Margin.cs
+++ b/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);
}
diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs
index 7f1e45979..acd3c2bd6 100644
--- a/Terminal.Gui/Views/Menu/PopoverMenu.cs
+++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs
@@ -1,16 +1,29 @@
-
-
namespace Terminal.Gui.Views;
///
-/// 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 as part of .
+/// A -derived view that provides a cascading menu.
+/// Can be used as a context menu or a drop-down menu as part of .
///
///
///
-/// To use as a context menu, register the popover menu with and call
-/// .
+/// IMPORTANT: Must be registered with via
+/// before calling or
+/// .
+///
+///
+/// Usage Example:
+///
+///
+/// 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);
+///
+///
+/// See and for lifecycle, focus, and keyboard handling details.
///
///
public class PopoverMenu : PopoverBaseImpl, IDesignable
@@ -22,9 +35,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
///
/// Initializes a new instance of the class. If any of the elements of
- /// is ,
- /// a see will be created instead.
+ /// is , a will be created instead.
///
+ /// The views to use as menu items. Null elements become separator lines.
+ ///
+ /// Remember to call before calling .
+ ///
public PopoverMenu (IEnumerable? menuItems) : this (
new Menu (menuItems?.Select (item => item ?? new Line ()))
{
@@ -32,17 +48,27 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
})
{ }
- ///
+ ///
+ /// Initializes a new instance of the class with the specified menu items.
+ ///
+ /// The menu items to display in the popover.
+ ///
+ /// Remember to call before calling .
+ ///
public PopoverMenu (IEnumerable? menuItems) : this (
- new Menu (menuItems)
- {
- Title = "Popover Root"
- })
+ new Menu (menuItems)
+ {
+ Title = "Popover Root"
+ })
{ }
///
/// Initializes a new instance of the class with the specified root .
///
+ /// The root menu that contains the top-level menu items.
+ ///
+ /// Remember to call before calling .
+ ///
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;
- /// Specifies the key that will activate the context menu.
+ ///
+ /// Gets or sets the key that will activate the popover menu when it is registered but not visible.
+ ///
+ ///
+ /// This key binding works as a global hotkey when the popover is registered with
+ /// . The default value is ( with
+ /// Shift).
+ ///
public Key Key
{
get => _key;
@@ -144,10 +177,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
}
}
- /// Raised when is changed.
+ ///
+ /// Raised when the property is changed.
+ ///
public event EventHandler? KeyChanged;
- /// The default key for activating popover menus.
+ ///
+ /// Gets or sets the default key for activating popover menus. The default value is with Shift.
+ ///
+ ///
+ /// This is a configuration property that affects all new instances.
+ ///
[ConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key DefaultKey { get; set; } = Key.F10.WithShift;
@@ -159,12 +199,25 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
///
/// Makes the popover menu visible and locates it at . 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 .
///
- /// If , the current mouse position will be used.
+ ///
+ /// The ideal screen-relative position for the menu. If , the current mouse position will be
+ /// used.
+ ///
+ ///
+ ///
+ /// IMPORTANT: The popover must be registered with before calling this
+ /// method.
+ /// Call first.
+ ///
+ ///
+ /// This method internally calls , which will throw
+ /// if the popover is not registered.
+ ///
+ ///
+ /// Thrown if the popover has not been registered.
public void MakeVisible (Point? idealScreenPosition = null)
{
if (Visible)
@@ -180,12 +233,18 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
}
///
- /// Locates the popover menu at . 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 . 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 (if possible).
///
- /// If , the current mouse position will be used.
+ ///
+ /// The ideal screen-relative position for the menu. If , the current mouse position will be
+ /// used.
+ ///
+ ///
+ /// This method only sets the position; it does not make the popover visible. Use to
+ /// both position and show the popover.
+ ///
public void SetPosition (Point? idealScreenPosition = null)
{
idealScreenPosition ??= App?.Mouse.LastMousePosition;
@@ -212,6 +271,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
}
///
+ ///
+ /// When becoming visible, the root menu is added and shown. When becoming hidden, the root menu is removed
+ /// and the popover is hidden via .
+ ///
protected override void OnVisibleChanged ()
{
// Logging.Debug ($"{Title} - Visible: {Visible}");
@@ -231,8 +294,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
private Menu? _root;
///
- /// Gets or sets the that is the root of the Popover Menu.
+ /// Gets or sets the that is the root of the popover menu hierarchy.
///
+ ///
+ ///
+ /// The root menu contains the top-level menu items. Setting this property updates key bindings and
+ /// event subscriptions for all menus in the hierarchy.
+ ///
+ ///
+ /// When set, all submenus are configured with appropriate event handlers for selection and acceptance.
+ ///
+ ///
public Menu? Root
{
get => _root;
@@ -306,6 +378,10 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
}
///
+ ///
+ /// This method checks all menu items in the hierarchy for a matching key binding and invokes the
+ /// appropriate menu item if found.
+ ///
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
}
///
- /// Gets all the submenus in the PopoverMenu.
+ /// Gets all the submenus in the popover menu hierarchy, including the root menu.
///
- ///
+ /// An enumerable collection of all instances in the hierarchy.
+ ///
+ /// This method performs a depth-first traversal of the menu tree, starting from .
+ ///
public IEnumerable GetAllSubMenus ()
{
List result = [];
@@ -358,9 +437,12 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
}
///
- /// Gets all the MenuItems in the PopoverMenu.
+ /// Gets all the menu items in the popover menu hierarchy.
///
- ///
+ /// An enumerable collection of all instances across all menus in the hierarchy.
+ ///
+ /// This method traverses all menus returned by and collects their menu items.
+ ///
internal IEnumerable GetMenuItemsOfAllSubMenus ()
{
List result = [];
@@ -380,9 +462,17 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
}
///
- /// Pops up the submenu of the specified MenuItem, if there is one.
+ /// Shows the submenu of the specified , if it has one.
///
- ///
+ /// The menu item whose submenu should be shown.
+ ///
+ ///
+ /// If another submenu is currently visible at the same level, it will be hidden before showing the new one.
+ ///
+ ///
+ /// The submenu is positioned to the right of the menu item, adjusted to ensure full visibility on screen.
+ ///
+ ///
internal void ShowSubMenu (MenuItem? menuItem)
{
var menu = menuItem?.SuperView as Menu;
@@ -416,11 +506,14 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
}
///
- /// Gets the most visible screen-relative location for .
+ /// Calculates the most visible screen-relative location for the specified .
///
- /// The menu to locate.
- /// Ideal screen-relative location.
- ///
+ /// The menu to position.
+ /// The ideal screen-relative location.
+ /// The adjusted screen-relative position that ensures maximum visibility of the menu.
+ ///
+ /// This method adjusts the position to keep the menu fully visible on screen, considering screen boundaries.
+ ///
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
}
///
+ ///
+ ///
+ /// When the popover is not visible, only hotkey commands are processed.
+ ///
+ ///
+ /// This method raises for commands that originate from menu items in the hierarchy.
+ ///
+ ///
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
}
///
+ ///
+ /// Thrown if attempting to add a or directly to the popover.
+ ///
+ ///
+ /// Do not add or views directly to the popover.
+ /// Use the property instead.
+ ///
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
}
///
+ ///
+ /// This method unsubscribes from all menu events and disposes the root menu.
+ ///
protected override void Dispose (bool disposing)
{
if (disposing)
@@ -600,7 +710,16 @@ public class PopoverMenu : PopoverBaseImpl, IDesignable
base.Dispose (disposing);
}
- ///
+ ///
+ /// Enables the popover menu for use in design-time scenarios.
+ ///
+ /// The type of the target view context.
+ /// The target view to associate with the menu commands.
+ /// if successfully enabled for design; otherwise, .
+ ///
+ /// 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.
+ ///
public bool EnableForDesign (ref TContext targetView) where TContext : notnull
{
// Note: This menu is used by unit tests. If you modify it, you'll likely have to update
diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs
index 07fccc069..09fae545a 100644
--- a/Terminal.Gui/Views/MessageBox.cs
+++ b/Terminal.Gui/Views/MessageBox.cs
@@ -11,8 +11,8 @@ namespace Terminal.Gui.Views;
/// or if the user pressed (typically Esc).
///
///
-/// uses the default Dialog color scheme.
-/// uses the Error color scheme.
+/// uses the default Dialog color scheme.
+/// uses the Error color scheme.
///
///
/// Important: All MessageBox methods require an instance to be passed.
@@ -126,11 +126,11 @@ public static class MessageBox
///
/// Thrown if is .
///
- /// Consider using which automatically sizes the
+ /// Consider using which automatically sizes the
/// MessageBox.
///
public static int? ErrorQuery (
- IApplication? app,
+ IApplication app,
int width,
int height,
string title,
@@ -165,7 +165,7 @@ public static class MessageBox
///
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
///
- 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
///
/// Thrown if is .
///
- /// Consider using which automatically sizes the
+ /// Consider using which automatically sizes the
/// MessageBox.
///
public static int? ErrorQuery (
- IApplication? app,
+ IApplication app,
int width,
int height,
string title,
@@ -236,7 +236,7 @@ public static class MessageBox
///
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
///
- 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
///
/// Thrown if is .
///
- /// Consider using which automatically
+ /// Consider using which automatically
/// sizes the MessageBox.
///
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.
///
public static int? ErrorQuery (
- IApplication? app,
+ IApplication app,
string title,
string message,
int defaultButton = 0,
@@ -352,10 +352,10 @@ public static class MessageBox
///
/// Thrown if is .
///
- /// Consider using which automatically sizes the
+ /// Consider using which automatically sizes the
/// MessageBox.
///
- 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
///
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
///
- 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
///
/// Thrown if is .
///
- /// Consider using which automatically sizes the
+ /// Consider using which automatically sizes the
/// MessageBox.
///
public static int? Query (
- IApplication? app,
+ IApplication app,
int width,
int height,
string title,
@@ -455,7 +455,7 @@ public static class MessageBox
///
/// The MessageBox is centered and auto-sized based on title, message, and buttons.
///
- 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
///
/// Thrown if is .
///
- /// Consider using which automatically sizes
+ /// Consider using which automatically sizes
/// the MessageBox.
///
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.
///
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 buttonList = new ();
+ List 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;
}
diff --git a/Tests/UnitTests/View/Adornment/AdornmentTests.cs b/Tests/UnitTests/View/Adornment/AdornmentTests.cs
index 6c19e7c70..123faf94e 100644
--- a/Tests/UnitTests/View/Adornment/AdornmentTests.cs
+++ b/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 (
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs
index 71ad1202c..ed4b34968 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs
+++ b/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);
}
}
\ No newline at end of file
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
index 281fa1b2f..f3691ebb0 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
+++ b/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);
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs
index e24280295..6d681eee4 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs
+++ b/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]
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Mouse/HighlightStatesTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Mouse/HighlightStatesTests.cs
new file mode 100644
index 000000000..6590aa809
--- /dev/null
+++ b/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 ();
+ }
+}
diff --git a/docfx/docs/Popovers.md b/docfx/docs/Popovers.md
index 54cc24d10..c1569ac11 100644
--- a/docfx/docs/Popovers.md
+++ b/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
-@Terminal.Gui.Views.PopoverMenu provides a sophisticated implementation that can be used as a context menu and is the basis for @Terminal.Gui.MenuBar.
\ No newline at end of file
+### 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`
+
+@Terminal.Gui.App.PopoverBaseImpl 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
+
+@Terminal.Gui.App.PopoverBaseImpl 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
+
+@Terminal.Gui.Views.PopoverMenu 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
+
+@Terminal.Gui.Views.PopoverMenu 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 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
\ No newline at end of file