Skip to content

Commit

Permalink
Fixes #2578 - Updates mouse events to be relative to View.Bounds (#…
Browse files Browse the repository at this point in the history
…2920)

* initial commit

* Clarified RootMouseEvent

* Added application mouse coord tests

* ViewToScreen -> BoundsToScreen

* Simplified View.Move

* Simplified View.Move

* Updated API docs; made some functions private

* more ViewLayout cleanup

* more ViewLayout cleanup

* Added View.ScreenToBounds and low-level coord unit tests

* Partial fix

* Refactored Application.OnMouseEvent... Tests still fail and views are broken

* Added Bounds/FrameToScreen

* Remamed ScreenToView->ScreenToFrame

* All unit tests pass

* Fixed ListView

* Fixed TableView

* Fixed ColorPicker

* Fixed RadioGroup

* Fixed ListView unit tests

* Fixed line drawing scenario

* Updated comment

* fixed api doc typo

* fixed formatting

* added some thickness Contains unit tests

* MouseEvent api doc updates

* More thickness tests

* More thickness tests
  • Loading branch information
tig committed Nov 26, 2023
1 parent 3f4d96b commit 42b9ad1
Show file tree
Hide file tree
Showing 35 changed files with 2,299 additions and 1,418 deletions.
197 changes: 140 additions & 57 deletions Terminal.Gui/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ static void ResetState ()
NotifyNewRunState = null;
NotifyStopRunState = null;
_initialized = false;
_mouseGrabView = null;
_lastMouseOwnerView = null;
MouseGrabView = null;
_mouseEnteredView = null;

// Reset synchronization context to allow the user to run async/await,
// as the main loop has been ended, the synchronization context from
Expand Down Expand Up @@ -304,7 +304,7 @@ public static RunState Begin (Toplevel Toplevel)
}

// Ensure the mouse is ungrabed.
_mouseGrabView = null;
MouseGrabView = null;

var rs = new RunState (Toplevel);

Expand Down Expand Up @@ -656,7 +656,7 @@ public static void RunIteration (ref RunState state, ref bool firstIteration)
}

MainLoop.RunIteration ();
Iteration?.Invoke (null, new IterationEventArgs());
Iteration?.Invoke (null, new IterationEventArgs ());

