fix!: Add HoverCallbacks.onHoverCancel for press-while-hovering (#3912)
# Description Flutter's `MouseRegion` only emits `onHover` events while **no** buttons are pressed. The moment a user presses a button mid-hover, `MouseRegion.onHover` stops firing and the hovered `HoverCallbacks` component never learns the hover ended — see #2741. The component stays "hovered" forever from its own perspective, even though no further hover events will ever arrive until release. This PR introduces an explicit cancel signal for that transition rather than synthesizing fake hover events from `PointerMove` (which an earlier revision of this PR did). Per review feedback from @spydon and @luanpotter, the synthesized-hover approach affected components that mix `HoverCallbacks` with `DragCallbacks` — drags would have started leaking into the hover dispatcher — which is a larger behavior change than introducing a new no-op callback. ### What changed - New virtual `HoverCallbacks.onHoverCancel()`. Default is a no-op, so existing components don't need to change. Override it to clear hover-driven state (e.g. a highlight) when a press interrupts the hover. - `PointerMoveDispatcher` gains an internal `_handlePointerPress` hook that walks its already-tracked `_records` and fires `cancelHover()` on any hovered `HoverCallbacks`. The hover state ends and stays ended until a fresh button-free hover re-enters the area. - `Game.mousePressDetector` is added, mirroring the existing `mouseDetector` setter pattern. The dispatcher wires it on mount, clears it on remove. - `applyMouseDetectors` forwards `Listener.onPointerDown` to `mousePressDetector`. `DragCallbacks` is **not** affected — drags continue to flow through `MultiDragDispatcher` exactly as before; the press hook only consults components that mix in `HoverCallbacks`. ### Behavior summary | Action | Before this PR | After this PR | | ------------------------------------- | -------------------------- | -------------------------------- | | Hover into a component | `onHoverEnter` fires | `onHoverEnter` fires | | Press a button while hovering | (silent — bug #2741) | `onHoverCancel` fires | | Drag the pressed pointer outside | (silent) | (silent — already cancelled) | | Release the button and re-hover | (silent — still "hovered") | `onHoverEnter` fires again | | Hover out without pressing | `onHoverExit` fires | `onHoverExit` fires (unchanged) | ### Tests `hover_callbacks_test.dart` adds a `testWidgets` regression that pumps a `GameWidget`, hovers into a component, presses, drags around while held (verifying no spurious enter/exit/cancel beyond the initial cancel), then releases and re-hovers (verifying `onHoverEnter` fires again). The existing pure-dispatcher test for plain hover behavior is untouched. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
L
Lorenzo DZ committed
ab354fb37b26e4f5d6398b57a3300a9735fdcdef
Parent: 4889413
Committed by GitHub <noreply@github.com>
on 5/8/2026, 3:50:02 PM