Skip to content

Fix: relayout without a full recalculation unmounts children and suspends hit testing - #157

Open
epo33 wants to merge 1 commit into
nabil6391:masterfrom
epo33:fix-relayout-without-recalculation
Open

Fix: relayout without a full recalculation unmounts children and suspends hit testing#157
epo33 wants to merge 1 commit into
nabil6391:masterfrom
epo33:fix-relayout-without-recalculation

Conversation

@epo33

@epo33 epo33 commented Jul 25, 2026

Copy link
Copy Markdown

Bugs

Both issues share the same trigger: a layout pass that does not go through the full-recalculation path. Such relayouts are easy to trigger: node children are laid out with loose constraints and parentUsesSize: true, so they are not relayout boundaries. Any descendant marking itself dirty propagates markNeedsLayout up to RenderCustomLayoutBox. A Tooltip inside a node does it when it fires (its OverlayPortal marks needsLayout), an Image does it when its data arrives, etc.

1. Every such relayout unmounts all node children

RenderCustomLayoutBox.performLayout always runs the startLayout()/endLayout() cycle of GraphViewElement, but only populates the children (via buildOrObtainChildForbuildChild/reuseChild) when _needsFullRecalculation || !_isInitialized is true.

startLayout() resets _newNodeToElement to an empty map, and endLayout() unmounts every element that was not re-registered during the pass, then replaces _nodeToElement with the (empty) new map. As a consequence, any relayout that does not go through the full-recalculation path unmounts every node widget, and the view goes blank.

Rebuilding the GraphView "repairs" the graph because the delegate setter unconditionally sets _needsFullRecalculation = true, which makes the symptom intermittent and hard to track.

2. Every such relayout restarts the node animation and suspends hit testing

performLayout unconditionally calls _updateAnimationStates() (animated mode), which does reset() + forward() on the node animation controller even when no node moved. Meanwhile hitTestChildren rejects every hit while that animation is not completed.

Combined with a hover Tooltip inside a node this produces an endless blink loop: the tooltip opens → its OverlayPortal triggers a relayout → the (no-op) animation restarts → nodes stop responding to hit tests → the mouse tracker reports an exit → the tooltip closes → the overlay removal triggers another relayout → another 600ms without hit testing → once the animation completes the pointer "re-enters" and the tooltip opens again.

Repro

The added widget tests build a two-node GraphView.builder, then call setState inside one node child (changing its height) without rebuilding the GraphView. Before the fix, both nodes disappear from the element tree, and a tap right after such a relayout hits nothing; with the fix the children stay mounted and remain tappable.

Fix

In the layout pass that skips the full recalculation:

  • still visit every node through _layoutNodesLazily, so existing children are re-registered through reuseChild and survive endLayout(). Positions are untouched (the algorithm is not re-run); children are simply re-laid out against the same loose constraints;
  • return before _updateAnimationStates()/_updateNodePositions(): no node moved, so there is nothing to animate, and restarting the animation would only suspend hit testing and cut short an animation still running.

The whole test suite passes (39 tests, including the 2 added ones).

A layout pass that skips the full recalculation still runs the
startLayout()/endLayout() cycle, which unmounted every node child, and it
restarted the node animation, which suspends hit testing while it runs.

- Re-register existing children through _layoutNodesLazily so they are
  reused instead of being unmounted.
- Skip _updateAnimationStates()/_updateNodePositions() on such passes: no
  node moved, so there is nothing to animate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant