Skip to content

Conversation

@tig
Copy link
Collaborator

@tig tig commented Dec 9, 2025

This PR goes deep in redesigning and re-implementing the mouse support in Terminal.Gui. It addresses a large number of deep design/legacy flaws. It is a major change that will:

  • Make building View subclasses and apps that do simple mouse things extremely straightforward
  • Make mouse actions such as double-click, drag, highlight, repeat easy to understand and deal with
  • Make terminology consistent and sensible

One of the crux changes here is how we deal with multi-click (including repeat). I studied all of the top UI frameworks and how they deal with multi-click and have borrowed the idea of a click count from AppKit/Cocoa. We will still have the ability to have mouse commands bound to e.g. MouseFlags.LeftButtonDoubleClick but the underlying system uses mouseEvent.ClickCount to track. This means that apps can have their own logic for dealing with multi-click with the low level View.MouseEvent or by looking in the CommandContext.

Fixes

Problems with Old Design

Problem: Weak definition of models for each stage between Drivers, Application, and View

  • Some v1 legacy still exists in how IDriver/IInputProcessor implementations work relative to ANSI (the preferred model that only WindowsDriver breaks).
  • The role of Application (specifically MouseImpl) is unclear
  • Who and what is responsible for what is unclear. E.g. who is reponsible for hold-repeat logic? Who tracks multi-clicks?

Problem: Confusing logic to convert PressedClicked in multiple places

Current Behavior:

  • Driver emits Pressed and Released
  • View converts PressedClicked before binding lookup
  • Multiple conversion points, easy to miss

Problem: Applications manually track double-click timing

Problem: Lexicon & Taxonomy is Inconsistent and Confusing

  • MouseEventArgs is borrowed from System.Console but is different. It should be named something distinctive. I like the simplicity of Key and will rename MouseEventArgs to Mouse.
  • Button3Pressed ect makes devs work too hard to remember which button is which. The names in the MouseFlags enums came from Windows. All modern frameworks use real names like RightButtonPressed. All these will be renamed.
  • Names like HighlightStates, WantContinousButtonPress, and WantMousePositionReports are too long and don't really convey purpose (esp with new design).
  • Existing classes like MouseButtonStateEx are poorly and confusingly named.

Design

This describes the new design.

Mouse Flow Summary

TL;DR - The Pipeline

ANSI Input → AnsiMouseParser → MouseInterpreter → MouseImpl → View → Commands
   (1-based)     (0-based screen)   (click synthesis)   (routing)  (viewport)  (Activate/Accept)

Stage Summary

