insertRoot method

void insertRoot(
  1. TreeNode<TKey, TData> node, {
  2. int? index,
  3. bool animate = true,
  4. bool preservePendingSubtreeState = false,
})

Adds a new root node to the tree.

If animate is true, the node will animate in. If the node is currently pending deletion (animating out from a previous remove), the deletion is cancelled and the node animates back in.

index is the position among live root siblings, exiting (pending-deletion) roots are skipped, matching liveRootKeys / getIndexInParent and the input space of reorderRoots.

preservePendingSubtreeState controls what happens to a re-added mid-exit node's in-flight removal subtree: false (the default) discards it, so the node comes back alone, matching remove(animate: false) followed by a fresh add; true restores it. A discarded descendant whose exit animation is still running is removed when that exit lands, not at the instant of the re-add, so the raw getChildren view still lists it until then.

Implementation

void insertRoot(
  TreeNode<TKey, TData> node, {
  int? index,
  bool animate = true,
  bool preservePendingSubtreeState = false,
}) {
  if (_animationStyle.effectiveEnterExit.duration == Duration.zero) {
    animate = false;
  }
  // See [insert] for the rationale: flush any deferred visible-order
  // rebuild from a prior in-batch mutator so the positional reads
  // below (`_calculateRootInsertIndex`, `_order.length`) operate on
  // fresh state instead of corrupting the buffer.
  _ensureVisibleOrder();
  // If the node is pending deletion, cancel the deletion
  if (_isPendingDeletion(node.key)) {
    // If the node was pending deletion under a non-null parent, detach
    // it and re-attach as a root. Without this relocation, cancelling
    // the deletion would resurrect it under its old parent, silently
    // ignoring the insertRoot() contract.
    final oldParent = _parentKeyOfKey(node.key);
    if (oldParent != null) {
      _childListOf(oldParent)?.remove(node.key);
      _setParentKey(node.key, null);
      // Explicit index is live-space; the comparator path already
      // returns a full-space position.
      final effectiveIndex = index != null
          ? _liveIndexToFullInsertIndex(_roots, index)
          : (comparator != null ? _sortedIndex(_roots, node) : null);
      if (effectiveIndex != null && effectiveIndex < _roots.length) {
        _roots.insert(effectiveIndex, node.key);
      } else {
        _roots.add(node.key);
      }
      _refreshSubtreeDepths(node.key, 0);
      // Raw sibling-list writes above; invalidate cached live indices.
      _liveIndexCache.bump();
    } else if (index != null) {
      // Already a root: honor an explicitly requested index by
      // relocating within _roots. The index is live-space; convert
      // after the removal so the conversion sees the list the insert
      // will apply to.
      final current = _roots.indexOf(node.key);
      if (current != -1) {
        _roots.removeAt(current);
        final fullIndex = _liveIndexToFullInsertIndex(_roots, index);
        _roots.insert(fullIndex, node.key);
      }
    }
    _cancelDeletion(
      node.key,
      animate: animate,
      preserveSubtreeState: preservePendingSubtreeState,
    );
    _adoptKey(node.key);
    _store.setData(node.key, node);
    // Fire the node-data channel so subscribers via
    // [addNodeDataListener] see the data update. The structural
    // notification below covers a different channel (full refresh)
    // that some callers don't subscribe to.
    _notifyNodeDataChanged(node.key);
    if (preservePendingSubtreeState) {
      _markVisibleOrderDirty();
      // Cancelling a pending deletion restores the node (and possibly
      // descendants) to the tree. Downstream builder-output effects span
      // the restored subtree plus any ancestor whose hasChildren state
      // flips; enumerating all of that precisely is complex, so fall back
      // to a full refresh.
      _notifyStructural();
      return;
    }
    // Reset expansion state so a subsequent expand() works cleanly.
    // Descendants that were mid-exit are left alone by _cancelDeletion
    // and continue animating out under the restored parent via
    // _rebuildVisibleOrder's "collapsed with active animations" branch.
    // Yanking them here would visually jump following rows upward by
    // the descendant's current extent in a single frame.
    _setExpandedKey(node.key, false);
    _markVisibleOrderDirty();
    _notifyStructural();
    return;
  }

  // Node is already present (e.g. restored by an ancestor's
  // _cancelDeletion, or a live re-insert). Update the data and, if the
  // caller requested a different location, relocate it to honor the
  // insertRoot(index:) contract instead of silently dropping the index.
  if (_hasKey(node.key)) {
    _adoptKey(node.key);
    _store.setData(node.key, node);
    final currentParent = _parentKeyOfKey(node.key);
    if (currentParent != null) {
      // Fire the node-data channel BEFORE delegating. moveNode's
      // structural notification is targeted, on a depth-preserving
      // move its affectedKeys omits the moved key itself, so this is
      // the only refresh path for the overwritten payload.
      _notifyNodeDataChanged(node.key);
      // Different parent: delegate to moveNode. Forward the caller's
      // `animate` so insertRoot(animate: false) doesn't silently slide
      // (now that moveNode itself defaults to animate: true).
      moveNode(node.key, null, index: index, animate: animate);
      return;
    }
    _relocateOrUpdateExistingSibling(null, node, index);
    return;
  }

  // Add to data structures
  _adoptKey(node.key);
  _store.setData(node.key, node);
  _setParentKey(node.key, null);
  _setChildList(node.key, []);
  _setDepthKey(node.key, 0);
  _setExpandedKey(node.key, false);

  // Add to roots list. Explicit index is live-space; the comparator
  // path already returns a full-space position.
  final effectiveIndex = index != null
      ? _liveIndexToFullInsertIndex(_roots, index)
      : (comparator != null ? _sortedIndex(_roots, node) : null);
  // Compute visible insert position BEFORE modifying _roots, since
  // _calculateRootInsertIndex reads _roots[effectiveIndex].
  final visibleInsertIndex =
      effectiveIndex != null && effectiveIndex < _roots.length
      ? _calculateRootInsertIndex(effectiveIndex)
      : _order.length;
  if (effectiveIndex != null && effectiveIndex < _roots.length) {
    _roots.insert(effectiveIndex, node.key);
  } else {
    _roots.add(node.key);
  }
  // Raw sibling-list writes above; invalidate cached live indices.
  _liveIndexCache.bump();

  // Add to visible order (root nodes are always visible)
  final insertIndex = visibleInsertIndex;
  _order.insertKey(insertIndex, node.key);
  _updateIndicesFrom(insertIndex);
  _structureGeneration++;

  if (animate) {
    _startStandaloneEnterAnimation(
      node.key,
      family: TreeAnimationFamily.enterExit,
    );
  }

  // The new key itself enters visible order via createChild, not a
  // refresh, and roots have no parent whose hasChildren could flip. The
  // other roots DO need one: the insert shifted their positions.
  _notifyStructural(affectedKeys: _siblingRefreshOrToken(null));
}