expandAll method

void expandAll({
  1. bool animate = true,
  2. int? maxDepth,
})

Expands all nodes in the tree.

Uses batch operations for better performance with large trees.

Implementation

void expandAll({bool animate = true, int? maxDepth}) {
  if (_animationStyle.expandCollapse.duration == Duration.zero) {
    animate = false;
  }
  // Flush any pending visible-order rebuild from a prior in-batch mutator
  // (moveNode, reorderRoots, reorderChildren, cancelDeletion, and so on).
  // The collection loop below classifies children via `_order.contains`;
  // inside a [runBatch] the order still reflects state at batch entry,
  // so a child whose visibility was changed by an earlier in-batch
  // mutation would be misclassified, omitted from nodesToShow (pops in
  // at full extent, never joins the bulk group). Same pattern as
  // [insert] / [expand] / [collapse].
  _ensureVisibleOrder();
  // Collect all nodes to expand, nodes to show, and nodes currently exiting
  final nodesToExpand = <TKey>[];
  final nodesToShow = <TKey>[];
  final nodesToReverseExit = <TKey>[];
  // Scratch for the expansion-gated flatten harvest below.
  final flattenScratch = <TKey>[];

  // Iterative DFS. Depth is recomputed per-visit via [_depthOfKey]
  // (matching the original recursive implementation) so we do not
  // need to carry depth along in a parallel stack.
  final stack = <TKey>[];
  for (final rootId in _roots) {
    stack.add(rootId);
  }
  while (stack.isNotEmpty) {
    final key = stack.removeLast();
    if (_isPendingDeletion(key)) {
      continue;
    }
    final children = _childListOf(key);
    if (children == null || children.isEmpty) {
      continue;
    }

    final depth = _depthOfKey(key);
    final withinDepthLimit = maxDepth == null || depth < maxDepth;

    if (withinDepthLimit && !_isExpandedKey(key)) {
      nodesToExpand.add(key);
      for (final childId in children) {
        if (!_order.contains(childId)) {
          // Harvest the child plus everything reachable through
          // already-expanded interior nodes: a hidden node B whose own
          // expansion flag stayed true through an ancestor collapse
          // becomes visible together with its children when [key]
          // expands. The DFS below only harvests at nodes it flips
          // (already-expanded children are never re-pushed as
          // nodesToExpand, and depth-limited nodes are not descended
          // into), so without this flatten those revealed descendants
          // would render at full extent from frame one while everything
          // around them animates, expand(key:) on the identical
          // structure animates the whole revealed subtree.
          flattenScratch.clear();
          _flattenSubtreeInto(childId, flattenScratch, includeRoot: true);
          for (final k in flattenScratch) {
            // Preserve the child-level invariants: skip nodes already
            // in the order (e.g. still animating an exit under this
            // collapsed ancestor) and pending-deletion nodes.
            if (!_order.contains(k) && !_isPendingDeletion(k)) {
              nodesToShow.add(k);
            }
          }
        }
      }
    }

    // Harvest exiting children only when this parent is POST-FLIP
    // expanded, which by induction from the roots means the children are
    // post-flip visible: the DFS pushes children only when the parent is
    // withinDepthLimit, and such a parent is either already expanded or
    // joins nodesToExpand. A child harvested under a parent that stays
    // collapsed would be un-pended below, grow back to full extent, and
    // stay in the visible order (the terminal handlers never remove a
    // completed enter).
    final postFlipExpanded = withinDepthLimit || _isExpandedKey(key);
    if (postFlipExpanded) {
      for (final childId in children) {
        // Check standalone exiting
        final animation = _standaloneAt(childId);
        if (animation != null && animation.type == AnimationType.exiting) {
          if (!_isPendingDeletion(childId)) {
            nodesToReverseExit.add(childId);
          }
        }
        // Check operation group exiting (pendingRemoval)
        final opGroupKey = _operationGroupOf(childId);
        if (opGroupKey != null) {
          final opGroup = _opGroupAt(opGroupKey);
          if (opGroup != null && opGroup.pendingRemoval.contains(childId)) {
            if (!_isPendingDeletion(childId)) {
              nodesToReverseExit.add(childId);
            }
          }
        }
      }
    }

    // Only descend into children if within depth limit.
    if (withinDepthLimit) {
      for (final childId in children) {
        stack.add(childId);
      }
    }
  }
  if (nodesToExpand.isEmpty && nodesToReverseExit.isEmpty) {
    return;
  }
  // Batch update expansion states. Skip per-call ancestors-expanded
  // propagation, we rebuild it wholesale below in O(N).
  for (final key in nodesToExpand) {
    _setExpandedKey(key, true, propagate: false);
    _recordExpansionChange(key, wasExpanded: false);
  }
  _rebuildAllAncestorsExpanded();
  // Rebuild visible order from scratch (more efficient for bulk operations)
  _markVisibleOrderDirty();
  // Flush the deferred rebuild NOW. The bulk-member registration loops
  // below gate on `_order.contains(key)` to skip children that aren't
  // structurally visible; inside a [runBatch], `_markVisibleOrderDirty`
  // sets only a dirty flag and `_order` remains pre-mutation. Without
  // this flush, every newly-visible child fails the `contains` check
  // and never gets added to the bulk group, so the bulk animation runs
  // empty and the children appear at full extent in one frame.
  // Outside a batch this is a no-op (the mark already triggered the
  // rebuild synchronously); inside a batch it forces consumption now,
  // and any subsequent in-batch mutation can re-mark dirty as needed.
  _ensureVisibleOrder();
  // The harvest gate above makes this exact, not conservative: every
  // harvested reverse-exit key is post-flip visible.
  assert(nodesToReverseExit.every(_ancestorsExpandedFast));
  // Start animations for newly visible nodes and reverse exiting animations
  if (animate) {
    // Reverse collapsing operation groups. Snapshot before iterating: a
    // group's controller already at upperBound would fire `completed`
    // synchronously inside `forward()`, removing itself from `_groups`
    // mid-iteration. See [_opGroupSnapshot] docs.
    bool opGroupReversed = false;
    _opGroupSnapshot
      ..clear()
      ..addAll(_opGroupEntries);
    for (final entry in _opGroupSnapshot) {
      final group = entry.value;
      if (group.pendingRemoval.isNotEmpty) {
        _partitionByPostFlipVisibility(
          group.members.keys,
          _partitionVisibleScratch,
          _partitionHiddenScratch,
        );
        if (_partitionVisibleScratch.isEmpty) {
          // No member is post-flip visible: leave the group pended and
          // reversing. Its dismissed handler removes the members via
          // _ancestorsExpandedFast; un-pending here would grow them
          // back to full extent under a collapsed chain.
          continue;
        }
        opGroupReversed = true;
        // Capture the curved value BEFORE any write: the displacement
        // reads it, and the rebase below re-anchors against it.
        final preReversalCurvedValue = group.curvedValue;
        // Displace the post-flip HIDDEN members to standalone exits
        // BEFORE the pendingRemoval.clear() (so disposeIfEmpty stays
        // hard-blocked by the non-empty pending set during the
        // detaches) and BEFORE the rebase and reset (the capture inside
        // _startStandaloneExitAnimation reads the endpoints the rebase
        // overwrites and the curved value the reset moves).
        for (final key in _partitionHiddenScratch) {
          _startStandaloneExitAnimation(
            key,
            family: TreeAnimationFamily.expandCollapse,
          );
        }
        group.pendingRemoval.clear();
        // Path-1 rebase (expand Path 1) over the SURVIVORS: capture
        // each member's CURRENT painted extent under the OLD envelope,
        // re-anchor start to it, target full, and reset the controller
        // so computeExtent at value 0 reproduces exactly that extent.
        // Re-targeting alone pops by (full - oldTarget) * curvedValue.
        //
        // `?? defaultExtent` is deliberate and REPLACES the
        // `?? _unknownExtent` that stood here. It is the same fallback
        // the read path applies, so an unmeasured member's captured
        // extent is its true painted one; and the sentinel must not be
        // WRITTEN here, because computeExtent ignores both endpoints
        // under it and the reset below would drop the row to zero in a
        // single frame. An INCOMING sentinel is a different thing (the
        // old collapseAll reversal never touched targetExtent, so one
        // could arrive here); the capture absorbs it either way,
        // returning full * preReversalCurvedValue, the member's true
        // painted extent, so the sentinel leaves the record here.
        for (final member in group.members.entries) {
          final full = _fullExtentOf(member.key) ?? defaultExtent;
          member.value.startExtent = member.value.computeExtent(
            preReversalCurvedValue,
            full,
          );
          member.value.targetExtent = full;
          // A natural reference, not a capture: cleared unconditionally
          // for the reason stated at expand Path 1.
          member.value.targetIsCaptured = false;
        }
        _anim.opGroups.runWithGroupDetached(entry.key, (g) {
          g.controller.value = 0.0;
        });
        group.controller.forward();
      }
    }
    _opGroupSnapshot.clear();
    if (opGroupReversed) _bumpAnimGen();

    // Check if there's a collapsing bulk animation we can reverse
    if (_activeBulkGroup != null &&
        _activeBulkGroup!.pendingRemoval.isNotEmpty) {
      // Partition the pending set by post-flip visibility: only a
      // post-flip-visible member may un-pend and grow back; a member
      // whose chain stays collapsed is displaced to a standalone exit
      // so it finishes shrinking away instead of becoming a permanent
      // hidden-parent row (the exit's finalizer removes it from the
      // order via _ancestorsExpandedFast).
      _partitionByPostFlipVisibility(
        _activeBulkGroup!.pendingRemoval,
        _partitionVisibleScratch,
        _partitionHiddenScratch,
      );
      for (final key in _partitionHiddenScratch) {
        // Captures full * bulk.value and removes the member from both
        // bulk sets.
        _startStandaloneExitAnimation(
          key,
          family: TreeAnimationFamily.expandCollapse,
        );
      }
      final anyVisible = _partitionVisibleScratch.isNotEmpty;
      for (final key in _partitionVisibleScratch) {
        _removeBulkPending(key);
      }
      // Both sub-cases share one joiner policy: reversing exits and
      // genuinely new rows ride standalone enters, never the mid-flight
      // group. The partition only decides whether forward() runs.
      _routeShownNodesToStandaloneEnters(nodesToReverseExit, nodesToShow);
      if (anyVisible) {
        // Reverse the controller direction
        _activeBulkGroup!.controller.forward();
      }
      _bumpBulkGen();
    } else if (_activeBulkGroup != null &&
        _activeBulkGroup!.members.isNotEmpty) {
      // Continuation: a bulk expand is already mid-flight (members
      // present, nothing pending removal). Keep the group, existing
      // members continue from their current extent. Creating a fresh
      // group here would dispose the in-flight one and pop every
      // half-expanded member to full extent in a single frame.
      _routeShownNodesToStandaloneEnters(nodesToReverseExit, nodesToShow);
      _bumpBulkGen();
    } else {
      // Create fresh group via the BulkAnimator (auto-disposes any
      // prior). All listener wiring happens inside createGroup.
      _anim.bulk.createGroup(
        _animationStyle.expandCollapse.duration,
        _animationStyle.expandCollapse.curve,
      );
      _bumpBulkGen(); // invalidate bulk + broad generation caches

      // Reverse standalone exit animations smoothly
      for (final key in nodesToReverseExit) {
        if (!_hasOperationGroup(key)) {
          _startStandaloneEnterAnimation(
            key,
            family: TreeAnimationFamily.expandCollapse,
          );
        }
      }

      // Add new nodes to the bulk group (skip if already in an operation
      // group)
      for (final key in nodesToShow) {
        if (_order.contains(key) && !_hasOperationGroup(key)) {
          _addBulkMember(key);
        }
      }

      // Start expanding (value 0 -> 1)
      _activeBulkGroup!.controller.forward();
      _bumpBulkGen();
    }
  } else {
    // Remove animations if not animating
    for (final key in nodesToReverseExit) {
      _removeAnimation(key);
    }
  }
  // Bulk expansion touches many ancestors' expansion state + every
  // previously-collapsed node now flips its chevron. Enumerating the
  // affected set precisely is error-prone; fall back to a full refresh.
  _notifyStructural();
}