Stage Input Output Key Transformation State Managed
1. ANSI User action ESC[<0;10;5M Hardware event → ANSI None
2. Parser ANSI string Mouse{Pressed, Screen(9,4)} 1-based → 0-based
Button code → MouseFlags
None
3. Interpreter Press/Release Mouse{Clicked, Screen(9,4)} Press+Release → Clicked
Timing → DoubleClicked
Last click time/pos/button
4. MouseImpl Screen coords Mouse{Clicked, Viewport(2,1)} Screen → Viewport coords
Find target view
Handle grab
MouseGrabView
ViewsUnderMouse
5. View Viewport coords Command invocation Clicked → Command.Activate
Grab/Ungrab
MouseState updates
MouseState
MouseGrabView
6. Commands Command Event Activate → Activating
Accept → Accepting
Command handlers

Key Concepts

Coordinates

Level Origin Example
ANSI 1-based, top-left = (1,1) ESC[<0;10;5M
Screen 0-based, top-left = (0,0) ScreenPosition = (9,4)
Viewport 0-based, relative to View Position = (2,1)

Mouse Flags

Category Flags Purpose
Raw Events LeftButtonPressed, LeftButtonReleased From driver, immediate
Synthetic Events LeftButtonClicked, LeftButtonDoubleClicked From MouseInterpreter
State Motion, Wheel, Modifiers Continuous state

Commands

Command Trigger Example
Activate Press/Click, spacebar Select item, toggle checkbox, set focus
Accept Enter, double-click Execute button, open file, submit dialog

Mouse Grab

When: View has MouseHighlightStates or MouseHoldRepeat

Lifecycle:

  1. Press inside → Auto-grab, set focus, MouseState |= Pressed
  2. Move outsideMouseState |= PressedOutside (unless WantContinuous)
  3. Release inside → Convert to Clicked, ungrab
  4. Clicked → Invoke commands

Grabbed View Receives:

  • ALL mouse events (even if outside viewport)
  • Coordinates converted to viewport-relative
  • mouse.View set to grabbed view

Key Design Principles:

  1. ClickCount is metadata on every mouse event (AppKit model)
  2. Flag type changes based on ClickCount (Clicked → DoubleClicked → TripleClicked)
  3. MouseHoldRepeat is about timer-based repetition, NOT multi-click semantics
  4. MouseState provides visual feedback, independent of command execution
  5. One event per physical action - no duplicate event emission

Complete Behavior Matrix

Normal Button (MouseHoldRepeat = false)

User Action MouseState Accept Count ClickCount Values Notes
Single click (press + immediate release) Press: Pressed
Release: Unpressed
1 Release: 1 Standard click
Press and hold (2+ seconds) Pressed → stays → Unpressed 1 Release: 1 No timer, single Accept on release
Double-click (2 quick clicks) Press→Unpress→Press→Unpress 2 Release(1): 1
Release(2): 2
Two separate Accept invocations
Triple-click (3 quick clicks) 3 press/release cycles 3 Release(1): 1
Release(2): 2
Release(3): 3
Three Accept invocations

Key Point: Each release fires Accept. ClickCount tracks which click in the sequence.

Repeat Button (MouseHoldRepeat = true)

User Action MouseState Accept Count ClickCount Values Notes
Single click (press + immediate release) Press: Pressed
Release: Unpressed
1 Release: 1 Too fast for timer to start
Press and hold (2+ seconds) Pressed → stays → Unpressed 10+ All: 1 Timer fires ~500ms initial, then ~50ms intervals (via SmoothAcceleratingTimeout)
Double-click (2 quick clicks) Press→Unpress→Press→Unpress 2 Release(1): 1
Release(2): 2
Two releases = two Accepts (timer doesn't start)
Triple-click 3 press/release cycles 3 Release(1-3): 1,2,3 Three releases = three Accepts
Hold then quick click Hold: many timer fires
Quick click: one release
10+ then +1 Hold: 1 (repeated)
Click: 1 or 2
Mixed repetition + click

Key Point: Timer fires Accept repeatedly with ClickCount=1. Quick releases also fire Accept with appropriate ClickCount.

Mouse Event Flow (Complete Pipeline)

Stage 1: ANSI Input → AnsiMouseParser

ANSI: ESC[<0;10;5M (button=0, x=10, y=5, terminator='M')
      ESC[<0;10;5m (button=0, x=10, y=5, terminator='m')
      
Output: Mouse { Timestamp = 0, Flags=LeftButtonPressed, ScreenPosition=(9,4) }
        Mouse { Timestamp = 42, Flags=LeftButtonReleased, ScreenPosition=(9,4) }

Stage 2: MouseInterpreter (Click Synthesis + ClickCount)

Single Click:

Input:  Pressed(time=0, pos=(10,10))
        Released(time=42, pos=(10,10))

Output: Pressed  + ClickCount=1
        Released + ClickCount=1
        Clicked  + ClickCount=1 (synthesized)

Double Click:

Input:  Pressed(time=0, pos=(10,10))
        Released(time=42, pos=(10,10))
        Pressed(time=200, pos=(10,10))   ← Within 500ms threshold
        Released(time=300, pos=(10,10))

Output: Pressed  + ClickCount=1
        Released + ClickCount=1
        Clicked  + ClickCount=1 (synthesized)
        
        Pressed  + ClickCount=2  ← Count incremented!
        Released + ClickCount=2
        DoubleClicked + ClickCount=2 (synthesized, NOT Clicked!)

Triple Click:

Similar pattern, third release emits:
        Released + ClickCount=3
        TripleClicked + ClickCount=3 (synthesized)

Key Behaviors:

  • ClickCount increments on each press if within threshold + same position
  • Flag type changes: Clicked → DoubleClicked → TripleClicked
  • Both Released AND Clicked/DoubleClicked/TripleClicked are emitted
  • Pressed events always emitted with current ClickCount

Stage 3: MouseImpl (Routing & Grab)

1. Find deepest view under mouse
2. Convert screen → viewport coordinates
3. Handle mouse grab (if MouseHighlightStates or WantContinuous)
4. Send to View.NewMouseEvent()

Stage 4: View.NewMouseEvent (Visual State + Commands)

For Views with MouseHighlightStates:

Pressed  → Grab mouse, MouseState |= Pressed (visual feedback)
Released → MouseState &= ~Pressed, Ungrab
         → Invoke commands bound to Clicked/DoubleClicked/etc.

For Views with MouseHoldRepeat:

Pressed  → Grab mouse, MouseState |= Pressed, Start timer
Timer    → Fire Accept command repeatedly (~50ms intervals using `SmoothAcceleratingTimeout`)
Released → Stop timer, MouseState &= ~Pressed, Ungrab
         → Invoke commands bound to Released

Default MouseBindings

View Base Class (All Views)

private void SetupMouse()
{
    MouseBindings = new();
    
    // Pressed → Activate (for selection/interaction on press)
    MouseBindings.Add (MouseFlags.LeftButtonPressed, Command.Activate);
    MouseBindings.Add (MouseFlags.MiddleButtonPressed, Command.Activate);
    MouseBindings.Add (MouseFlags.Button4Pressed, Command.Activate);
    MouseBindings.Add (MouseFlags.RightButtonPressed, Command.Context);
    MouseBindings.Add (MouseFlags.LeftButtonPressed | MouseFlags.ButtonCtrl, Command.Context);
    
    // Clicked → Accept (single click action)
    MouseBindings.Add(MouseFlags.LeftButtonClicked, Command.Accept);
}

Button Class

// Normal button: inherits defaults, uses Accept on Clicked/DoubleClicked

// Repeat button (MouseHoldRepeat = true):
// Sets HightlightStates = MouseState.In | MouseState.Pressed | MouseState.PressedOutside;
// Timer fires Accept repeatedly via MouseHeldDown
// Bindings stay the same - Accept on Clicked/DoubleClicked for quick clicks

ListView Class (Example of Custom Handling)

// Option 1: Use ClickCount in handler
MouseBindings.Add(MouseFlags.LeftButtonPressed, Command.Activate);
MouseBindings.Add(MouseFlags.LeftButtonDoubleClicked, Command.Accept);

protected override bool OnActivating(CommandEventArgs args)
{
    if (args.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouse })
    {
        // ClickCount available for custom logic
        SelectItem(mouse.Position);  // Always select on click
        return true;
    }
    return base.OnActivating(args);
}

protected override bool OnAccepting(CommandEventArgs args)
{
    if (args.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouse })
    {
        OpenItem(mouse.Position);  // Open on double-click
        return true;
    }
    return base.OnAccepting(args);
}

MouseState vs ClickCount vs Commands

Three Independent Concerns

Concern Purpose Set By Used For
MouseState Visual feedback View.NewMouseEvent Button "pressed" appearance, hover effects
ClickCount Semantic metadata MouseInterpreter Distinguishing single/double/triple click intent
Command Action to execute MouseBindings Activate, Accept, Toggle, etc.

Relationships

MouseState.Pressed  ≠  Command.Activate
  ↑ Visual state        ↑ Action execution
  
ClickCount = 2  →  MouseFlags.DoubleClicked  →  Command.Accept
  ↑ Metadata        ↑ Event type                  ↑ Action

Example: Button with MouseHighlightStates = MouseState.Pressed

Press   → MouseState |= Pressed (button LOOKS pressed)
        → Command.Activate fires (action on press)
        → ClickCount = 1 (metadata)

Release → MouseState &= ~Pressed (button looks normal)
        → Command.Accept fires (action on release)
        → MouseFlags = LeftButtonClicked (event type)

MouseHoldRepeat Deep Dive

Timer Behavior

// When MouseHoldRepeat = true:

PressGrab → Start Timer (500ms initial delay)Timer.Tick (after 500ms)Fire Accept
  ↓
Timer.Tick (every ~50ms) → Fire Accept (with 0.5 acceleration)
  ↓
Release → Stop Timer → Ungrab → Fire Accept once more (from release)

ClickCount Interaction

Hold for 2+ seconds:

Press(ClickCount=1) → Timer starts
Timer fires 10+ times → All with ClickCount=1 (same press sequence)
Release(ClickCount=1) → Timer stops, final Accept

Double-click quickly:

Press(ClickCount=1) → Timer starts but...
Release(ClickCount=1) → Timer stops (< 500ms, never fired), Accept
Press(ClickCount=2) → Timer starts but...
Release(ClickCount=2) → Timer stops, Accept
Total: 2 Accepts (one per release, timer never fired)

Key Insight: Timer and multi-click are independent. Timer repeats with ClickCount=1 until release. Quick clicks don't trigger timer but still track ClickCount.

tig added 5 commits December 7, 2025 15:43
Enhanced logging across multiple classes to improve observability:
- Added debug log for `OperationCanceledException` in `MainLoopCoordinator<TInputRecord>`.
- Logged mouse event details in `MouseImpl` for better event tracking.
- Added trace log in `InputImpl<TInputRecord>` to indicate input availability.
- Replaced `LogInformation` with `Logging.Information` in `WindowsInput`.
- Logged detailed error information for console input failures in `WindowsInput`.

These changes enhance maintainability and debugging capabilities.
@codecov
Copy link

codecov bot commented Dec 9, 2025

Codecov Report

❌ Patch coverage is 45.56075% with 699 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.23%. Comparing base (84f9779) to head (6910084).

Files with missing lines Patch % Lines
...Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs 32.93% 163 Missing and 4 partials ⚠️
...minal.Gui/Drivers/AnsiHandling/AnsiMouseEncoder.cs 0.00% 91 Missing ⚠️
...al.Gui/Drivers/AnsiHandling/AnsiKeyboardEncoder.cs 0.00% 72 Missing ⚠️
...rminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs 0.00% 70 Missing ⚠️
Terminal.Gui/ViewBase/View.Mouse.cs 74.62% 22 Missing and 12 partials ⚠️
...minal.Gui/Drivers/UnixDriver/UnixInputProcessor.cs 0.00% 21 Missing ⚠️
Terminal.Gui/Drivers/InputProcessorImpl.cs 66.10% 16 Missing and 4 partials ⚠️
Terminal.Gui/Views/Slider/Slider.cs 0.00% 18 Missing ⚠️
Terminal.Gui/ViewBase/MouseHoldRepeaterImpl.cs 77.33% 8 Missing and 9 partials ⚠️
Terminal.Gui/Views/TextInput/TextView.cs 54.54% 8 Missing and 7 partials ⚠️
... and 37 more

❌ Your patch check has failed because the patch coverage (45.56%) is below the target coverage (70.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@              Coverage Diff               @@
##           v2_develop    #4472      +/-   ##
==============================================
- Coverage       77.44%   68.23%   -9.22%     
==============================================
  Files             386      389       +3     
  Lines           44678    44881     +203     
  Branches         6283     6307      +24     
==============================================
- Hits            34602    30624    -3978     
- Misses           8225    12303    +4078     
- Partials         1851     1954     +103     
Files with missing lines Coverage Δ
Terminal.Gui/App/Application.Mouse.cs 68.42% <100.00%> (ø)
Terminal.Gui/App/ApplicationImpl.Lifecycle.cs 79.39% <100.00%> (-0.13%) ⬇️
Terminal.Gui/App/Timeout/TimedEvents.cs 74.38% <ø> (-8.27%) ⬇️
...nal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs 100.00% <100.00%> (ø)
.../Drivers/AnsiHandling/AnsiKeyboardParserPattern.cs 100.00% <ø> (ø)
...nal.Gui/Drivers/AnsiHandling/AnsiResponseParser.cs 7.40% <ø> (-81.77%) ⬇️
...minal.Gui/Drivers/AnsiHandling/CsiCursorPattern.cs 82.60% <ø> (ø)
Terminal.Gui/Drivers/AnsiHandling/CsiKeyPattern.cs 70.00% <ø> (ø)
...rminal.Gui/Drivers/AnsiHandling/EscAsAltPattern.cs 82.35% <ø> (ø)
...ui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs 42.60% <ø> (-31.31%) ⬇️
... and 65 more

... and 106 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 84f9779...6910084. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

tig added 7 commits December 9, 2025 12:44
continuous actions like auto-scrolling. Enhanced `IMouseHeldDown`
to include richer event data and updated `Start` to accept
`MouseEventArgs`. Removed `MouseGrabHandler` and integrated its
functionality into `MouseHeldDown` and `MouseImpl`.

Streamlined mouse event handling in `View` by introducing lazy
instantiation of `MouseHeldDown` and replacing legacy methods
with `RaiseCommandsBoundToButtonClickedFlags` and
`RaiseCommandsBoundToWheelFlags`. Removed the `MouseWheel` event
and transitioned wheel handling to the command-binding system.

Improved `MouseBindings` to convert "pressed" events to "clicked"
events for better command invocation. Updated `TimedEvents` to
ensure proper handling of scheduled timeouts.

Refactored `MouseTests` to align with the new `MouseHeldDown`
implementation. Removed redundant code in `Button` and performed
general cleanup and documentation updates for better
maintainability.
- Introduced semantic aliases for `MouseFlags` to improve readability.
- Enhanced XML documentation for `MouseFlags` and related enums.
- Refactored `MouseHeldDown` to improve type safety and add detailed logging.
- Updated `View.Mouse.cs` to handle mouse events more consistently.
- Changed default mouse bindings to use semantic aliases (e.g., `LeftButtonClicked`).
- Removed redundant and excessive logging across multiple files.
- Updated `Button` class and test cases to align with new mouse flag aliases.
- Improved handling of continuous button presses and mouse grab logic.
tig added 16 commits December 10, 2025 14:50
Introduces a broad suite of new xUnit tests for Terminal.Gui's driver, input, and output subsystems under the DriverTests namespace. Tests cover keyboard mapping, mouse click detection, input processing (including Unicode/surrogate pairs), and output buffer behavior. Includes edge cases for modifiers, wide/combining characters, and mouse event quirks. Some tests document known limitations or are marked as skipped for future investigation. This significantly increases test coverage and documents both expected and legacy behaviors.
Added Timestamp property to MouseEventArgs. Refactored MouseButtonClickTracker and MouseInterpreter to use event timestamps. Updated all test constructors. MouseButtonClickTrackerTests: 17/17 passing. MouseInterpreterExtendedTests: 9/18 passing (9 need deferred-click assertion updates).
@tig tig changed the title Fixes #4471 - Continuous mouse presses are not working Fixes #4471, #4474, #3714, #2588 - MASSIVE: Refactors Mouse Support Dec 12, 2025
tig added 17 commits December 12, 2025 05:18
Implemented ITestableInput<char> in UnixInput, enabling injection of synthetic keyboard and mouse events for robust unit testing. Overrode EnqueueKeyDownEvent and EnqueueMouseEvent in UnixInputProcessor to inject ANSI sequences for keys and mouse actions. Added comprehensive unit and debug tests for input injection, event sequencing, and ANSI code mapping. Included detailed driver input/output analysis documentation comparing all drivers and their use of native APIs and ANSI infrastructure. These changes greatly improve testability and cross-platform input simulation.
Introduce AnsiKeyboardEncoder and AnsiMouseEncoder utility classes to convert Key and Mouse objects to ANSI escape sequences, enabling round-trip testing and input injection. Refactor UnixInputProcessor to use these encoders, removing legacy conversion methods. Add comprehensive AnsiKeyboardEncoderTests and remove obsolete MouseToAnsiDebugTests. Minor formatting improvements in AnsiKeyboardParser. These changes improve modularity and testability of ANSI input handling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant