docfx/docs/mouse.md
Quick Start: Jump to Quick Reference for a condensed overview of the mouse pipeline and common patterns.
ANSI Input ? AnsiMouseParser ? MouseInterpreter ? ApplicationMouse ? View ? Commands
(1-based) (0-based screen) (click synthesis) (routing) (viewport) (Activate/Accept)
| Stage | Input | Output | Key Transformation |
|---|---|---|---|
| ANSI | User clicks | ESC[<0;10;5M | Hardware ? ANSI escape sequence |
| Parser | ANSI string | Mouse{Pressed, Screen(9,4)} | 1-based ? 0-based, Button code ? MouseFlags |
| Interpreter | Press/Release | Mouse{Clicked, Screen(9,4)} | Press+Release ? Clicked, Timing ? DoubleClicked |
| ApplicationMouse | Screen coords | Mouse{Clicked, Viewport(2,1)} | Screen ? Viewport, Find view, Handle grab |
| View | Viewport coords | Command invocation | Clicked ? Command.Activate, MouseState updates |
| Commands | Command | Event | Activate ? Activating, Accept ? Accepting |
| Level | Origin | Example |
|---|---|---|
| ANSI | 1-based, (1,1) = top-left | ESC[<0;10;5M |
| Screen | 0-based, (0,0) = top-left of terminal | ScreenPosition = (9,4) |
| Viewport | 0-based, relative to view's content area | Position = (2,1) |
Handle mouse clicks:
view.Activating += (s, e) =>
{
if (e.Context?.Binding is MouseBinding { MouseEvent: { } mouse })
{
Point position = mouse.Position; // Viewport-relative
HandleClick(position);
e.Handled = true;
}
};
Enable visual feedback and auto-grab:
view.MouseHighlightStates = MouseState.In | MouseState.Pressed;
Continuous button press (scrollbar arrows, spin buttons):
view.MouseHoldRepeat = MouseFlags.LeftButtonReleased;
view.Activating += (s, e) => { DoRepeatAction(); e.Handled = true; };
Tenets higher in the list have precedence over tenets lower in the list.
Keyboard Required; Mouse Optional - Terminal users expect full functionality without a mouse. We strive to ensure anything that can be done with the keyboard is also possible with the mouse, and avoid mouse-only features.
Be Consistent With the User's Platform - Users choose their platform and Terminal.Gui apps should respond to mouse input consistent with platform conventions. For example, on Windows: right-click shows context menus, double-click activates items, mouse wheel scrolls content.
| Scenario | Visual State | Command.Accept Count | Notes |
|---|---|---|---|
| Single click (press + release inside) | Pressed ? Released | 1 on release | Standard click behavior |
| Hold (MouseHoldRepeat = false) | Pressed ? stays ? Released | 1 on release | Normal push-button |
| Hold (MouseHoldRepeat = true) | Same visual | ~10+ (timer ~500ms initial, ~50ms intervals) + 1 final on release | Scrollbar arrow behavior |
| Drag outside ? release outside | Pressed ? Released | 0 (canceled) | Standard click cancellation |
| Double-click (MouseHoldRepeat = false) | Press?Release?Press?Release | 2 (one per release) | Two separate accepts |
| Double-click (MouseHoldRepeat = true) | Same cycle | 2 (one per release) | Each press/release fires Accept |
Key Point for MouseHoldRepeat: When enabled, the view responds to Press and Release events only. Each press starts the timer (which fires Accept repeatedly), and each release fires one final Accept (if released inside).
| Scenario | Selection State | Command.Activate Count | Command.Accept Count | Notes |
|---|---|---|---|---|
| Single click | Item selected on click | 1 | 0 | Selection happens immediately |
| Double-click | Selected on first click | 1 (first click) | 1 (second click) | Standard file browser behavior |
| Enter key | No change (already selected) | 0 | 1 | Keyboard equivalent of double-click |
Terminal.Gui provides these APIs for handling mouse input:
Mouse Bindings - Declarative approach using xref:Terminal.Gui.Input.MouseBindings to map mouse events to commands. Recommended for most scenarios.
Mouse Events - Direct event handling via MouseEvent for complex scenarios like drag-and-drop.
Mouse State - xref:Terminal.Gui.ViewBase.MouseState property provides current interaction state for visual feedback.
Mouse class - Platform-independent abstraction (xref:Terminal.Gui.Input.Mouse) for mouse events.
Mouse Bindings is the recommended way to handle mouse input. Views call AddCommand to declare command support, then use xref:Terminal.Gui.Input.MouseBindings to map mouse events to commands:
public class MyView : View
{
public MyView()
{
AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
MouseBindings.Add (MouseFlags.WheelUp, Command.ScrollUp);
AddCommand (Command.ScrollDown, () => ScrollVertical (1));
MouseBindings.Add (MouseFlags.WheelDown, Command.ScrollDown);
// Mouse clicks invoke Command.Activate by default
AddCommand (Command.Activate, () => {
SelectItem();
return true;
});
}
}
All views have these default bindings:
MouseBindings.Add (MouseFlags.LeftButtonPressed, Command.Activate);
MouseBindings.Add (MouseFlags.LeftButtonPressed | MouseFlags.Ctrl, Command.Context);
When a mouse event occurs matching a binding, the bound command is invoked, which raises the corresponding event (e.g., xref:Terminal.Gui.Input.Command.Activate ? xref:Terminal.Gui.ViewBase.View.Activating event).
MouseFlags.LeftButtonPressed for selection/interactionMouseFlags.RightButtonPressed or LeftButtonPressed | CtrlMouseFlags.WheelUp / WheelDownMouseFlags.LeftButtonPressed + mouse move trackingMouse events are processed using the Cancellable Work Pattern:
MouseIMouse.RaiseMouseEvent determines target view and routes eventView.NewMouseEvent() processes:
MouseEvent (raises OnMouseEvent() and MouseEvent event)MouseHighlightStates or MouseHoldRepeat set)For scenarios requiring direct event handling (drag-and-drop, custom gestures):
public class CustomView : View
{
public CustomView()
{
MouseEvent += OnMouseEventHandler;
}
private void OnMouseEventHandler(object sender, Mouse e)
{
if (e.Flags.HasFlag(MouseFlags.LeftButtonPressed))
{
// Handle drag start
e.Handled = true;
}
}
// Alternative: Override the virtual method
protected override bool OnMouseEvent(Mouse mouse)
{
if (mouse.Flags.HasFlag(MouseFlags.LeftButtonPressed))
{
return true; // Handled
}
return base.OnMouseEvent(mouse);
}
}
Recommended pattern - Use xref:Terminal.Gui.ViewBase.View.Activating event with command context:
public class ClickableView : View
{
public ClickableView()
{
Activating += OnActivating;
}
private void OnActivating(object sender, CommandEventArgs e)
{
if (e.Context?.Binding is MouseBinding { MouseEvent: { } mouse })
{
Point clickPosition = mouse.Position; // Viewport-relative
if (mouse.Flags.HasFlag(MouseFlags.LeftButtonPressed))
{
HandleLeftClick(clickPosition);
}
else if (mouse.Flags.HasFlag(MouseFlags.RightButtonPressed))
{
ShowContextMenu(clickPosition);
}
e.Handled = true;
}
}
}
For custom button handling:
// Clear defaults and add custom bindings
MouseBindings.Clear();
MouseBindings.Add(MouseFlags.LeftButtonPressed, Command.Activate);
MouseBindings.Add(MouseFlags.RightButtonPressed, Command.Context);
AddCommand(Command.Context, HandleContextMenu);
The xref:Terminal.Gui.ViewBase.MouseState property tracks the current mouse interaction state:
Configure which states trigger highlighting:
view.MouseHighlightStates = MouseState.In | MouseState.Pressed;
view.MouseStateChanged += (sender, e) =>
{
switch (e.Value)
{
case MouseState.In:
// Hover appearance
break;
case MouseState.Pressed:
// Pressed appearance
break;
}
};
Views with MouseHighlightStates or MouseHoldRepeat enabled automatically grab the mouse when a button is pressed. For manual grab control, use the IMouseGrabHandler interface via xref:Terminal.Gui.ViewBase.View.App.Mouse.
Grab Lifecycle:
GrabMouse(view) (auto or manual), fires GrabbingMouse (cancellable) then GrabbedMouseMouseState |= PressedOutside (unless MouseHoldRepeat)UngrabMouse(), fires UnGrabbingMouse (cancellable) then UnGrabbedMouseGrabbed View Receives:
mouse.Position)mouse.View set to grabbed viewAuto-ungrab occurs when:
WeakReference<View> internally)Manual Grab Example (for custom drag operations):
protected override bool OnMouseEvent(Mouse mouse)
{
if (mouse.Flags.HasFlag(MouseFlags.Button1Pressed))
{
App?.Mouse.GrabMouse(this);
_isDragging = true;
return true;
}
if (_isDragging && mouse.Flags.HasFlag(MouseFlags.Button1Released))
{
App?.Mouse.UngrabMouse();
_isDragging = false;
return true;
}
if (_isDragging)
{
// mouse.Position is viewport-relative during grab
UpdateDragPosition(mouse.Position);
return true;
}
return false;
}
Preventing Grab Theft (for complex drag operations):
// Subscribe to prevent other views from stealing the grab during drag
App.Mouse.GrabbingMouse += (sender, e) =>
{
if (_isDragging && !ReferenceEquals(e.View, this))
{
e.Cancel = true; // Prevent other views from grabbing
}
};
When MouseHoldRepeat is set, the xref:Terminal.Gui.ViewBase.View receives repeated events while the button is held:
view.MouseHoldRepeat = MouseFlags.LeftButtonReleased;
view.Activating += (s, e) =>
{
// Called repeatedly while held (~500ms initial, ~50ms intervals)
DoRepeatAction();
e.Handled = true;
};
Mouse coordinates in Terminal.Gui use multiple coordinate systems:
Mouse.ScreenPositionMouse.PositionWhen handling mouse events in views, use Position for viewport-relative coordinates:
view.MouseEvent += (s, e) =>
{
// e.Position is viewport-relative
if (e.Position.X < 10 && e.Position.Y < 5)
{
// Click in top-left corner of viewport
}
};
Views provide methods to convert between coordinate systems:
// Screen ? Viewport
Point viewportPos = view.ScreenToViewport(screenPos);
Point screenPos = view.ViewportToScreen(viewportPos);
// Screen ? Content
Point contentPos = view.ScreenToContent(screenPos);
Point screenPos = view.ContentToScreen(contentPos);
// Screen ? Frame
Point framePos = view.ScreenToFrame(screenPos);
Rectangle screenRect = view.FrameToScreen();
This section documents the complete flow from raw terminal input to View command execution.
Input Format: SGR Extended Mouse Mode (ESC[<button;x;yM/m)
Example - Single click at column 10, row 5:
Press: ESC[<0;10;5M (button=0, x=10, y=5, 'M'=press)
Release: ESC[<0;10;5m (button=0, x=10, y=5, 'm'=release)
Key Points:
M terminator = press, m terminator = releaseLocation: Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs
Responsibilities:
\u001b\[<(\d+);(\d+);(\d+)(M|m)Mouse instanceOutput: Mouse { Timestamp=now, ScreenPosition=(9,4), Flags=LeftButtonPressed }
Location: Terminal.Gui/Drivers/MouseInterpreter.cs
Responsibilities:
LeftButtonClickedLeftButtonDoubleClickedLeftButtonTripleClickedKey Behavior:
Output: Stream of Mouse events including synthesized clicks
Location: Terminal.Gui/App/Mouse/ApplicationMouse.cs
Entry: IMouse.RaiseMouseEvent(Mouse mouse)
Processing:
List<View?> viewsUnderMouse = App.TopRunnableView.GetViewsUnderLocation(
mouse.ScreenPosition,
ViewportSettingsFlags.TransparentMouse
);
View? deepestView = viewsUnderMouse?.LastOrDefault();
if (mouse.IsPressed &&
App.Popover?.GetActivePopover() is {} popover &&
!View.IsInHierarchy(popover, deepestView, true))
{
ApplicationPopover.HideWithQuitCommand(popover);
RaiseMouseEvent(mouse); // Recurse to handle event below popover
}
Re-show Guard: When a popover is dismissed, ApplicationMouse records the dismissed popover in DismissedByMousePress and wraps the recursive RaiseMouseEvent call with an _isDismissRecursing flag. ApplicationPopover.Show checks this guard and silently returns if the caller is trying to re-show the same popover that was just dismissed. This prevents views beneath the popover (e.g., a DropDownList toggle button or a MenuBarItem) from re-opening the popover during the recursed press event or during the subsequent release/click events in the same mouse interaction cycle (press → release → click). The guard is cleared when the next fresh press event arrives or when the click cycle completes.
// If a view has grabbed the mouse, route events exclusively to that view
// HandleMouseGrab converts coordinates to the grabbed view's viewport
// and delivers the event directly, returning true to stop further processing
if (HandleMouseGrab(deepestViewUnderMouse, mouse))
return; // Grabbed view received the event
Point viewportLocation = deepestView.ScreenToViewport(mouse.ScreenPosition);
Mouse viewMouseEvent = new() {
Position = viewportLocation, // Viewport-relative!
Flags = mouse.Flags,
ScreenPosition = mouse.ScreenPosition,
View = deepestView
};
RaiseMouseEnterLeaveEvents(mouse.ScreenPosition, viewsUnderMouse);
deepestView.NewMouseEvent(viewMouseEvent);
// If not handled, propagate to SuperView
Location: Terminal.Gui/ViewBase/View.Mouse.cs
Entry: View.NewMouseEvent(Mouse mouse)
if (!Enabled) return false;
if (!CanBeVisible(this)) return false;
if (!MousePositionTracking && mouse.Flags == MouseFlags.PositionReport)
return false;
if (RaiseMouseEvent(mouse) || mouse.Handled)
return true; // View handled via OnMouseEvent or subscriber
Conditions: MouseHighlightStates != None OR MouseHoldRepeat.HasValue
On Pressed:
if (!App.Mouse.IsGrabbed(this))
{
// GrabbingMouse event fires first (can be cancelled)
// If not cancelled, GrabbedMouse event fires
App.Mouse.GrabMouse(this);
}
if (!HasFocus && CanFocus) SetFocus();
if (mouse.Position in Viewport)
MouseState |= MouseState.Pressed;
else if (!MouseHoldRepeat)
MouseState |= MouseState.PressedOutside;
On Released:
MouseState &= ~MouseState.Pressed;
MouseState &= ~MouseState.PressedOutside;
On Clicked:
if (App.Mouse.IsGrabbed(this))
{
// UnGrabbingMouse event fires first (can be cancelled)
// If not cancelled, UnGrabbedMouse event fires
// MouseEnter/Leave events update for views under current mouse position
App.Mouse.UngrabMouse();
}
if (MouseBindings.TryGet(mouse.Flags, out binding))
{
binding.MouseEventArgs = mouse;
InvokeCommands(binding.Commands, binding);
}
Default Bindings:
LeftButtonPressed ? Command.ActivateLeftButtonPressed | Ctrl ? Command.ContextSee Command Deep Dive for details.
Example - LeftButtonPressed ? Command.Activate:
InvokeCommand(Command.Activate, context):
OnActivating(args) || args.Cancel // Subclass override
Activating?.Invoke(this, args) // Event subscribers
if (!args.Cancel && CanFocus) SetFocus();
Platform-Specific Input:
WindowsInputProcessor - ReadConsoleInput() ? direct Mouse conversionInput Processing:
Platform API ? InputProcessorImpl ? AnsiResponseParser ? MouseInterpreter ? Application
This ensures consistent mouse behavior across platforms while maintaining platform-specific optimizations.
A common need is forwarding mouse-wheel events from a child view to an ancestor (e.g., a gutter subview inside a scrollable editor's Padding that should scroll the editor). Terminal.Gui supports this idiomatically via the CommandNotBound bubbling mechanism.
MouseBinding for the wheel event without calling AddCommand for that command.AddCommand was not called), DefaultCommandNotBoundHandler runs → xref:Terminal.Gui.ViewBase.View.TryBubbleUp* finds the ancestor with the command in CommandsToBubbleUp → invokes it on the ancestor.// Parent (Editor) handles scroll commands and opts into bubbling
public class Editor : View
{
public Editor ()
{
AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
AddCommand (Command.ScrollDown, () => ScrollVertical (1));
// Allow scroll commands from SubViews/adornment SubViews to bubble here
CommandsToBubbleUp = [Command.ScrollUp, Command.ScrollDown];
}
}
// Child (Gutter) binds wheel events but does NOT add command handlers
public class Gutter : View
{
public Gutter ()
{
// Bind mouse wheel to scroll commands — no AddCommand needed.
// The unhandled command will bubble up to the nearest ancestor
// whose CommandsToBubbleUp includes it.
MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
}
}
Mouse wheel on Gutter
→ MouseBindings maps WheeledUp → Command.ScrollUp
→ Gutter.InvokeCommand(ScrollUp)
→ No handler found (AddCommand was never called)
→ DefaultCommandNotBoundHandler runs
→ RaiseCommandNotBound → TryBubbleUp
→ Finds ancestor (Editor) with Command.ScrollUp in CommandsToBubbleUp
→ Editor.InvokeCommand(ScrollUp) → scrolls
For views hosted inside an adornment (Padding, Border, or Margin), the bubble target is the adornment's Parent (the owning View), not the SuperView. This means a view added to editor.Padding will bubble commands directly to editor, skipping the PaddingView intermediary.
// Gutter added to Editor's Padding — bubbles to Editor automatically
editor.Padding.Add (gutter);
This is handled internally by xref:Terminal.Gui.ViewBase.View.TryBubbleUp* and xref:Terminal.Gui.ViewBase.View.CommandWillBubbleToAncestor*.
[!TIP] Adding a
MouseBindingwithout a correspondingAddCommandhandler is intentional and idiomatic. It signals that the command should bubble to an ancestor rather than being handled locally. See xref:Terminal.Gui.ViewBase.View.CommandNotBound.
if (e.Context?.Binding is MouseBinding { MouseEvent: { } mouse })
{
Point pos = mouse.Position; // Viewport-relative
MouseFlags flags = mouse.Flags;
}
MouseHighlightStates for automatic grab and visual feedbackMouseHoldRepeat for repeating actions (scroll buttons, spinners)For comprehensive documentation, see Input Injection.
Terminal.Gui provides sophisticated input injection for testing without hardware:
Recommended approach - Use helper methods for cleaner test code:
using IApplication app = Application.Create();
app.Init(DriverRegistry.Names.ANSI);
// Inject a left click - simple and clear
app.InjectSequence(InputInjectionExtensions.LeftButtonClick(new Point(10, 5)));
// Inject a right click
app.InjectSequence(InputInjectionExtensions.RightButtonClick(new Point(10, 5)));
// Inject a double-click
app.InjectSequence(InputInjectionExtensions.LeftButtonDoubleClick(new Point(10, 5)));
Alternative approach - Manual event creation for advanced scenarios:
VirtualTimeProvider time = new();
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
// Inject click manually
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonPressed
});
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonReleased
});
Using helper method (recommended):
using IApplication app = Application.Create();
app.Init(DriverRegistry.Names.ANSI);
// One line for a complete double-click
app.InjectSequence(InputInjectionExtensions.LeftButtonDoubleClick(new Point(10, 5)));
Manual approach (for custom timing control):
VirtualTimeProvider time = new();
time.SetTime(new DateTime(2025, 1, 1, 12, 0, 0));
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
// First click
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonPressed,
Timestamp = time.Now
});
time.Advance(TimeSpan.FromMilliseconds(50));
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonReleased,
Timestamp = time.Now
});
// Second click within threshold
time.Advance(TimeSpan.FromMilliseconds(250));
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonPressed,
Timestamp = time.Now
});
time.Advance(TimeSpan.FromMilliseconds(50));
app.InjectMouse(new() {
ScreenPosition = new(10, 5),
Flags = MouseFlags.LeftButtonReleased,
Timestamp = time.Now
});
// Double-click detected!
Terminal.Gui provides three helper methods in InputInjectionExtensions to simplify common mouse click patterns:
LeftButtonClick(Point p) - Single left click (Press + Release)RightButtonClick(Point p) - Single right click (Press + Release)LeftButtonDoubleClick(Point p) - Double left click (two complete click sequences)Benefits:
InputInjectionExtensions.LeftButtonClick(), etc. for simplified injectionapp.InjectMouse(mouse) handles everythingLearn More: See Input Injection for complete documentation.
Handle mouse events application-wide before views process them:
App.Mouse.MouseEvent += (sender, e) =>
{
// Application-wide handling
if (e.Flags.HasFlag(MouseFlags.RightButtonClicked))
{
ShowGlobalContextMenu(e.Position);
e.Handled = true;
}
};
Views can respond when the mouse enters or exits:
view.MouseEnter += (sender, e) =>
{
UpdateTooltip("Hovering");
};
view.MouseLeave += (sender, e) =>
{
HideTooltip();
};
These events work with xref:Terminal.Gui.ViewBase.MouseState to enable hover effects and visual feedback.
IMouseGrabHandler - API reference for mouse grab handlingIMouse - API reference for the mouse interfaceFor debugging mouse event flow, use the Trace class from the Terminal.Gui.Tracing namespace:
using Terminal.Gui.Tracing;
Trace.MouseEnabled = true;
When enabled, mouse events are logged via Logging.Trace showing the flow from Driver → Application → View. Enable via:
Trace.MouseEnabled = true;"Trace.MouseEnabled": trueSee Logging - View Event Tracing for more details.