Skip to content

Duplicate Mouse Click Events on Double-Click #4474

@tig

Description

@tig

When a user double-clicks the mouse, v2_devellop raises duplicate click and double-click events. The drivers send 6 events instead of the expected 3-4:

Current (Incorrect) Behavior:

Button1Pressed
Button1Released
Button1Clicked        ← First duplicate
Button1Pressed
Button1Released
Button1DoubleClicked  ← Second duplicate

Expected Behavior:

Button1Pressed
Button1Released
Button1Released
Button1DoubleClicked

Root Cause

The mouse event processing pipeline has two layers that both attempt to synthesize click events from press/release events:

  1. Platform Drivers (WindowsDriver, CursesDriver, NetDriver) - Some drivers (especially WindowsDriver) synthesize their own Clicked and DoubleClicked events
  2. MouseInterpreter - Always synthesizes Clicked and DoubleClicked events from Pressed/Released pairs

This causes duplication because both layers are doing the same work.

Architecture

Current Flow

┌──────────────────┐
│  Platform Driver │
│  (Windows/Unix)  │
└────────┬─────────┘
         │ Sends: Pressed, Released, [Clicked], [DoubleClicked]
         ↓
┌──────────────────┐
│ MouseInterpreter │ ← Process(MouseEventArgs e)
│                  │   - yield return e;  (passes through original)
│                  │   - Tracks button state
│                  │   - Synthesizes Clicked/DoubleClicked from Pressed/Released
└────────┬─────────┘
         │ Result: DUPLICATE events
         ↓
┌──────────────────┐
│   Application    │
│   (receives 6    │
│    events)       │
└──────────────────┘

Code Location

The duplication logic is split between:

1. MouseInterpreter (Terminal.Gui/Drivers/MouseInterpreter.cs)

public IEnumerable<MouseEventArgs> Process (MouseEventArgs e)
{
    yield return e;  // ← Passes through driver events (including any Clicked/DoubleClicked)

    // For each mouse button
    for (var i = 0; i < 4; i++)
    {
        _buttonStates [i].UpdateState (e, out int? numClicks);

        if (numClicks.HasValue)
        {
            yield return RaiseClick (i, numClicks.Value, e);  // ← ALSO synthesizes Clicked/DoubleClicked
        }
    }
}

2. MouseButtonStateEx (Terminal.Gui/Drivers/MouseButtonStateEx.cs)

public void UpdateState (MouseEventArgs e, out int? numClicks)
{
    // ...
    if (Pressed)
    {
        // Click released
        numClicks = ++_consecutiveClicks;  // ← Synthesizes click on release
    }
    // ...
}

3. Platform Drivers

The drivers may also synthesize these events:

  • WindowsDriver: Receives DOUBLE_CLICK flag from Windows API and translates to Button1DoubleClicked
  • CursesDriver: Only sends Pressed/Released (no native double-click detection)
  • NetDriver: Only sends Pressed/Released via ANSI sequences (no native double-click detection)

Platform Behavior

Windows Console API

Native events from OS:

  1. MOUSE_BUTTON_PRESSEDButton1Pressed
  2. MOUSE_BUTTON_RELEASEDButton1Released
  3. DOUBLE_CLICK flag → Button1DoubleClicked
  4. MOUSE_BUTTON_RELEASEDButton1Released

Unix/Linux (ncurses)

Native events from ncurses:

  1. BUTTON1_PRESSEDButton1Pressed
  2. BUTTON1_RELEASEDButton1Released
  3. BUTTON1_PRESSEDButton1Pressed (second press)
  4. BUTTON1_RELEASEDButton1Released

Note: ncurses does NOT detect double-clicks natively.

.NET Console (ANSI/VT)

Native events from ANSI escape sequences:

  1. CSI < 0;x;y MButton1Pressed
  2. CSI < 0;x;y mButton1Released
  3. CSI < 0;x;y MButton1Pressed (second press)
  4. CSI < 0;x;y mButton1Released

Note: ANSI sequences do NOT include click/double-click information.

Impact

This affects any code that handles mouse clicks:

view.MouseEvent += (s, e) =>
{
    if (e.Flags.HasFlag(MouseFlags.Button1Clicked))
    {
        // This fires TWICE on a double-click!
        DoSomething();
    }
    
    if (e.Flags.HasFlag(MouseFlags.Button1DoubleClicked))
    {
        // This fires TWICE on a double-click!
        DoSomethingElse();
    }
};

Or when using mouse bindings:

MouseBindings.Add(MouseFlags.Button1Clicked, Command.Activate);
// Command.Activate gets invoked twice on double-click

Proposed Solution

Option 1: Drivers Send Only Pressed/Released (Recommended)

Have all drivers send ONLY Pressed and Released events, and let MouseInterpreter handle click synthesis uniformly:

  1. Strip out any Clicked/DoubleClicked/TripleClicked event generation from all drivers
  2. Drivers only send: Pressed, Released, Moved, Wheel* events
  3. MouseInterpreter synthesizes all Clicked/DoubleClicked/TripleClicked events

Pros:

  • Consistent behavior across all platforms
  • Single source of truth for click detection
  • Easier to maintain and test

Cons:

  • Loses native platform double-click detection on Windows (but we can use platform timing)

Option 2: Skip Processing of Pre-Synthesized Events

Modify MouseInterpreter.Process() to skip processing if the driver already sent click events:

public IEnumerable<MouseEventArgs> Process (MouseEventArgs e)
{
    yield return e;

    // Don't re-process if driver already sent a click/double-click event
    if (e.IsSingleDoubleOrTripleClicked)
    {
        yield break;
    }

    // For each mouse button
    for (var i = 0; i < 4; i++)
    {
        _buttonStates [i].UpdateState (e, out int? numClicks);
        // ...
    }
}

Pros:

  • Preserves native platform double-click detection on Windows
  • Less invasive change

Cons:

  • Inconsistent behavior across platforms
  • More complex logic
  • Drivers and MouseInterpreter stay coupled

Recommendation

Option 1 is recommended because:

  1. It provides consistent behavior across all platforms
  2. It simplifies the codebase (single source of truth)
  3. It's easier to test and maintain
  4. The MouseInterpreter already has the infrastructure for accurate click detection (timing, position tracking)

Future Consideration: Windows Driver ANSI Mouse Support

The Windows driver currently uses native Win32 API (ReadConsoleInput()) for mouse input regardless of the IsLegacyConsole setting, while Unix/Linux drivers use ANSI escape sequences. This architectural difference complicates cross-platform testing and maintenance.

Potential Benefits of ANSI Mouse Input on Windows:

  • Unified codebase: Same mouse handling logic across all platforms
  • Simplified testing: Mock ANSI sequences instead of Win32 INPUT_RECORD structs
  • Consistency: Already using ANSI for keyboard input when IsLegacyConsole = false
  • Standards-based: VT100/xterm mouse protocol is well-documented

Trade-offs:

  • ANSI parsing overhead (string/regex vs. binary structs) - likely negligible for typical mouse event rates (<100/sec)
  • Loss of native Windows double-click timing - MouseInterpreter already handles this uniformly
  • Requires Windows 10+ with VT support enabled (already a requirement for non-legacy mode)

This change would complement Option 1 by ensuring all drivers provide identical mouse event streams (Pressed/Released only), with MouseInterpreter as the sole authority for click synthesis. Worth investigating after resolving the current duplication issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Status

    No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions