Fixes #4468 - MouseGrab regressions (#4469)

* Fixed mouse grab issue

* Fixed mouse grab regrssions.

* Update Terminal.Gui/ViewBase/View.Mouse.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Mouse.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Mouse.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Mouse.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Mouse.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Mouse.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* code cleanup

* Update Terminal.Gui/ViewBase/View.Mouse.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Addressing pr feedback

* updated mouse.md

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Tig
2025-12-09 08:32:04 -07:00
committed by GitHub
parent aab90e1fbc
commit b2cf674e0b
5 changed files with 330 additions and 153 deletions

View File

@@ -291,13 +291,6 @@ internal class MouseImpl : IMouse, IDisposable
return;
}
#if DEBUG_IDISPOSABLE
if (View.EnableDebugIDisposableAsserts)
{
ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
}
#endif
if (!RaiseUnGrabbingMouseEvent (MouseGrabView))
{
View view = MouseGrabView;

View File

@@ -239,6 +239,11 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
Logging.Warning ($"{view} cannot be Removed. It has not been added to {this}.");
}
if (App?.Mouse.MouseGrabView == view)
{
App.Mouse.UngrabMouse ();
}
Rectangle touched = view.Frame;
bool hadFocus = view.HasFocus;

View File

@@ -1,12 +1,13 @@
using System.ComponentModel;
using System.Diagnostics;
namespace Terminal.Gui.ViewBase;
public partial class View // Mouse APIs
{
/// <summary>
/// Handles <see cref="WantContinuousButtonPressed"/>, we have detected a button
/// down in the view and have grabbed the mouse.
/// Handles <see cref="WantContinuousButtonPressed"/>, we have detected a button
/// down in the view and have grabbed the mouse.
/// </summary>
public IMouseHeldDown? MouseHeldDown { get; set; }
@@ -227,22 +228,76 @@ public partial class View // Mouse APIs
public bool WantMousePositionReports { get; set; }
/// <summary>
/// Processes a new <see cref="MouseEvent"/>. This method is called by <see cref="IMouse.RaiseMouseEvent"/> when a
/// mouse
/// event occurs.
/// Processes a mouse event for this view. This is the main entry point for mouse input handling,
/// called by <see cref="IMouse.RaiseMouseEvent"/> when the mouse interacts with this view.
/// </summary>
/// <remarks>
/// <para>
/// A view must be both enabled and visible to receive mouse events.
/// This method orchestrates the complete mouse event handling pipeline:
/// </para>
/// <list type="number">
/// <item>
/// <description>
/// Validates pre-conditions (view must be enabled and visible)
/// </description>
/// </item>
/// <item>
/// <description>
/// Raises <see cref="MouseEvent"/> for low-level handling via <see cref="OnMouseEvent"/>
/// and event subscribers
/// </description>
/// </item>
/// <item>
/// <description>
/// Handles mouse grab scenarios when <see cref="HighlightStates"/> or
/// <see cref="WantContinuousButtonPressed"/> are set (press/release/click)
/// </description>
/// </item>
/// <item>
/// <description>
/// Invokes commands bound to mouse clicks via <see cref="MouseBindings"/>
/// (default: <see cref="Command.Select"/> → <see cref="Selecting"/> event)
/// </description>
/// </item>
/// <item>
/// <description>
/// Handles mouse wheel events via <see cref="OnMouseWheel"/> and <see cref="MouseWheel"/>
/// </description>
/// </item>
/// </list>
/// <para>
/// <strong>Continuous Button Press:</strong> When <see cref="WantContinuousButtonPressed"/> is
/// <see langword="true"/> and the user holds a mouse button down, this method is repeatedly called
/// with <see cref="MouseFlags.Button1Pressed"/> (or Button2-4) events, enabling repeating button
/// behavior (e.g., scroll buttons).
/// </para>
/// <para>
/// If <see cref="WantContinuousButtonPressed"/> is <see langword="true"/>, and the user presses and holds the
/// mouse button, <see cref="NewMouseEvent"/> will be repeatedly called with the same <see cref="MouseFlags"/> for
/// as long as the mouse button remains pressed.
/// <strong>Mouse Grab:</strong> Views with <see cref="HighlightStates"/> or
/// <see cref="WantContinuousButtonPressed"/> enabled automatically grab the mouse on button press,
/// receiving all subsequent mouse events until the button is released, even if the mouse moves
/// outside the view's <see cref="Viewport"/>.
/// </para>
/// <para>
/// Most views should handle mouse clicks by subscribing to the <see cref="Selecting"/> event or
/// overriding <see cref="OnSelecting"/> rather than overriding this method. Override this method
/// only for custom low-level mouse handling (e.g., drag-and-drop).
/// </para>
/// </remarks>
/// <param name="mouseEvent"></param>
/// <returns><see langword="true"/> if the event was handled, <see langword="false"/> otherwise.</returns>
/// <param name="mouseEvent">
/// The mouse event to process. Coordinates in <see cref="MouseEventArgs.Position"/> are relative
/// to the view's <see cref="Viewport"/>.
/// </param>
/// <returns>
/// <see langword="true"/> if the event was handled and should not be propagated;
/// <see langword="false"/> if the event was not handled and should continue propagating;
/// <see langword="null"/> if the view declined to handle the event (e.g., disabled or not visible).
/// </returns>
/// <seealso cref="MouseEvent"/>
/// <seealso cref="OnMouseEvent"/>
/// <seealso cref="MouseBindings"/>
/// <seealso cref="Selecting"/>
/// <seealso cref="WantContinuousButtonPressed"/>
/// <seealso cref="HighlightStates"/>
public bool? NewMouseEvent (MouseEventArgs mouseEvent)
{
// Pre-conditions
@@ -269,17 +324,17 @@ public partial class View // Mouse APIs
}
// Post-Conditions
if (HighlightStates != MouseState.None || WantContinuousButtonPressed)
{
if (WhenGrabbedHandlePressed (mouseEvent))
{
return mouseEvent.Handled;
// If we raised Clicked/Activated on the grabbed view, we are done
// regardless of whether the event was handled.
return true;
}
if (WhenGrabbedHandleReleased (mouseEvent))
{
return mouseEvent.Handled;
}
WhenGrabbedHandleReleased (mouseEvent);
if (WhenGrabbedHandleClicked (mouseEvent))
{
@@ -287,6 +342,15 @@ public partial class View // Mouse APIs
}
}
// We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent, and
// it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked
if (mouseEvent.IsSingleDoubleOrTripleClicked)
{
// Logging.Debug ($"{mouseEvent.Flags};{mouseEvent.Position}");
return RaiseCommandsBoundToMouse (mouseEvent);
}
if (mouseEvent.IsWheel)
{
return RaiseMouseWheelEvent (mouseEvent);
@@ -322,11 +386,6 @@ public partial class View // Mouse APIs
MouseEvent?.Invoke (this, mouseEvent);
if (!mouseEvent.Handled)
{
mouseEvent.Handled = InvokeCommandsBoundToMouse (mouseEvent) == true;
}
return mouseEvent.Handled;
}
@@ -353,138 +412,166 @@ public partial class View // Mouse APIs
#region WhenGrabbed Handlers
/// <summary>
/// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event
/// (typically
/// when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStates"/> are set).
/// INTERNAL: For cases where the view is grabbed and the mouse is pressed, this method handles the pressed events from
/// the driver.
/// When <see cref="WantContinuousButtonPressed"/> is set, this method will raise the Clicked/Selecting event
/// via <see cref="Command.Select"/> each time it is called (after the first time the mouse is pressed).
/// </summary>
/// <remarks>
/// Marked internal just to support unit tests
/// </remarks>
/// <param name="mouseEvent"></param>
/// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent)
{
mouseEvent.Handled = false;
if (mouseEvent.IsReleased)
{
if (App?.Mouse.MouseGrabView == this)
{
//Logging.Debug ($"{Id} - {MouseState}");
MouseState &= ~MouseState.Pressed;
MouseState &= ~MouseState.PressedOutside;
}
return mouseEvent.Handled = true;
}
return false;
}
/// <summary>
/// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event
/// (typically
/// when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStates"/> are set).
/// </summary>
/// <remarks>
/// <para>
/// Marked internal just to support unit tests
/// </para>
/// </remarks>
/// <param name="mouseEvent"></param>
/// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
/// <returns><see langword="true"/>, if processing should stop, <see langword="false"/> otherwise.</returns>
private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent)
{
mouseEvent.Handled = false;
if (mouseEvent.IsPressed)
if (!mouseEvent.IsPressed)
{
// The first time we get pressed event, grab the mouse and set focus
if (App?.Mouse.MouseGrabView != this)
{
App?.Mouse.GrabMouse (this);
if (!HasFocus && CanFocus)
{
// Set the focus, but don't invoke Accept
SetFocus ();
}
mouseEvent.Handled = true;
}
if (Viewport.Contains (mouseEvent.Position))
{
//Logging.Debug ($"{Id} - Inside Viewport: {MouseState}");
// The mouse is inside.
if (HighlightStates.HasFlag (MouseState.Pressed))
{
MouseState |= MouseState.Pressed;
}
// Always clear PressedOutside when the mouse is pressed inside the Viewport
MouseState &= ~MouseState.PressedOutside;
}
if (!Viewport.Contains (mouseEvent.Position))
{
// Logging.Debug ($"{Id} - Outside Viewport: {MouseState}");
// The mouse is outside.
// When WantContinuousButtonPressed is set we want to keep the mouse state as pressed (e.g. a repeating button).
// This shows the user that the button is doing something, even if the mouse is outside the Viewport.
if (HighlightStates.HasFlag (MouseState.PressedOutside) && !WantContinuousButtonPressed)
{
MouseState |= MouseState.PressedOutside;
}
}
return mouseEvent.Handled = true;
return false;
}
return false;
Debug.Assert (!mouseEvent.Handled);
mouseEvent.Handled = false;
// If the user has just pressed the mouse, grab the mouse and set focus
if (App is null || App.Mouse.MouseGrabView != this)
{
App?.Mouse.GrabMouse (this);
if (!HasFocus && CanFocus)
{
// Set the focus, but don't invoke Accept
SetFocus ();
}
// This prevents raising Clicked/Selecting the first time the mouse is pressed.
mouseEvent.Handled = true;
}
if (Viewport.Contains (mouseEvent.Position))
{
//Logging.Debug ($"{Id} - Inside Viewport: {MouseState}");
// The mouse is inside.
if (HighlightStates.HasFlag (MouseState.Pressed))
{
MouseState |= MouseState.Pressed;
}
// Always clear PressedOutside when the mouse is pressed inside the Viewport
MouseState &= ~MouseState.PressedOutside;
}
else
{
// Logging.Debug ($"{Id} - Outside Viewport: {MouseState}");
// The mouse is outside.
// When WantContinuousButtonPressed is set we want to keep the mouse state as pressed (e.g. a repeating button).
// This shows the user that the button is doing something, even if the mouse is outside the Viewport.
if (HighlightStates.HasFlag (MouseState.PressedOutside) && !WantContinuousButtonPressed)
{
MouseState |= MouseState.PressedOutside;
}
}
if (!mouseEvent.Handled && WantContinuousButtonPressed && App?.Mouse.MouseGrabView == this)
{
// Ignore the return value here, because the semantics of WhenGrabbedHandlePressed is the return
// value indicates whether processing should stop or not.
RaiseCommandsBoundToMouse (mouseEvent);
return true;
}
return mouseEvent.Handled = true;
}
/// <summary>
/// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the click event
/// INTERNAL: For cases where the view is grabbed, this method handles the released events from the driver
/// (typically
/// when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStates"/> are set).
/// </summary>
/// <remarks>
/// Marked internal just to support unit tests
/// </remarks>
/// <param name="mouseEvent"></param>
/// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent)
internal void WhenGrabbedHandleReleased (MouseEventArgs mouseEvent)
{
mouseEvent.Handled = false;
if (App?.Mouse.MouseGrabView == this && mouseEvent.IsSingleClicked)
if (App is { } && App.Mouse.MouseGrabView == this)
{
// We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab
App?.Mouse.UngrabMouse ();
// TODO: Prove we need to unset MouseState.Pressed and MouseState.PressedOutside here
// TODO: There may be perf gains if we don't unset these flags here
//Logging.Debug ($"{Id} - {MouseState}");
MouseState &= ~MouseState.Pressed;
MouseState &= ~MouseState.PressedOutside;
// If mouse is still in bounds, generate a click
if (!WantMousePositionReports && Viewport.Contains (mouseEvent.Position))
{
// By default, this will raise Selecting/OnSelecting - Subclasses can override this via AddCommand (Command.Select ...).
mouseEvent.Handled = InvokeCommandsBoundToMouse (mouseEvent) == true;
}
return mouseEvent.Handled = true;
}
return false;
}
/// <summary>
/// INTERNAL: For cases where the view is grabbed, this method handles the click events from the driver
/// (typically
/// when <see cref="WantContinuousButtonPressed"/> or <see cref="HighlightStates"/> are set).
/// </summary>
/// <param name="mouseEvent"></param>
/// <returns><see langword="true"/>, if processing should stop; <see langword="false"/> otherwise.</returns>
internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent)
{
if (App is null || App.Mouse.MouseGrabView != this || !mouseEvent.IsSingleClicked)
{
return false;
}
// Logging.Debug ($"{mouseEvent.Flags};{mouseEvent.Position}");
// We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab
App?.Mouse.UngrabMouse ();
// TODO: Prove we need to unset MouseState.Pressed and MouseState.PressedOutside here
// TODO: There may be perf gains if we don't unset these flags here
MouseState &= ~MouseState.Pressed;
MouseState &= ~MouseState.PressedOutside;
// If mouse is still in bounds, return false to indicate a click should be raised.
return WantMousePositionReports || !Viewport.Contains (mouseEvent.Position);
}
#endregion WhenGrabbed Handlers
#region Mouse Click Events
/// <summary>
/// INTERNAL API: Converts mouse click events into <see cref="Command"/>s by invoking the commands bound
/// to the mouse button via <see cref="MouseBindings"/>. By default, all mouse clicks are bound to
/// <see cref="Command.Select"/> which raises the <see cref="Selecting"/> event.
/// </summary>
protected bool RaiseCommandsBoundToMouse (MouseEventArgs args)
{
// Pre-conditions
if (!Enabled)
{
// QUESTION: Is this right? Should a disabled view eat mouse clicks?
return args.Handled = false;
}
Debug.Assert (!args.Handled);
// Logging.Debug ($"{args.Flags};{args.Position}");
MouseEventArgs clickedArgs = new ();
clickedArgs.Flags = args.IsPressed
? args.Flags switch
{
MouseFlags.Button1Pressed => MouseFlags.Button1Clicked,
MouseFlags.Button2Pressed => MouseFlags.Button2Clicked,
MouseFlags.Button3Pressed => MouseFlags.Button3Clicked,
MouseFlags.Button4Pressed => MouseFlags.Button4Clicked,
_ => clickedArgs.Flags
}
: args.Flags;
clickedArgs.Position = args.Position;
clickedArgs.ScreenPosition = args.ScreenPosition;
clickedArgs.View = args.View;
// By default, this will raise Activating/OnActivating - Subclasses can override this via
// ReplaceCommand (Command.Activate ...).
args.Handled = InvokeCommandsBoundToMouse (clickedArgs) == true;
return args.Handled;
}
#endregion Mouse Click Events
#region Mouse Wheel Events
/// <summary>Raises the <see cref="OnMouseWheel"/>/<see cref="MouseWheel"/> event.</summary>
@@ -601,18 +688,26 @@ public partial class View // Mouse APIs
}
/// <summary>
/// Called when <see cref="MouseState"/> has changed, indicating the View should be highlighted or not. The <see cref="MouseState"/> passed in the event
/// Called when <see cref="MouseState"/> has changed, indicating the View should be highlighted or not. The
/// <see cref="MouseState"/> passed in the event
/// indicates the highlight style that will be applied.
/// </summary>
protected virtual void OnMouseStateChanged (EventArgs<MouseState> args) { }
/// <summary>
/// RaisedCalled when <see cref="MouseState"/> has changed, indicating the View should be highlighted or not. The <see cref="MouseState"/> passed in the event
/// Raised when <see cref="MouseState"/> has changed, indicating the View should be highlighted or not. The
/// <see cref="MouseState"/> passed in the event
/// indicates the highlight style that will be applied.
/// </summary>
public event EventHandler<EventArgs<MouseState>>? MouseStateChanged;
#endregion MouseState Handling
private void DisposeMouse () { }
private void DisposeMouse ()
{
if (App?.Mouse.MouseGrabView == this)
{
App.Mouse.UngrabMouse ();
}
}
}

