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
42
src/App.tsx
42
src/App.tsx
|
|
@ -23,6 +23,8 @@ import {
|
|||
toggleBroadcast as toggleBroadcastInTree,
|
||||
setAllBroadcast,
|
||||
reshapeToPreset,
|
||||
flattenLayout,
|
||||
updateSplitRatio,
|
||||
serialize,
|
||||
deserialize,
|
||||
presetSingle,
|
||||
|
|
@ -32,10 +34,12 @@ import {
|
|||
presetTwoByTwo,
|
||||
} from "./lib/layout/tree";
|
||||
import { OrchestrationProvider, type Orchestration } from "./lib/layout/orchestration";
|
||||
import Pane from "./lib/layout/Pane";
|
||||
import LeafPane from "./lib/layout/LeafPane";
|
||||
import Gutter from "./lib/layout/Gutter";
|
||||
import Notifications, { type Toast } from "./components/Notifications";
|
||||
import Palette from "./components/Palette";
|
||||
import "./App.css";
|
||||
import "./lib/layout/Gutter.css";
|
||||
|
||||
const LEGACY_STORAGE_KEY = "tiletopia.tree.v1";
|
||||
const SAVE_DEBOUNCE_MS = 500;
|
||||
|
|
@ -299,6 +303,16 @@ export default function App() {
|
|||
[paletteOpen, tree],
|
||||
);
|
||||
|
||||
// ---- flat layout — leaves as siblings keyed by id; gutters separate -----
|
||||
// This lets React preserve LeafPane (and its PTY) across any tree reshape
|
||||
// — split, close, preset application, etc. The tree changes, the boxes
|
||||
// change, the leaves re-position but DON'T unmount.
|
||||
const layout = useMemo(() => flattenLayout(tree), [tree]);
|
||||
const paneWrapRef = useRef<HTMLDivElement>(null);
|
||||
const onGutterRatio = useCallback((splitId: NodeId, ratio: number) => {
|
||||
setTree((t) => updateSplitRatio(t, splitId, ratio));
|
||||
}, []);
|
||||
|
||||
// ---- global broadcast state (derived from tree) -------------------------
|
||||
const broadcastStats = useMemo(() => {
|
||||
let on = 0;
|
||||
|
|
@ -395,10 +409,32 @@ export default function App() {
|
|||
</span>
|
||||
</header>
|
||||
|
||||
<div className="pane-wrap">
|
||||
<div className="pane-wrap" ref={paneWrapRef}>
|
||||
{ready && (
|
||||
<OrchestrationProvider value={orch}>
|
||||
<Pane node={tree} />
|
||||
{layout.leaves.map(({ leaf, box }) => (
|
||||
<div
|
||||
key={leaf.id}
|
||||
className="leaf-slot"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: `${box.top * 100}%`,
|
||||
left: `${box.left * 100}%`,
|
||||
width: `${box.width * 100}%`,
|
||||
height: `${box.height * 100}%`,
|
||||
}}
|
||||
>
|
||||
<LeafPane leaf={leaf} />
|
||||
</div>
|
||||
))}
|
||||
{layout.gutters.map((g) => (
|
||||
<Gutter
|
||||
key={g.splitId}
|
||||
info={g}
|
||||
containerRef={paneWrapRef}
|
||||
onRatioChange={onGutterRatio}
|
||||
/>
|
||||
))}
|
||||
</OrchestrationProvider>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue