insertRoot method
void
insertRoot(})
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.
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;
}
final currentRootIndex = _roots.indexOf(node.key);
// Explicit index is live-space (compare live-vs-live and convert at
// the insert); the comparator path stays full-space end to end.
final int? sortedDesired = index == null && comparator != null
? _sortedIndex(_roots, node)
: null;
final bool wantsRelocate;
if (index != null) {
final currentLiveIndex = getIndexInParent(node.key);
final liveCount = _liveCountOf(_roots);
wantsRelocate =
index != currentLiveIndex &&
// Appending is a no-op if already live-last.
!(currentLiveIndex == liveCount - 1 && index >= liveCount);
} else if (sortedDesired != null) {
wantsRelocate =
sortedDesired != currentRootIndex &&
// Appending is a no-op if already at the end.
!(currentRootIndex == _roots.length - 1 &&
sortedDesired >= _roots.length);
} else {
wantsRelocate = false;
}
if (wantsRelocate) {
_roots.removeAt(currentRootIndex);
final insertAt = index != null
? _liveIndexToFullInsertIndex(_roots, index)
: sortedDesired!.clamp(0, _roots.length);
_roots.insert(insertAt, node.key);
// Raw sibling-list writes above; invalidate cached live indices.
_liveIndexCache.bump();
_markVisibleOrderDirty();
// Relocation changes row positions (and the payload was
// overwritten), structural refresh, which subsumes the data
// channel's row refresh, so the data channel does not also fire.
_notifyStructural(affectedKeys: <TKey>{node.key});
} else {
// Data-only update: fire the node-data channel only, matching
// updateNode's contract. Firing a structural notification too
// would refresh the same row twice.
_notifyNodeDataChanged(node.key);
}
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);
}
// 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: _siblingRefreshSet(null));
}