View File

@@ -3,11 +3,20 @@ using Xunit.Abstractions;
namespace ViewBaseTests.Mouse;
[Collection ("Global Test Setup")]
[Trait ("Category", "Input")]
public class MouseTests (ITestOutputHelper output) : TestsAllViews
{
[Fact]
public void Default_MouseBindings ()
{
var testView = new View ();
Assert.Contains (MouseFlags.Button1Clicked, testView.MouseBindings.GetAllFromCommands (Command.Select));
// Assert.Contains (MouseFlags.Button1DoubleClicked, testView.MouseBindings.GetAllFromCommands (Command.Accept));
Assert.Equal (5, testView.MouseBindings.GetBindings ().Count ());
}
[Theory]
[InlineData (false, false, false)]
[InlineData (true, false, true)]

View File

@@ -126,12 +126,16 @@ Mouse events are processed through the following workflow using the [Cancellable
1. **Driver Level**: The driver captures platform-specific mouse events and converts them to `MouseEventArgs`
2. **Application Level**: `IApplication.Mouse.RaiseMouseEvent` determines the target view and routes the event
3. **View Level**: The target view processes the event through:
- `OnMouseEvent` (virtual method that can be overridden)
- `MouseEvent` event (for event subscribers)
- Mouse bindings (if the event wasn't handled) which invoke commands
- Command handlers (e.g., `OnSelecting` for `Command.Select`)
- High-level events like `MouseEnter`, `MouseLeave`
3. **View Level**: The target view processes the event through `View.NewMouseEvent()`:
1. **Pre-condition validation** - Checks if view is enabled, visible, and wants the event type
2. **Low-level MouseEvent** - Raises `OnMouseEvent()` and `MouseEvent` event
3. **Mouse grab handling** - If `HighlightStates` or `WantContinuousButtonPressed` are set:
- Automatically grabs mouse on button press
- Handles press/release/click lifecycle
- Sets focus if view is focusable
- Updates `MouseState` (Pressed, PressedOutside)
4. **Command invocation** - For click events, invokes commands via `MouseBindings` (default: `Command.Select` ? `Selecting` event)
5. **Mouse wheel handling** - Raises `OnMouseWheel()` and `MouseWheel` event
### Handling Mouse Events Directly
@@ -228,15 +232,17 @@ public class MultiButtonView : View
}
```
## Mouse State
## Mouse State and Mouse Grab
### Mouse State
The @Terminal.Gui.ViewBase.View.MouseState property provides an abstraction for the current state of the mouse, enabling views to do interesting things like change their appearance based on the mouse state.
Mouse states include:
* **Normal** - Default state when mouse is not interacting with the view
* **None** - No mouse interaction with the view
* **In** - Mouse is positioned over the view (inside the viewport)
* **Pressed** - Mouse button is pressed down while over the view
* **PressedOutside** - Mouse was pressed inside but moved outside the view
* **PressedOutside** - Mouse was pressed inside but moved outside the view (when not using `WantContinuousButtonPressed`)
It works in conjunction with the @Terminal.Gui.ViewBase.View.HighlightStates which is a list of mouse states that will cause a view to become highlighted.
@@ -253,6 +259,9 @@ view.MouseStateChanged += (sender, e) =>
case MouseState.Pressed:
// Change appearance when pressed
break;
case MouseState.PressedOutside:
// Mouse was pressed inside but moved outside
break;
}
};
```
@@ -264,6 +273,59 @@ Configure which states should cause highlighting:
view.HighlightStates = MouseState.In | MouseState.Pressed;
```
### Mouse Grab
Views with `HighlightStates` or `WantContinuousButtonPressed` enabled automatically **grab the mouse** when a button is pressed. This means:
1. **Automatic Grab**: The view receives all mouse events until the button is released, even if the mouse moves outside the view's `Viewport`
2. **Focus Management**: If the view is focusable (`CanFocus = true`), it automatically receives focus on the first button press
3. **State Tracking**: The view's `MouseState` is updated to reflect press/release/outside states
4. **Automatic Ungrab**: The mouse is released when:
- The button is released (via `WhenGrabbedHandleClicked()`)
- The view is removed from its parent hierarchy (via `View.OnRemoved()`)
- The application ends (via `App.End()`)
#### Continuous Button Press
When `WantContinuousButtonPressed` is set to `true`, the view receives repeated click events while the button is held down:
```cs
view.WantContinuousButtonPressed = true;
view.Selecting += (s, e) =>
{
// This will be called repeatedly while the button is held down
// Useful for scroll buttons, increment/decrement buttons, etc.
DoRepeatAction();
e.Handled = true;
};
```
**Note**: With `WantContinuousButtonPressed`, the `MouseState.PressedOutside` flag has no effect - the view continues to receive events and maintains the pressed state even when the mouse moves outside.
#### Mouse Grab Lifecycle
```
Button Press (inside view)
?
Mouse Grabbed Automatically
?? View receives focus (if CanFocus)
?? MouseState |= MouseState.Pressed
?? All mouse events route to this view
Mouse Move (while grabbed)
?? Inside Viewport: MouseState remains Pressed
?? Outside Viewport: MouseState |= MouseState.PressedOutside
(unless WantContinuousButtonPressed is true)
Button Release
?
Mouse Ungrabbed Automatically
?? MouseState &= ~MouseState.Pressed
?? MouseState &= ~MouseState.PressedOutside
?? Click event raised (if still in bounds)
```
## Mouse Button and Movement Concepts
* **Down** - Indicates the user pushed a mouse button down.
@@ -355,12 +417,25 @@ view.MouseEvent += (s, e) =>
* **Use Mouse Bindings and Commands** for simple mouse interactions - they integrate well with the Command system and work alongside keyboard bindings
* **Use the `Selecting` event** to handle mouse clicks - it's raised by the default `Command.Select` binding for all mouse buttons
* **Access mouse details via CommandContext** when you need position or flags in `Selecting` handlers
* **Handle Mouse Events directly** for complex interactions like drag-and-drop or custom gestures
* **Access mouse details via CommandContext** when you need position or flags in `Selecting` handlers:
```cs
view.Selecting += (s, e) =>
{
if (e.Context is CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
{
Point position = mouseArgs.Position;
MouseFlags flags = mouseArgs.Flags;
// Handle with position and flags
}
};
```
* **Handle Mouse Events directly** only for complex interactions like drag-and-drop or custom gestures (override `OnMouseEvent` or subscribe to `MouseEvent`)
* **Use `HighlightStates`** to enable automatic mouse grab and visual feedback - views will automatically grab the mouse and update their appearance
* **Use `WantContinuousButtonPressed`** for repeating actions (scroll buttons, increment/decrement) - the view will receive repeated events while the button is held
* **Respect platform conventions** - use right-click for context menus, double-click for default actions
* **Provide keyboard alternatives** - ensure all mouse functionality has keyboard equivalents
* **Test with different terminals** - mouse support varies between terminal applications
* **Use Mouse State** to provide visual feedback when users hover or interact with views
* **Mouse grab is automatic** - you don't need to manually call `GrabMouse()`/`UngrabMouse()` when using `HighlightStates` or `WantContinuousButtonPressed`
## Limitations and Considerations