[Bugfix] Cursor leaving the window never fires on_cell_exit; hovered_cell stays stuck #363
Labels
No labels
Alpha Release Requirement
Bugfix
Demo Target
Documentation
Major Feature
Minor Feature
priority:tier1-active
priority:tier2-foundation
priority:tier3-future
priority:tier4-deferred
Refactoring & Cleanup
system:animation
system:documentation
system:grid
system:input
system:performance
system:procgen
system:python-binding
system:rendering
system:ui-hierarchy
Tiny Feature
workflow:blocked
workflow:needs-benchmark
workflow:needs-documentation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
john/McRogueFace#363
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by adversarial review of the #355 branch (
feat/355-grid-input-revival,85e4bbe). Reported as minor; not fixed there.Defect
UIGridView::updateHover()is driven exclusively byPyScene::do_mouse_hover, which is only invoked fromsf::Event::MouseMoved. There is nosf::Event::MouseLefthandling anywhere in the scene input path.So if the cursor moves off the window while it is over a grid cell:
MouseMovedevents arrive,updateHoveris never called again,on_cell_exitnever fires for the cell the cursor was on, andgrid.hovered_cellremains stuck reporting that cell indefinitely, even though the mouse is nowhere near the window.Consequence
Any hover-driven affordance built on
on_cell_enter/on_cell_exit(tile highlight, tooltip, range preview) gets stranded in its "hovered" visual state when the player's cursor leaves the window, and only clears if they re-enter and move across a boundary.hovered_cellis a lying read.Fix
Handle
sf::Event::MouseLeftinPySceneand dispatch a hover-exit to the drawable tree (anupdateHoverwith a sentinel/"no position", or a dedicatedclearHover()virtual onUIDrawable). Whatever the shape, it must clearhovered_celland fireon_cell_exitfor whichever view was hovering.Note this is a pre-existing gap in the input layer that #355 merely made observable — before #355 no grid hover fired at all.
Test
Needs an injectable
MouseLeftin the automation API (check whether one exists; if not, that is part of the work). Asserton_cell_exitfires andhovered_cell is Noneafter the cursor leaves.Fixed on
feat/355-grid-input-revival(4bb1966). Suite 325/325.The injection question, answered
The issue asked whether an injectable
MouseLeftexists. It does not — but less was needed than expected, becausesf::Event::MouseLeftalready exists in all three backends, andSDL2Renderer.cpp:774already translatesSDL_WINDOWEVENT_LEAVEinto it. The event was being emitted and simply had nobody listening.injectMouseEventis also already type-generic (MouseLeftcarries no payload, so it falls through thedefault:arm straight toprocessEvent).So the automation surface addition is one function,
automation.mouseLeave(), and the only real design question was what position it takes. Answer: none. The cursor is outside the window; there is no position. Taking an(x, y)would have been an invented coordinate dressed up as a real one.MouseLeftcarries no coordinates for exactly this reason, and the exit callbacks instead receive the last position seen bydo_mouse_hover— the same convention the DOM'smouseleaveuses.simulated_mouse_posis deliberately not moved by a leave.I considered and rejected having
moveTo()synthesize a leave when the target falls outside the window bounds. That is magic-by-inference: it would fire on a coordinate that is merely off-screen, and it would silently do nothing in headless mode (no window, no bounds). An explicit call is honest.The fix
Rather than write a parallel teardown path, the leave reuses the hover walk.
PyScene::do_mouse_hover's body becamedispatch_hover(pos, in_window), and a window-leave is that same walk withhit_allowed=falseat the root — which is precisely "the mouse is over nothing". Every hovered drawable then takes its ordinary exit path and none can enter. Grid cells, Frames, and grid overlay children all clear through the code that already knows how to clear them, so there is no second implementation of "what exiting means" to drift out of sync.GameEngine::processEventroutesMouseLeft→PyScene::do_mouse_leave().PyScene::last_mouse_posrecords the last hovered position for the exit callbacks.Confirmed: this was never grid-specific
As suspected in the issue,
Frame.on_exitwas stranded the same way — the test covers it. #355 only made the gap observable by giving grids hover callbacks that could visibly stick.Test
tests/regression/issue_363_hover_exit_on_window_leave_test.py. Beyond the obvious assertions (exit fires,hovered_cell is None), two that carry weight:on_exit;on_cell_enteragain. This is what separates a real clear from a faked one — if the leave had only reported the clear without resettinghovered_cell, the re-entered cell would compare equal to the stale one and the enter would be silently swallowed.Out of scope, deliberately
sf::Event::LostFocus(alt-tab with the cursor stationary over the window) leaves hover stuck by the same mechanism, and the same one-line route would fix it. I did not do it here: it is a distinct trigger with its own semantics question (should an unfocused-but-hovered window report hover? a tooltip arguably should persist), and it is not what this issue describes. Worth its own issue if it matters.The deferred
LostFocushalf is now filed as #367 — it reuses thedo_mouse_leave()/dispatch_hover(pos, in_window)machinery this issue introduced, but carries a policy question this one did not.