insertRoot method
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));
}