Fix: relayout without a full recalculation unmounts children and suspends hit testing - #157
Open
epo33 wants to merge 1 commit into
Open
Fix: relayout without a full recalculation unmounts children and suspends hit testing#157epo33 wants to merge 1 commit into
epo33 wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 propagatesmarkNeedsLayoutup toRenderCustomLayoutBox. ATooltipinside a node does it when it fires (itsOverlayPortalmarks needsLayout), anImagedoes it when its data arrives, etc.1. Every such relayout unmounts all node children
RenderCustomLayoutBox.performLayoutalways runs thestartLayout()/endLayout()cycle ofGraphViewElement, but only populates the children (viabuildOrObtainChildFor→buildChild/reuseChild) when_needsFullRecalculation || !_isInitializedis true.startLayout()resets_newNodeToElementto an empty map, andendLayout()unmounts every element that was not re-registered during the pass, then replaces_nodeToElementwith 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 thedelegatesetter 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
performLayoutunconditionally calls_updateAnimationStates()(animated mode), which doesreset()+forward()on the node animation controller even when no node moved. MeanwhilehitTestChildrenrejects every hit while that animation is not completed.Combined with a hover
Tooltipinside a node this produces an endless blink loop: the tooltip opens → itsOverlayPortaltriggers 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 callsetStateinside one node child (changing its height) without rebuilding theGraphView. 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:
_layoutNodesLazily, so existing children are re-registered throughreuseChildand surviveendLayout(). Positions are untouched (the algorithm is not re-run); children are simply re-laid out against the same loose constraints;_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).