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.
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.