Flat-list layout: render leaves as siblings keyed by id
The fix for the real preset bug: previously, presetSingle/2H/3H/2V/2×2
appeared to preserve panes (we copied id/distro/cwd/label/broadcast
into the preset's slots), but React's reconciliation tore down every
LeafPane and re-mounted it because the tree structure changed —
killing all PTYs and spawning fresh shells. The "preservation" was
data-only; the React components didn't survive.
Solution: stop rendering the Pane → SplitNode → LeafPane recursion.
Walk the tree to produce a FLAT layout of `{leaf, box}` entries (each
box is top/left/width/height as fractions 0–1). Render all leaves as
siblings of a relative-positioned container, each absolutely
positioned by its box. Key each one by leaf.id — React preserves the
component (and its XtermPane → PTY) across any tree reshape; only the
inline style changes.
Gutters render as separate sibling overlays at the split boundaries,
each with its own pointer handlers. Dragging mutates the split's
ratio via `updateSplitRatio(tree, splitId, r)`; the layout
recomputes; leaf boxes change; nothing remounts.
Now: clicking 2×2 on 4 stacked panes keeps all 4 shells alive and
just rearranges them into the grid. Same for any preset that doesn't
overflow.
Side benefit: removed the recursive Pane.tsx + SplitNode.tsx + their
CSS. The render path is now straightforward, no recursion, easier to
reason about.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8c3af8f9ee
commit
c4747546e0
7 changed files with 272 additions and 134 deletions
|
|
@ -253,6 +253,109 @@ export function setAllBroadcast(root: TreeNode, on: boolean): TreeNode {
|
|||
return { ...root, a, b };
|
||||
}
|
||||
|
||||
// --- flat layout (for absolute-positioned rendering) ------------------------
|
||||
|
||||
/** Normalised bounding box: top/left/width/height as fractions [0, 1]. */
|
||||
export interface Box {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** A leaf rendered as a flat sibling: its current LeafNode plus the box
|
||||
* it occupies in the container. */
|
||||
export interface LeafSlot {
|
||||
leaf: LeafNode;
|
||||
box: Box;
|
||||
}
|
||||
|
||||
/** A draggable gutter at a split boundary. `box` is where to render the
|
||||
* draggable strip; `parentBox` is the area the gutter divides (needed to
|
||||
* convert pointer position → ratio). */
|
||||
export interface GutterInfo {
|
||||
splitId: NodeId;
|
||||
orientation: Orientation;
|
||||
ratio: number;
|
||||
box: Box;
|
||||
parentBox: Box;
|
||||
}
|
||||
|
||||
/** Walk the tree and produce a flat list of leaf slots + draggable gutters.
|
||||
* Renderer uses these to position all leaves as siblings in the DOM, which
|
||||
* lets React preserve component instances (and thus PTYs) across any tree
|
||||
* reshape — splits, closes, presets, etc. */
|
||||
export function flattenLayout(
|
||||
root: TreeNode,
|
||||
box: Box = { top: 0, left: 0, width: 1, height: 1 },
|
||||
): { leaves: LeafSlot[]; gutters: GutterInfo[] } {
|
||||
if (root.kind === "leaf") {
|
||||
return { leaves: [{ leaf: root, box }], gutters: [] };
|
||||
}
|
||||
const isH = root.orientation === "h";
|
||||
const r = root.ratio;
|
||||
let boxA: Box;
|
||||
let boxB: Box;
|
||||
let gutter: GutterInfo;
|
||||
if (isH) {
|
||||
const splitPos = box.width * r;
|
||||
boxA = { top: box.top, left: box.left, width: splitPos, height: box.height };
|
||||
boxB = {
|
||||
top: box.top,
|
||||
left: box.left + splitPos,
|
||||
width: box.width - splitPos,
|
||||
height: box.height,
|
||||
};
|
||||
gutter = {
|
||||
splitId: root.id,
|
||||
orientation: "h",
|
||||
ratio: r,
|
||||
box: {
|
||||
top: box.top,
|
||||
left: box.left + splitPos,
|
||||
width: 0,
|
||||
height: box.height,
|
||||
},
|
||||
parentBox: box,
|
||||
};
|
||||
} else {
|
||||
const splitPos = box.height * r;
|
||||
boxA = { top: box.top, left: box.left, width: box.width, height: splitPos };
|
||||
boxB = {
|
||||
top: box.top + splitPos,
|
||||
left: box.left,
|
||||
width: box.width,
|
||||
height: box.height - splitPos,
|
||||
};
|
||||
gutter = {
|
||||
splitId: root.id,
|
||||
orientation: "v",
|
||||
ratio: r,
|
||||
box: {
|
||||
top: box.top + splitPos,
|
||||
left: box.left,
|
||||
width: box.width,
|
||||
height: 0,
|
||||
},
|
||||
parentBox: box,
|
||||
};
|
||||
}
|
||||
const a = flattenLayout(root.a, boxA);
|
||||
const b = flattenLayout(root.b, boxB);
|
||||
return {
|
||||
leaves: [...a.leaves, ...b.leaves],
|
||||
gutters: [gutter, ...a.gutters, ...b.gutters],
|
||||
};
|
||||
}
|
||||
|
||||
/** Update a split's ratio by its id. */
|
||||
export function updateSplitRatio(root: TreeNode, splitId: NodeId, ratio: number): TreeNode {
|
||||
return replaceById(root, splitId, (node) => {
|
||||
if (node.kind !== "split") return node;
|
||||
return { ...node, ratio };
|
||||
});
|
||||
}
|
||||
|
||||
export function serialize(root: TreeNode): string {
|
||||
return JSON.stringify(root);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue