From 4b88d269e34cf05a41975f44a8589fcf5c89c7b4 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Tue, 9 Jun 2026 21:29:26 +0700 Subject: [PATCH] =?UTF-8?q?feat(app):=20LayoutEngine=20=E2=80=94=20recursi?= =?UTF-8?q?ve=20split=20render,=20splitter=20resize,=20stopped=20overlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- app/src/LayoutEngine.tsx | 90 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 app/src/LayoutEngine.tsx diff --git a/app/src/LayoutEngine.tsx b/app/src/LayoutEngine.tsx new file mode 100644 index 0000000..2ca517e --- /dev/null +++ b/app/src/LayoutEngine.tsx @@ -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; +} + +export function LayoutEngine({ workspaceId, layout, running }: Props) { + if (!layout) { + return
Empty workspace — apply a preset to add panels.
; + } + return ; +} + +function Node({ workspaceId, node, path, running }: { workspaceId: string; node: LayoutNode; path: number[]; running: Record }) { + if ("leaf" in node) { + const id = node.leaf.surface_id; + if (running[id] === false) { + return ( +
+
Process exited
+ +
+ ); + } + return ; + } + + const { orient, ratios, children } = node.split; + const dir = orient === "h" ? "row" : "column"; + return ( +
+ {children.map((child, i) => ( + { + 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); + }}> + + + ))} +
+ ); +} + +function Pane({ grow, isLast, orient, onResize, children }: { grow: number; isLast: boolean; orient: "h" | "v"; onResize: (deltaFrac: number) => void; children: React.ReactNode }) { + const ref = useRef(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 ( + <> +
+ {children} +
+ {!isLast && ( +
+ )} + + ); +}