Use CachedViewsUnderMouse in HandleAutoGrabPress; skip disabled SubViews

Replace the redundant GetViewsUnderLocation call with a read from the
already-populated CachedViewsUnderMouse. Walk the cache backwards to
find the deepest *enabled* view, so disabled SubViews don't block the
SuperView from grabbing the mouse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tig
2026-02-06 11:31:11 -07:00
parent 002ce88883
commit 69e14401b9
2 changed files with 79 additions and 12 deletions

View File

@@ -572,21 +572,26 @@ public partial class View // Mouse APIs
return false;
}
// Don't grab if a SubView at the mouse position can handle the event
// This ensures that SubViews receive their own mouse events even when the SuperView has MouseHighlightStates set
if (mouse.Position is { } pos && Viewport.Contains (pos))
// Don't grab if an enabled SubView at the mouse position can handle the event.
// CachedViewsUnderMouse is already updated by RaiseMouseEnterLeaveEvents before NewMouseEvent runs.
// Disabled views are included in the cache, so we must find the deepest enabled view.
if (App?.Mouse.CachedViewsUnderMouse is { Count: > 0 } cached)
{
// Convert viewport-relative position to screen coordinates
Point screenPos = ViewportToScreen (pos);
View? deepestEnabledView = null;
// Get all views under this screen position - the deepest view is at the end of the list
List<View?> viewsUnderMouse = GetViewsUnderLocation (screenPos, ViewportSettingsFlags.TransparentMouse);
View? deepestView = viewsUnderMouse.LastOrDefault ();
// If the deepest view is a SubView of this view (not this view itself), don't grab
if (deepestView is { } && deepestView != this)
for (int i = cached.Count - 1; i >= 0; i--)
{
// A SubView is under the cursor - let it handle its own events
if (cached [i] is { Enabled: true } candidate)
{
deepestEnabledView = candidate;
break;
}
}
if (deepestEnabledView is { } && deepestEnabledView != this)
{
// An enabled SubView is under the cursor - let it handle its own events
return false;
}
}

View File

@@ -225,6 +225,68 @@ public class MouseHighlightStatesSubViewTests
runnable.Dispose ();
}
/// <summary>
/// Tests that when a SubView is disabled, clicking on it routes the event to the SuperView.
/// A disabled SubView should not prevent the SuperView from grabbing the mouse.
/// </summary>
// Claude - Opus 4.6
[Theory]
[InlineData (MouseState.In)]
[InlineData (MouseState.Pressed)]
[InlineData (MouseState.In | MouseState.Pressed)]
public void MouseHighlightStates_DisabledSubView_DoesNotPreventSuperViewGrab (MouseState highlightState)
{
// Arrange
VirtualTimeProvider time = new ();
using IApplication app = Application.Create (time);
app.Init (DriverRegistry.Names.ANSI);
var superViewActivateCount = 0;
var subViewActivateCount = 0;
Runnable runnable = new ();
View superView = new ()
{
Id = "superView",
X = 0,
Y = 0,
Width = 10,
Height = 10,
MouseHighlightStates = highlightState
};
superView.Activating += (_, _) => { superViewActivateCount++; };
View subView = new ()
{
Id = "disabledSubView",
X = 2,
Y = 2,
Width = 5,
Height = 5,
CanFocus = true,
Enabled = false
};
subView.Activating += (_, _) => { subViewActivateCount++; };
superView.Add (subView);
runnable.Add (superView);
app.Begin (runnable);
// Act: Click on the disabled SubView at screen position (3, 3)
app.InjectMouse (new Mouse { ScreenPosition = new Point (3, 3), Flags = MouseFlags.LeftButtonPressed });
app.InjectMouse (new Mouse { ScreenPosition = new Point (3, 3), Flags = MouseFlags.LeftButtonReleased });
// Assert: SuperView should receive the event since SubView is disabled
Assert.Equal (1, superViewActivateCount);
Assert.Equal (0, subViewActivateCount);
runnable.Dispose ();
}
/// <summary>
/// Tests that nested views (SuperView with MouseHighlightStates, SubView with MouseHighlightStates)
/// route events to the deepest view under the mouse.