feat(app): LayoutEngine — recursive split render, splitter resize, stopped overlay
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { useRef } from "react";
|
||||
import { TerminalView } from "./TerminalView";
|
||||
import type { LayoutNode } from "./layoutTypes";
|
||||
import { setRatios, restartSurface } from "./socketBridge";
|
||||
|
||||
interface Props {
|
||||
workspaceId: string;
|
||||
layout: LayoutNode | null;
|
||||
/** surface_id -> running flag, from the latest status/events. */
|
||||
running: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export function LayoutEngine({ workspaceId, layout, running }: Props) {
|
||||
if (!layout) {
|
||||
return <div style={{ color: "#666", padding: 24 }}>Empty workspace — apply a preset to add panels.</div>;
|
||||
}
|
||||
return <Node workspaceId={workspaceId} node={layout} path={[]} running={running} />;
|
||||
}
|
||||
|
||||
function Node({ workspaceId, node, path, running }: { workspaceId: string; node: LayoutNode; path: number[]; running: Record<string, boolean> }) {
|
||||
if ("leaf" in node) {
|
||||
const id = node.leaf.surface_id;
|
||||
if (running[id] === false) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", width: "100%", background: "#0A0D12", color: "#8B97A6", flexDirection: "column", gap: 10 }}>
|
||||
<div style={{ fontFamily: "monospace", fontSize: 13 }}>Process exited</div>
|
||||
<button onClick={() => void restartSurface(id)} style={{ padding: "6px 14px" }}>⏎ Restart</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <TerminalView key={id} surfaceId={id} />;
|
||||
}
|
||||
|
||||
const { orient, ratios, children } = node.split;
|
||||
const dir = orient === "h" ? "row" : "column";
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: dir, width: "100%", height: "100%" }}>
|
||||
{children.map((child, i) => (
|
||||
<Pane key={i} grow={ratios[i] ?? 1} isLast={i === children.length - 1} orient={orient}
|
||||
onResize={(deltaFrac) => {
|
||||
const next = [...ratios];
|
||||
next[i] = Math.max(0.05, next[i] + deltaFrac);
|
||||
next[i + 1] = Math.max(0.05, (next[i + 1] ?? 1) - deltaFrac);
|
||||
void setRatios(workspaceId, path, next);
|
||||
}}>
|
||||
<Node workspaceId={workspaceId} node={child} path={[...path, i]} running={running} />
|
||||
</Pane>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Pane({ grow, isLast, orient, onResize, children }: { grow: number; isLast: boolean; orient: "h" | "v"; onResize: (deltaFrac: number) => void; children: React.ReactNode }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const startDrag = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const parent = ref.current?.parentElement;
|
||||
if (!parent) return;
|
||||
const total = orient === "h" ? parent.clientWidth : parent.clientHeight;
|
||||
const start = orient === "h" ? e.clientX : e.clientY;
|
||||
let last = start;
|
||||
const move = (ev: MouseEvent) => {
|
||||
const cur = orient === "h" ? ev.clientX : ev.clientY;
|
||||
const delta = (cur - last) / total;
|
||||
last = cur;
|
||||
onResize(delta);
|
||||
};
|
||||
const up = () => {
|
||||
window.removeEventListener("mousemove", move);
|
||||
window.removeEventListener("mouseup", up);
|
||||
};
|
||||
window.addEventListener("mousemove", move);
|
||||
window.addEventListener("mouseup", up);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div ref={ref} style={{ flexGrow: grow, flexBasis: 0, minWidth: 0, minHeight: 0, overflow: "hidden", position: "relative" }}>
|
||||
{children}
|
||||
</div>
|
||||
{!isLast && (
|
||||
<div onMouseDown={startDrag}
|
||||
style={{
|
||||
flex: "0 0 4px",
|
||||
cursor: orient === "h" ? "col-resize" : "row-resize",
|
||||
background: "#232A33",
|
||||
}} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user