EnsureModalOrVisibleAlwaysOnTop (state.Toplevel);
if (state.Toplevel != Current) {
Expand Down Expand Up @@ -1018,20 +1018,19 @@ public static bool OnSizeChanging (SizeChangedEventArgs args)
/// </summary>
public static View WantContinuousButtonPressedView { get; private set; }

static View _mouseGrabView;

/// <summary>
/// The view that grabbed the mouse, to where mouse events will be routed to.
/// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be
/// routed to this view until the view calls <see cref="UngrabMouse"/> or the mouse is released.
/// </summary>
public static View MouseGrabView => _mouseGrabView;
public static View MouseGrabView { get; private set; }

/// <summary>
/// Invoked when a view wants to grab the mouse; can be canceled.
/// </summary>
public static event EventHandler<GrabMouseEventArgs> GrabbingMouse;

/// <summary>
/// Invoked when a view wants ungrab the mouse; can be canceled.
/// Invoked when a view wants un-grab the mouse; can be canceled.
/// </summary>
public static event EventHandler<GrabMouseEventArgs> UnGrabbingMouse;

Expand All @@ -1041,7 +1040,7 @@ public static bool OnSizeChanging (SizeChangedEventArgs args)
public static event EventHandler<ViewEventArgs> GrabbedMouse;

/// <summary>
/// Invoked after a view has ungrabbed the mouse.
/// Invoked after a view has un-grabbed the mouse.
/// </summary>
public static event EventHandler<ViewEventArgs> UnGrabbedMouse;

Expand All @@ -1051,12 +1050,12 @@ public static bool OnSizeChanging (SizeChangedEventArgs args)
/// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
public static void GrabMouse (View view)
{
if (view == null)
if (view == null) {
return;
}
if (!OnGrabbingMouse (view)) {
OnGrabbedMouse (view);
_mouseGrabView = view;
//Driver.UncookMouse ();
MouseGrabView = view;
}
}

Expand All @@ -1065,48 +1064,53 @@ public static void GrabMouse (View view)
/// </summary>
public static void UngrabMouse ()
{
if (_mouseGrabView == null)
if (MouseGrabView == null) {
return;
if (!OnUnGrabbingMouse (_mouseGrabView)) {
OnUnGrabbedMouse (_mouseGrabView);
_mouseGrabView = null;
//Driver.CookMouse ();
}
if (!OnUnGrabbingMouse (MouseGrabView)) {
OnUnGrabbedMouse (MouseGrabView);
MouseGrabView = null;
}
}

static bool OnGrabbingMouse (View view)
{
if (view == null)
if (view == null) {
return false;
}
var evArgs = new GrabMouseEventArgs (view);
GrabbingMouse?.Invoke (view, evArgs);
return evArgs.Cancel;
}

static bool OnUnGrabbingMouse (View view)
{
if (view == null)
if (view == null) {
return false;
}
var evArgs = new GrabMouseEventArgs (view);
UnGrabbingMouse?.Invoke (view, evArgs);
return evArgs.Cancel;
}

static void OnGrabbedMouse (View view)
{
if (view == null)
if (view == null) {
return;
}
GrabbedMouse?.Invoke (view, new ViewEventArgs (view));
}

static void OnUnGrabbedMouse (View view)
{
if (view == null)
if (view == null) {
return;
}
UnGrabbedMouse?.Invoke (view, new ViewEventArgs (view));
}

static View _lastMouseOwnerView;
// Used by OnMouseEvent to track the last view that was clicked on.
static View _mouseEnteredView;

/// <summary>
/// Event fired when a mouse move or click occurs. Coordinates are screen relative.
Expand All @@ -1128,16 +1132,16 @@ static void OnUnGrabbedMouse (View view)
/// <remarks>
/// This method can be used to simulate a mouse event, e.g. in unit tests.
/// </remarks>
/// <param name="a"></param>
/// <param name="a">The mouse event with coordinates relative to the screen.</param>
public static void OnMouseEvent (MouseEventEventArgs a)
{
static bool OutsideBounds (Point p, Rect r) => p.X < 0 || p.X > r.Right || p.Y < 0 || p.Y > r.Bottom;
static bool OutsideRect (Point p, Rect r) => p.X < 0 || p.X > r.Right || p.Y < 0 || p.Y > r.Bottom;

if (IsMouseDisabled) {
return;
}

var view = View.FindDeepestView (Current, a.MouseEvent.X, a.MouseEvent.Y, out int rx, out int ry);
var view = View.FindDeepestView (Current, a.MouseEvent.X, a.MouseEvent.Y, out int screenX, out int screenY);

if (view != null && view.WantContinuousButtonPressed) {
WantContinuousButtonPressedView = view;
Expand All @@ -1153,8 +1157,10 @@ public static void OnMouseEvent (MouseEventEventArgs a)
return;
}

if (_mouseGrabView != null) {
var newxy = _mouseGrabView.ScreenToView (a.MouseEvent.X, a.MouseEvent.Y);
if (MouseGrabView != null) {
// If the mouse is grabbed, send the event to the view that grabbed it.
// The coordinates are relative to the Bounds of the view that grabbed the mouse.
var newxy = MouseGrabView.ScreenToFrame (a.MouseEvent.X, a.MouseEvent.Y);
var nme = new MouseEvent () {
X = newxy.X,
Y = newxy.Y,
Expand All @@ -1163,57 +1169,134 @@ public static void OnMouseEvent (MouseEventEventArgs a)
OfY = a.MouseEvent.Y - newxy.Y,
View = view
};
if (OutsideBounds (new Point (nme.X, nme.Y), _mouseGrabView.Bounds)) {
_lastMouseOwnerView?.OnMouseLeave (a.MouseEvent);
if (OutsideRect (new Point (nme.X, nme.Y), MouseGrabView.Bounds)) {
// The mouse has moved outside the bounds of the the view that
// grabbed the mouse, so we tell the view that last got
// OnMouseEnter the mouse is leaving
// BUGBUG: That sentence makes no sense. Either I'm missing something
// or this logic is flawed.
_mouseEnteredView?.OnMouseLeave (a.MouseEvent);
}
//System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
if (_mouseGrabView?.OnMouseEvent (nme) == true) {
if (MouseGrabView?.OnMouseEvent (nme) == true) {
return;
}
}

if ((view == null || view == OverlappedTop) && !Current.Modal && OverlappedTop != null
&& a.MouseEvent.Flags != MouseFlags.ReportMousePosition && a.MouseEvent.Flags != 0) {
if ((view == null || view == OverlappedTop) &&
Current is { Modal: false } && OverlappedTop != null &&
a.MouseEvent.Flags != MouseFlags.ReportMousePosition &&
a.MouseEvent.Flags != 0) {

var top = FindDeepestTop (Top, a.MouseEvent.X, a.MouseEvent.Y, out _, out _);
view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y, out rx, out ry);
view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y, out screenX, out screenY);

if (view != null && view != OverlappedTop && top != Current) {
MoveCurrent ((Toplevel)top);
}
}

bool FrameHandledMouseEvent (Frame frame)
{
if (frame?.Thickness.Contains (frame.FrameToScreen (), a.MouseEvent.X, a.MouseEvent.Y) ?? false) {
var boundsPoint = frame.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y);
var me = new MouseEvent () {
X = boundsPoint.X,
Y = boundsPoint.Y,
Flags = a.MouseEvent.Flags,
OfX = boundsPoint.X,
OfY = boundsPoint.Y,
View = frame
};
frame.OnMouseEvent (me);
return true;
}
return false;
}

if (view != null) {
var nme = new MouseEvent () {
X = rx,
Y = ry,
Flags = a.MouseEvent.Flags,
OfX = 0,
OfY = 0,
View = view
};
// Work inside-out (Padding, Border, Margin)
// TODO: Debate whether inside-out or outside-in is the right strategy
if (FrameHandledMouseEvent (view?.Padding)) {
return;
}
if (FrameHandledMouseEvent (view?.Border)) {
if (view is Toplevel) {
// TODO: This is a temporary hack to work around the fact that
// drag handling is handled in Toplevel (See Issue #2537)

var me = new MouseEvent () {
X = screenX,
Y = screenY,
Flags = a.MouseEvent.Flags,
OfX = screenX,
OfY = screenY,
View = view
};

if (_mouseEnteredView == null) {
_mouseEnteredView = view;
view.OnMouseEnter (me);
} else if (_mouseEnteredView != view) {
_mouseEnteredView.OnMouseLeave (me);
view.OnMouseEnter (me);
_mouseEnteredView = view;
}

if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition) {
return;
}

if (_lastMouseOwnerView == null) {
_lastMouseOwnerView = view;
view.OnMouseEnter (nme);
} else if (_lastMouseOwnerView != view) {
_lastMouseOwnerView.OnMouseLeave (nme);
view.OnMouseEnter (nme);
_lastMouseOwnerView = view;
WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;

if (view.OnMouseEvent (me)) {
// Should we bubble up the event, if it is not handled?
//return;
}

BringOverlappedTopToFront ();
}
return;
}

if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition)
if (FrameHandledMouseEvent (view?.Margin)) {
return;
}

var bounds = view.BoundsToScreen (view.Bounds);
if (bounds.Contains (a.MouseEvent.X, a.MouseEvent.Y)) {
var boundsPoint = view.ScreenToBounds (a.MouseEvent.X, a.MouseEvent.Y);
var me = new MouseEvent () {
X = boundsPoint.X,
Y = boundsPoint.Y,
Flags = a.MouseEvent.Flags,
OfX = boundsPoint.X,
OfY = boundsPoint.Y,
View = view
};

if (_mouseEnteredView == null) {
_mouseEnteredView = view;
view.OnMouseEnter (me);
} else if (_mouseEnteredView != view) {
_mouseEnteredView.OnMouseLeave (me);
view.OnMouseEnter (me);
_mouseEnteredView = view;
}

if (view.WantContinuousButtonPressed)
WantContinuousButtonPressedView = view;
else
WantContinuousButtonPressedView = null;
if (!view.WantMousePositionReports && a.MouseEvent.Flags == MouseFlags.ReportMousePosition) {
return;
}

WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null;

// Should we bubbled up the event, if it is not handled?
view.OnMouseEvent (nme);
if (view.OnMouseEvent (me)) {
// Should we bubble up the event, if it is not handled?
//return;
}

BringOverlappedTopToFront ();
BringOverlappedTopToFront ();
}
}
}
#endregion Mouse handling
Expand Down
13 changes: 13 additions & 0 deletions Terminal.Gui/Drawing/Thickness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ public int Horizontal {
}
}

/// <summary>
/// Gets whether the specified coordinates lie within the thickness (inside the bounding rectangle but outside of
/// the rectangle described by <see cref="GetInside(Rect)"/>.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns><see langword="true"/> if the specified coordinate is within the thickness; <see langword="false"/> otherwise.</returns>
public bool Contains (Rect outside, int x, int y)

Check warning on line 114 in Terminal.Gui/Drawing/Thickness.cs

View workflow job for this annotation

GitHub Actions / build_and_test

Parameter 'outside' has no matching param tag in the XML comment for 'Thickness.Contains(Rect, int, int)' (but other parameters do)

Check warning on line 114 in Terminal.Gui/Drawing/Thickness.cs

View workflow job for this annotation

GitHub Actions / build_and_test

Parameter 'outside' has no matching param tag in the XML comment for 'Thickness.Contains(Rect, int, int)' (but other parameters do)

Check warning on line 114 in Terminal.Gui/Drawing/Thickness.cs

View workflow job for this annotation

GitHub Actions / Build and Publish to Nuget.org

Parameter 'outside' has no matching param tag in the XML comment for 'Thickness.Contains(Rect, int, int)' (but other parameters do)

Check warning on line 114 in Terminal.Gui/Drawing/Thickness.cs

View workflow job for this annotation

GitHub Actions / Build and Publish to Nuget.org

Parameter 'outside' has no matching param tag in the XML comment for 'Thickness.Contains(Rect, int, int)' (but other parameters do)
{
var inside = GetInside (outside);
return outside.Contains (x, y) && !inside.Contains (x, y);
}

/// <summary>
/// Returns a rectangle describing the location and size of the inside area of <paramref name="rect"/>
/// with the thickness widths subtracted. The height and width of the returned rectangle will
Expand Down
Loading

0 comments on commit 42b9ad1

Please sign in to comment.