Popovers Deep Dive
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.
Overview
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.Application.Popover. Popovers differ from alternatives like modifying Border or Margin behavior because they:
- 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
Architecture
The popover system follows a layered architecture with well-defined responsibilities at each level:
Application.Popover (static accessor)
└── ApplicationPopover (manager)
└── IPopover (interface contract)
└── PopoverBaseImpl (abstract base)
└── PopoverMenu (concrete implementation)
@Terminal.Gui.IPopover — The Contract
The @Terminal.Gui.IPopover interface defines the minimal contract: a single Current property that associates the popover with a specific @Terminal.Gui.IRunnable. This association controls keyboard event scoping — if Current is null, the popover receives all keyboard events globally; if set, events only flow when the associated runnable is the active TopRunnableView.
@Terminal.Gui.PopoverBaseImpl — The Foundation
@Terminal.Gui.PopoverBaseImpl provides the standard popover behavior that all concrete popovers inherit. It configures:
- Full-screen sizing —
Width = Dim.Fill(),Height = Dim.Fill() - Transparency —
ViewportSettings.Transparent | ViewportSettings.TransparentMouse - Dismiss key — Binds
Application.QuitKeytoCommand.Quit - Focus management — Restores focus to the previously-focused view when hidden
- Layout on show — Lays out the popover to fit the screen when becoming visible
@Terminal.Gui.ApplicationPopover — The Manager
@Terminal.Gui.ApplicationPopover is a singleton held by IApplication.Popover that manages the lifecycle of all popovers. It maintains a list of registered popovers and tracks the single active (visible) popover. Only one popover can be active at a time — showing a new one automatically hides the previous one.
The Transparency Model
Popovers use a "full-screen transparent overlay" technique. Instead of drawing a small popup widget directly, a popover fills the entire screen with Dim.Fill() but sets its viewport to be transparent. This means:
- The popover view itself is invisible — only its SubViews (like a @Terminal.Gui.Menu) are drawn
- Mouse clicks that don't hit a SubView pass through to views beneath (via
TransparentMouse) - This naturally creates the "click outside to dismiss" behavior without complex hit-testing
This approach is elegant because the framework's existing transparency system handles the overlay logic. The popover doesn't need to know what's behind it or where it is positioned relative to other views.
Creating a Popover
Using PopoverMenu
The easiest way to create a popover is to use @Terminal.Gui.PopoverMenu, which provides a cascading menu implementation:
// 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.PopoverBaseImpl:
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:
- **Implements @Terminal.Gui.IPopover** — Provides the
Currentproperty for runnable association - Is Focusable —
CanFocus = trueto receive keyboard input - Is Transparent —
ViewportSettingsincludes both:ViewportSettings.Transparent— Allows content beneath to show throughViewportSettings.TransparentMouse— Mouse clicks outside SubViews pass through
- Handles Quit — Binds
Application.QuitKeytoCommand.Quitand setsVisible = false
@Terminal.Gui.PopoverBaseImpl provides all these requirements by default.
Registration and Lifecycle
The Registration-Before-Show Pattern
All popovers must be registered before they can be shown. Registration and showing are intentionally separate operations:
- Registration (
Register()) is done once and enables the popover to participate in keyboard event routing, even when hidden. It also enrolls the popover for automatic lifecycle management. - Showing (
Show()) can be called many times and makes the popover visible.
Attempting to call Show() on an unregistered popover throws InvalidOperationException.
PopoverMenu popover = new ([...]);
// REQUIRED: Register with the application
Application.Popover?.Register (popover);
// Now you can show it (and hide/show it repeatedly)
Application.Popover?.Show (popover);
// OR
popover.MakeVisible (); // For PopoverMenu
Why Registration is Required:
- Enables keyboard event routing to the popover (global hotkeys work even when hidden)
- Manages popover lifecycle (auto-disposal on
Application.Shutdown) - Sets the
Currentrunnable association automatically toTopRunnableView
Showing and Hiding
Show a popover:
Application.Popover?.Show (popover);
The Show() method validates that the popover:
- Is registered
- Has
TransparentandTransparentMouseviewport flags set - Has a key binding for
Command.Quit
It then initializes the popover if needed, hides any previously active popover, and makes the new one visible.
Hide a popover:
// 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:
// Deregister to take ownership of disposal
Application.Popover?.DeRegister (popover);
// Now you're responsible for disposal
popover.Dispose ();
Visibility-Driven Lifecycle
The popover lifecycle is driven entirely by the Visible property — there is no separate "Open/Close" API. When visibility changes, a cascade of events occurs:
Becoming visible (Visible changes to true):
PopoverBaseImpl.OnVisibleChanging()callsLayout(App.Screen.Size)to size the popover to the screenPopoverMenu.OnVisibleChanged()adds the root @Terminal.Gui.Menu as a SubView- The popover receives focus
Becoming hidden (Visible changes to false):
PopoverBaseImpl.OnVisibleChanging()restores focus to the previously-focused view in theTopRunnableViewPopoverMenu.OnVisibleChanged()removes the root menu SubView and callsApplicationPopover.Hide()ApplicationPopover.Hide()clears the active popover reference and triggers a redraw
This pattern means setting Visible = false is equivalent to calling Hide() — both produce the same result.
Keyboard Event Routing
Dispatch Order
When a key is pressed, @Terminal.Gui.ApplicationPopover.DispatchKeyDown routes it through popovers in a specific order:
- Active (visible) popover receives ALL key events first. If it handles the key, processing stops.
- Inactive (hidden) popovers each receive the key event, filtered by their
Currentrunnable association. Popovers whoseCurrentdoesn't match the activeTopRunnableVieware skipped.
This design ensures:
- A visible popover can intercept any key (e.g., Escape to close, arrow keys to navigate)
- Hidden popovers can still respond to global hotkeys (e.g., Shift+F10 to open a menu)
- Popovers scoped to a specific runnable only activate when that runnable is in focus
Global Hotkeys
Registered popovers receive keyboard events even when not visible, enabling global hotkey support:
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.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.TopRunnableViewduring registration
// 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.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:
public class MyPopover : PopoverBaseImpl
{
public MyPopover ()
{
Width = 40; // Fixed width
Height = Dim.Auto (); // Auto height based on content
}
}
Positioning with PopoverMenu
@Terminal.Gui.PopoverMenu provides positioning helpers:
// 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.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 discovery from Commands — menu items that specify a
Commandautomatically display the correct keyboard shortcut - Mouse support
- Separator lines via
new Line ()
Example with submenus:
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
Always Register First
// WRONG - Will throw InvalidOperationException PopoverMenu menu = new ([...]); menu.MakeVisible (); // CORRECT PopoverMenu menu = new ([...]); Application.Popover?.Register (menu); menu.MakeVisible ();Use PopoverMenu for Menus
- Don't reinvent the wheel for standard menu scenarios
- Leverage built-in keyboard navigation and positioning
Manage Lifecycle Appropriately
- Let the application manage disposal for long-lived popovers
- Deregister and manually dispose short-lived or conditional popovers
Test Global Hotkeys
- Ensure hotkeys don't conflict with application-level keys
- Consider providing configuration for custom hotkeys
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
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
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
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.IPopover - Interface for popover views
- @Terminal.Gui.PopoverBaseImpl - Abstract base class for custom popovers
- @Terminal.Gui.PopoverMenu - Cascading menu implementation
- @Terminal.Gui.ApplicationPopover - Popover manager (accessed via
Application.Popover)
See Also
- Keyboard Deep Dive - Understanding keyboard event routing
- Mouse Deep Dive - Mouse event handling
- View List - Full list of views including MenuBar