Duplicate Mouse Click Events on Double-Click
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:
- Platform Drivers (WindowsDriver, CursesDriver, NetDriver) - Some drivers (especially WindowsDriver) synthesize their own
ClickedandDoubleClickedevents - MouseInterpreter - Always synthesizes
ClickedandDoubleClickedevents fromPressed/Releasedpairs
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_CLICKflag from Windows API and translates toButton1DoubleClicked - 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:
MOUSE_BUTTON_PRESSED→Button1PressedMOUSE_BUTTON_RELEASED→Button1ReleasedDOUBLE_CLICKflag →Button1DoubleClickedMOUSE_BUTTON_RELEASED→Button1Released
Unix/Linux (ncurses)
Native events from ncurses:
BUTTON1_PRESSED→Button1PressedBUTTON1_RELEASED→Button1ReleasedBUTTON1_PRESSED→Button1Pressed(second press)BUTTON1_RELEASED→Button1Released
Note: ncurses does NOT detect double-clicks natively.
.NET Console (ANSI/VT)
Native events from ANSI escape sequences:
CSI < 0;x;y M→Button1PressedCSI < 0;x;y m→Button1ReleasedCSI < 0;x;y M→Button1Pressed(second press)CSI < 0;x;y m→Button1Released
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:
- Strip out any
Clicked/DoubleClicked/TripleClickedevent generation from all drivers - Drivers only send:
Pressed,Released,Moved,Wheel*events MouseInterpretersynthesizes allClicked/DoubleClicked/TripleClickedevents
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:
- It provides consistent behavior across all platforms
- It simplifies the codebase (single source of truth)
- It's easier to test and maintain
- 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_RECORDstructs - 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.
@tznind I'd really appreciate your input on this topic. I uncovered all of this when I was working on
- #4472
which is broken because I failed to re-wire up MouseHeldDown. As I was working on this I noticed confusing behavior so I decided to document and investigate. So here we are....
Button1Pressed Button1Released Button1Released Button1DoubleClicked
Do you mean pressed,released,pressed,released
Then double clicked?
You cannot release twice without pressing twice.
‐-----
Is the 'duplicate' purely 1 click and 1 double?
This was intentional, if you 'hold' the first click while you wait to see if theres another then you run into delay and complicstion
--‐----
Ah i see you get repeat events for windows input. Yes we should go with option 1
Only WindowsDriver send their own events. FYI UnixDriver isn't CursesDriver and doesn't have anything of ncurses library and the mouse event is pure native escape sequences as the DotnetDriver.
Without a doubt, option 1 should be considered, allowing MouseInterpreter to perform the necessary procedure for all drivers.
Only WindowsDriver send their own events. FYI UnixDriver isn't CursesDriver and doesn't have anything of ncurses library and the mouse event is pure native escape sequences as the DotnetDriver.
Yeah. I included the ncurses into just to ensure I documented the native behavior.
Button1Pressed Button1Released Button1Released Button1DoubleClicked
Do you mean pressed,released,pressed,released
Then double clicked?
You cannot release twice without pressing twice.
‐-----
Is the 'duplicate' purely 1 click and 1 double?
This was intentional, if you 'hold' the first click while you wait to see if theres another then you run into delay and complicstion
--‐----
Ah i see you get repeat events for windows input. Yes we should go with option 1
For clarity, at the end of the day, what we want is for View.NewMouseEvent to receive the same flow that the ANSI model has:
User single-clicks:
CSI < 0;x;y M→Button1Pressed(first press)CSI < 0;x;y m→Button1Released(first release)-
-
Button1SingleClicked(synthetic single click)
-
and double-clicks:
CSI < 0;x;y M→Button1Pressed(first press)CSI < 0;x;y m→Button1Released(first release)CSI < 0;x;y M→Button1Pressed(second press)CSI < 0;x;y m→Button1Released(second release)-
-
Button1DoubleClicked(synthetic double click)
-
and triple-clicks:
CSI < 0;x;y M→Button1Pressed(first press)CSI < 0;x;y m→Button1Released(first release)CSI < 0;x;y M→Button1Pressed(second press)CSI < 0;x;y m→Button1Released(second releaseCSI < 0;x;y M→Button1Pressed(third press)CSI < 0;x;y m→Button1Released(third release)-
-
Button1TripleClicked(synthetic triple click)
-
The definition of a "single click", "double click", and "triple click" is the time between "first press" and the last "Button1Release" is less than doubleClickThreshold (misnamed; should be multiClickThreshold).