Skip to content

Commit 1b07411

Browse files
Merge pull request #1466 from corbitsdev/cl-10650-tui-adopt-native-layout-primitives
refactor(tui): read zone rects from Yoga instead of stacking them
2 parents ddfdbc3 + 98208f2 commit 1b07411

2 files changed

Lines changed: 87 additions & 21 deletions

File tree

‎src/tui/geometry/resolve.ts‎

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
// Pure: terminal size + zone visibility + overlay mode → rects. No I/O.
22

3-
import { resolveContentWidth, resolveSideMargin } from "./zones.js";
3+
import {
4+
Align,
5+
Direction,
6+
Display,
7+
Edge,
8+
FlexDirection,
9+
Justify,
10+
Node,
11+
PositionType,
12+
} from "@opentui/core/yoga";
413
import {
514
COLLAPSE_ORDER,
615
FLEET_BOARD_CAP_FRACTION,
@@ -13,6 +22,8 @@ import {
1322
PROMPT_CAP_FRACTION,
1423
PROMPT_IDLE_ROWS,
1524
ZONE_REGISTRY,
25+
resolveContentWidth,
26+
resolveSideMargin,
1627
type ZoneId,
1728
} from "./zones.js";
1829

@@ -300,26 +311,80 @@ function collapseOnce(
300311
return null;
301312
}
302313

303-
function assignRects(
314+
function roundLayout(n: number): number {
315+
return Math.round(n);
316+
}
317+
318+
/**
319+
* Place already-collapsed zone heights with Yoga. The ordered collapse pass
320+
* stays in TypeScript: unequal `flexShrink` weights (the usual priority
321+
* trick) make this Yoga build stop shrinking and overflow the column, and
322+
* equal weights shrink zones together instead of in `COLLAPSE_ORDER`.
323+
* Gutters are root padding; each zone is a column item with its height
324+
* locked (basis, min, and max) so the native layout is the rect source.
325+
*/
326+
function layoutRects(
304327
heights: MutableHeights,
305328
terminal: TerminalSize,
306329
): Partial<Record<ZoneId, Rect>> {
307-
const regions: Partial<Record<ZoneId, Rect>> = {};
308-
const x = resolveSideMargin(terminal.columns);
309-
const contentWidth = resolveContentWidth(terminal.columns);
310-
let y = 0;
311-
for (const id of PAINT_ORDER) {
312-
const height = heights[id];
313-
if (height <= 0) continue;
314-
regions[id] = {
315-
x,
316-
y,
317-
width: contentWidth,
318-
height,
319-
};
320-
y += height;
330+
const sideMargin = resolveSideMargin(terminal.columns);
331+
const root = Node.create();
332+
try {
333+
root.setWidth(terminal.columns);
334+
root.setHeight(terminal.rows);
335+
root.setFlexDirection(FlexDirection.Column);
336+
root.setJustifyContent(Justify.FlexStart);
337+
root.setAlignItems(Align.Stretch);
338+
root.setDisplay(Display.Flex);
339+
root.setPositionType(PositionType.Relative);
340+
root.setPadding(Edge.Left, sideMargin);
341+
root.setPadding(Edge.Right, sideMargin);
342+
root.setPadding(Edge.Top, 0);
343+
root.setPadding(Edge.Bottom, 0);
344+
345+
const placed: ZoneId[] = [];
346+
for (const id of PAINT_ORDER) {
347+
const height = heights[id];
348+
if (height <= 0) continue;
349+
const child = Node.create();
350+
child.setDisplay(Display.Flex);
351+
child.setPositionType(PositionType.Relative);
352+
child.setFlexDirection(FlexDirection.Column);
353+
child.setFlexGrow(0);
354+
child.setFlexShrink(0);
355+
child.setFlexBasis(height);
356+
child.setHeight(height);
357+
child.setMinHeight(height);
358+
child.setMaxHeight(height);
359+
child.setWidthPercent(100);
360+
child.setMargin(Edge.Left, 0);
361+
child.setMargin(Edge.Right, 0);
362+
child.setMargin(Edge.Top, 0);
363+
child.setMargin(Edge.Bottom, 0);
364+
root.insertChild(child, root.getChildCount());
365+
placed.push(id);
366+
}
367+
368+
root.calculateLayout(terminal.columns, terminal.rows, Direction.LTR);
369+
370+
const regions: Partial<Record<ZoneId, Rect>> = {};
371+
for (let index = 0; index < placed.length; index += 1) {
372+
const id = placed[index];
373+
if (id === undefined) continue;
374+
const child = root.getChild(index);
375+
if (child === null) continue;
376+
const layout = child.getComputedLayout();
377+
regions[id] = {
378+
x: roundLayout(layout.left),
379+
y: roundLayout(layout.top),
380+
width: roundLayout(layout.width),
381+
height: roundLayout(layout.height),
382+
};
383+
}
384+
return regions;
385+
} finally {
386+
root.freeRecursive();
321387
}
322-
return regions;
323388
}
324389

325390
/** Resolve shell region rects from terminal size, optional chrome, and
@@ -426,7 +491,7 @@ export function resolveGeometry(input: GeometryInput): GeometryLayout {
426491
heights.transcript += terminal.rows - assigned;
427492
}
428493

429-
const regions = assignRects(heights, terminal);
494+
const regions = layoutRects(heights, terminal);
430495

431496
return {
432497
terminal,

‎src/tui/geometry/zones.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// Chrome zone registry for the OpenTUI shell.
2-
// Source of truth: docs/TUI.md "How it should look" (zone table + collapse order).
3-
// Pure data — no process.stdout, no paint framework.
1+
// Normative geometry constants for the OpenTUI shell.
2+
// Source of truth: docs/TUI.md "How it should look" (gutter, pads, floors,
3+
// collapse order). Rects are read back from Yoga in geometry/resolve.ts.
4+
// No process.stdout, no paint framework.
45

56
/** Constitution zone ids (snake_case matches the registry table). */
67
const ZONE_IDS = [

0 commit comments

Comments
 (0)