fix!: Add HoverCallbacks.onHoverCancel for press-while-hovering#3912
fix!: Add HoverCallbacks.onHoverCancel for press-while-hovering#3912Barba2k2 wants to merge 4 commits intoflame-engine:mainfrom
Conversation
Flutter's MouseRegion only emits onHover events when no buttons are pressed. During a press the same physical motion surfaces as a PointerMoveEvent on the surrounding Listener and never reaches the hover dispatcher, so HoverCallbacks components stop receiving onHoverEnter / onHoverExit the moment the user clicks down — the behaviour reported in issue flame-engine#2741. Forward Listener.onPointerMove to the same game.mouseDetector by synthesising a PointerHoverEvent from the move event's pointer data. The synthesized: true flag preserves provenance for any consumer that inspects it. Adds a widget regression test that hovers in, presses, drags out, and drags back in to verify enter/exit fire on each transition. Fixes flame-engine#2741
Format the touched file to match `melos run format-check`.
spydon
left a comment
There was a problem hiding this comment.
Hey, thanks for your contribution.
Please follow the PR template, there are tags in there used for the part that should go into the git history.
There was a problem hiding this comment.
Drag out while still pressed — expect onHoverExit to fire (this fails on main without the patch).
It's no longer a hover event once a button is pressed.
We should call onHoverCancel as soon as a button is pressed though.
Edit: apparently it is still counted as a hover in some languages, but this would be a breaking change for us so we would have to think about how to handle this...
|
@spydon reformatted the description to follow the PR template. On the |
|
No matter the route we go here it is technically a "breaking change" - we are changing the behaviour of hovers regardless. So I think we should just pick what we think is the ideal end state and just bite the bullet. From what I am gathering the options are:
I don't have a strong preference for behaviour here, it seems that 1 is contradicting what Flutter would do but it is arguably more intuitive? wondering if there are any reasons they decided to not consider hovers after a press. I do like 2 as a pretty reasonable option that is more in-line with how Flutter handles things. |
|
If we go with 1 it will also be affecting drags right? |
@Barba2k2 I'd argue that this is a bigger breaking change, since it now will affect people that mix |
|
@spydon @luanpotter — converged, going with option 2. Concrete plan before I push so we agree on shape: Public API ( /// Called when a hover is interrupted because a pointer button was pressed
/// while the component was hovered. After a cancel, the component is no
/// longer in the hovered state; `onHoverEnter` will fire again only when a
/// fresh button-free hover re-enters the area.
void onHoverCancel() {}Internal Wiring
Tests Replace the synthesized-hover regression test with two:
Breaking surface Adding a virtual no-op method to a mixin isn't a source-break, but the behavior on press changes (today: nothing fires; this PR: If shape looks right I'll push — otherwise tell me what to tweak (naming, where to hook the press event, whether to also cancel on |
…ne#2741) Replace the synthesize-hover-from-PointerMove bridge with a dedicated onHoverCancel() callback on HoverCallbacks. PointerMoveDispatcher now walks its tracked records on PointerDown and fires cancelHover() on any hovered HoverCallbacks, mirroring how DragCallbacks handles cancellation without affecting the existing drag dispatcher. Per maintainer review on the PR (spydon, luanpotter): synthesizing hover events during a press affects components that mix HoverCallbacks with DragCallbacks, which is a larger behavior change than introducing a new no-op callback. Adding onHoverCancel keeps the existing hover contract intact for unaffected users and makes the press semantics explicit for those who want to react to it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pivot pushed in 145641f — implementation matches the plan I laid out above. Title bumped to |
spydon
left a comment
There was a problem hiding this comment.
Lgtm, thanks for your contribution
Description
Flutter's
MouseRegiononly emitsonHoverevents while no buttons are pressed. The moment a user presses a button mid-hover,MouseRegion.onHoverstops firing and the hoveredHoverCallbackscomponent 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 mixHoverCallbackswithDragCallbacks— drags would have started leaking into the hover dispatcher — which is a larger behavior change than introducing a new no-op callback.What changed
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.PointerMoveDispatchergains an internal_handlePointerPresshook that walks its already-tracked_recordsand firescancelHover()on any hoveredHoverCallbacks. The hover state ends and stays ended until a fresh button-free hover re-enters the area.Game.mousePressDetectoris added, mirroring the existingmouseDetectorsetter pattern. The dispatcher wires it on mount, clears it on remove.applyMouseDetectorsforwardsListener.onPointerDowntomousePressDetector.DragCallbacksis not affected — drags continue to flow throughMultiDragDispatcherexactly as before; the press hook only consults components that mix inHoverCallbacks.Behavior summary
onHoverEnterfiresonHoverEnterfiresonHoverCancelfiresonHoverEnterfires againonHoverExitfiresonHoverExitfires (unchanged)Tests
hover_callbacks_test.dartadds atestWidgetsregression that pumps aGameWidget, hovers into a component, presses, drags around while held (verifying no spurious enter/exit/cancel beyond the initial cancel), then releases and re-hovers (verifyingonHoverEnterfires again). The existing pure-dispatcher test for plain hover behavior is untouched.Checklist
docsand added dartdoc comments with///.examplesordocs.Breaking Change?
Migration instructions
Adding the new
onHoverCancelvirtual toHoverCallbacksis source-compatible (default is a no-op), but the runtime behavior of pressing a button while hovering changes:isHoveredstayedtruewhile a button was held, that assumption no longer holds —isHoveredbecomesfalsethe moment a press starts.onHoverExitfiring on press (it didn't reliably before this fix in any case), overrideonHoverCancelinstead — it's the explicit hook for that transition and won't be confused with a "mouse left the area" exit.No migration is required for components that don't observe these specific edge cases.
Related Issues
Fixes #2741