Expand description
Phase E.4.B Phase 4 — pure layout-tree helpers (Rust port of layoutTree.ts). Pure layout-tree helper functions for the srv E.4.B reducer.
Phase 4 of docs/specs/srv-phase-e4b-formal-spec-2026-05-03.md.
Ports the 11 pure-ish action handlers from
frontend/layout/lib/layoutTree.ts to Rust. These functions take
&mut LayoutNode (the tree root) and operation params; they have no
I/O and no side effects. The reducer arms (Phase 5) will call these.
Test oracle: the 30 tests in frontend/layout/tests/layoutTree.test.ts
(shipped in PR #686) — the Rust implementations must produce identical
state transitions for identical inputs.
Structs§
- Insert
Candidate 🔒 - Candidate for insertion location, collected during tree traversal.
Enums§
Constants§
- DEFAULT_
MAX_ CHILDREN - Maximum children before
insert_node’s heuristic descends. Mirrorsfrontend/layout/lib/layoutTree.ts::DEFAULT_MAX_CHILDREN. - DEFAULT_
NODE_ SIZE - Default flex size for newly-inserted nodes.
Mirrors
frontend/layout/lib/types.ts::DefaultNodeSize.
Functions§
- activate_
stack_ member - Make
block_idthe visible tab of the pane whose stack holds it. Returns false when no pane’s stack holds it. SPEC_PANE_TABS_REDUCER_COMMANDS_2026_09_18.md §3.2 (LayoutStackActivate). - balance_
node - Port of
balanceNodeinfrontend/layout/lib/layoutNode.ts. Recursively normalizes the tree: validates each node, alternates child flex direction, hoists redundant single-child-branch chains, drops empty branches, and collapses a branch with a single leaf child into that leaf. - collect_
insert_ 🔒candidates - delete_
node - Remove the node with the given id from the tree.
Returns
Ok(())on success, orErr(NodeNotFound)ifnode_idis not in the tree. Callers are responsible for clearingfocused_node_idif needed (spec §7.1). Collapses single-child parents. - delete_
recursive 🔒 - Recursive delete. Returns
truewhen the node was found and removed,falsewhen not found. - enforce_
minimized_ locks - Write-point enforcement of the minimize lock (the invariant-at-the-write
companion to
prune_dangling_block_refs, perSPEC_LAYOUT_MINIMIZE_LOCKED_STATE_REDESIGN_2026_07_16.md): snap every locked node’ssizeback to its recordedminimizedLockedSize, returning the delta to the nearest unlocked sibling (next preferred, then previous) so the parent’s flex-unit budget is conserved. It no longer has a frontend twin: since minimize became a display mode (#2197) the frontend dropsminimizedLockedSizewhen it loads a tree (rebuildMinimizedSetinfrontend/layout/lib/layoutMinimize.ts), so only a tree that pass has not migrated yet carries a size to snap to. Returns the number of snapped nodes (0 = tree already honored the locks — the common case, cheap to call unconditionally on every write). - ensure_
group_ 🔒node - If
nodeis a leaf (hasdata), promote it to a group by wrapping its data in an intermediate child. Mirrors TypeScriptaddIntermediateNodeinfrontend/layout/lib/layoutTree.ts. - find_
leaf_ containing_ block - Find the (first) leaf that has
block_idas its visible block OR as any member of itsblock_stack. Unlike [find_node_id_by_block], which only matches the visible block, this finds the pane a background tab lives in. Returns the leaf node itself. - find_
next_ 🔒insert_ location - Find the best insertion location using the TypeScript scoring heuristic
(
Math.pow(depth, index + maxChildren)). Collects all candidates, then sorts by ascending score. MirrorsfindNextInsertLocationinfrontend/layout/lib/layoutNode.ts. - find_
node_ by_ id - Find a node by id (immutable).
- find_
node_ by_ id_ mut - Find a node by id (mutable).
- find_
parent_ 🔒id - first_
dangling_ 🔒leaf_ id - First leaf (depth-first) whose
data.block_idis set but not present inlive_block_ids.data.block_idis only ever non-empty on leaves — container/group nodes havedata: None(seeensure_group_node). - insert_
node - Insert
nodeusing the TypeScript scoring heuristic (up to DEFAULT_MAX_CHILDREN per node before descending). If the target location is a leaf, it is promoted to a group first viaensure_group_node. MirrorsinsertNode/addChildAtinfrontend/layout/lib/layoutTree.ts. - insert_
node_ at_ index - Insert
nodeat the location identified byindex_arr— an ordered sequence of child indices (e.g.[0, 2]= root.children[0].children at index 3). The node is inserted AFTER the position identified by the last index. - is_
effectively_ minimized - True when a node renders as minimized: a locked leaf, or a branch whose
every child is effectively minimized (a stacked chip strip). Mirrors
isEffectivelyMinimizedinfrontend/layout/lib/layoutInvariants.ts. Reducer guards use THIS (notis_node_locked): a fully-collapsed branch has no branch-level marker in the display-mode model, but its stored size becomes load-bearing again the moment a child restores — so it must be just as untargetable as a minimized leaf. - is_
node_ locked - A minimize-locked node: a minimized leaf (
minimizedSize), a slipped header (slipMinimize), or a dissolved column (columnDissolve). The fields are written by the frontend minimize subsystem and round-trip through the untypedextracatch-all on this side. MirrorsisNodeLockedinfrontend/layout/lib/layoutMinimize.ts. - leaf_
members - A leaf’s members: its
block_stack, or[block_id]for a leaf with no stack (mirrors the frontend’seffectiveStack,layoutStack.ts). - move_
node - Move the node identified by
node_idto be the child atindexunder the node identified bynew_parent_id. Resets the moved node’s size to DEFAULT_NODE_SIZE unless it stays under the same parent. - move_
stack_ member - Move
block_idtopositionrelative totarget_block_id: a same-leaf reorder when both are already members of one leaf’s stack, otherwise a cross-leaf move (removed from its source leaf viaremove_stack_member, placed intotarget_block_id’s leaf via a position-aware push). Returnsfalse, leaving the tree untouched, when either block doesn’t exist in the tree. - next_
visible_ member - Which tab becomes visible once the one at
removed_idxof the ORIGINAL stack is gone: the nearest eligible member to its right, else the nearest to its left.remainingis the stack with it removed;eligiblefilters (e.g. “still exists”). The one rule every removal path uses — frontendremoveMemberFromStackmirrors it (frontend/layout/lib/stackMembers.ts). SPEC_PANE_TABS_REDUCER_COMMANDS_2026_09_18.md §3.6. - prune_
dangling_ block_ refs - Removes every leaf whose
data.block_idis not inlive_block_ids, using the same collapse semantics as an explicit user delete (delete_node’s single-child-parent promotion). Returns the number of leaves pruned (0 = tree was already clean — the common case, so this is cheap to call unconditionally on every write). - prune_
dangling_ 🔒stack_ members - Recursively drop dead entries from every leaf’s
block_stack. The active member (data.block_id) is never removed here — by the time this runs, the caller’s whole-leaf pruning loop has already guaranteed it’s live. Returns the number of stack entries removed. - push_
stack_ member - Add
block_idto the tab stack of the pane holdingtarget_block_id(as its visible tab or a background member). A pane with no stack yet becomes a two-member stack. Already a member → only (re)activated if asked. Returns false when no pane holdstarget_block_id. SPEC_PANE_TABS_REDUCER_COMMANDS_2026_09_18.md §3.2 (LayoutStackPush). - push_
stack_ 🔒member_ at - Position-aware variant of
push_stack_member: addsblock_idto the stack of the leaf holdingtarget_block_id, atpositionrelative totarget_block_idinstead of always appending. Promotes a single-block leaf (emptyblock_stack) into a real stack first, exactly likepush_stack_memberdoes. - reactivate_
dangling_ 🔒active_ stack_ members - Recursively repair leaves whose ACTIVE block (
data.block_id) is dangling but a live sibling still exists inblock_stack: drop the dead entry from the stack and reactivate the first remaining live member (block_id/active_block_idboth updated). Leaves with no live sibling are left withblock_idstill pointing at the dangling id — the caller’s whole-leaf pass then correctly deletes them, same as before this function existed. Returns the number of leaves repaired. - remove_
node_ 🔒from_ parent - remove_
stack_ member - Remove
block_idfrom the stack of the leaf holding it, WITHOUT removing the leaf, when that leaf has other members. If it was the visible tab, the right-hand neighbour becomes visible (the new last member when it was rightmost) — the same rule as the frontend’sremoveMemberFromStack(frontend/layout/lib/stackMembers.ts). Returns false and changes nothing whenblock_idis in no leaf or is its leaf’s only member (removing it means removing the leaf — the caller’s job). SPEC_PANE_TABS_REDUCER_COMMANDS_2026_09_18.md §3.4. - reorder_
within_ 🔒leaf - Splice
block_idtopositionrelative totarget_block_idwithin the SAME leaf’s stack. Both must already be members of that leaf (checked by the caller,move_stack_member). Only touchesblock_id/active_block_idwhenactivateis set — a reorder alone never changes which member is visible. - replace_
node - Replace the node at
target_idwithnew_node, preserving the target’ssize. If target is root, replaces root-in-place (caller’sOption<LayoutNode>doesn’t change reference — the fields are updated on the root node). - resize_
nodes - Apply resize operations. Validates all sizes first (0.0–100.0 range); if any is invalid, returns an error WITHOUT applying any ops (atomically rejected — matches the frontend’s early-return semantic from PR #686).
- reverse_
flex_ 🔒direction - Reverse a flex direction (Row ↔ Column).
Mirrors
frontend/layout/lib/layoutNode.ts::reverseFlexDirection. - source_
leaf_ emptied_ by_ move - The node id of
block_id’s leaf, whenmove_stack_member(tree, block_id, target_block_id, ..)would delete that leaf:block_idis its only member andtarget_block_idlives in a different leaf.Nonefor every other move (reorder, multi-member source, missing blocks, self-target). - split_
horizontal - Horizontal split: insert
new_nodebefore/aftertarget_id. - split_
impl 🔒 - split_
vertical - Vertical split: insert
new_nodebefore/aftertarget_id. - swap_
nodes - Swap two nodes (by id). Their sizes travel with them (swap positions but preserve the size each node had). Neither can be the root.
- validate_
layout_ invariants - Layout doctor — structural invariant validation, mirroring
validateLayoutInvariantsinfrontend/layout/lib/layoutInvariants.ts(keep the check lists in sync). Returns human-readable violation strings; empty = healthy. Never mutates. Reducer choke points call this after prune/enforce andtracing::error!any findings, so a corrupted tree is attributed to the write that produced it instead of being reconstructed later from db_layout archaeology (issue #2179). - validate_
node 🔒 - Validate the leaf-XOR-branch invariant. Mirrors
validateNodeinfrontend/layout/lib/layoutNode.ts: a node must have eitherdataorchildren, never both and never neither. (In TS the “empty children array” case is a separate check; here an emptychildrenvec with nodatais the same “neither” case and is rejected by the single XOR below.)