Skip to content

Commit 33ae3fe

Browse files
Merge pull request #39 from RtlZeroMemory/feat/runtime-polish-foundation-pass
feat: runtime polish pass across core/node/native layers
2 parents 4ca19db + a37a32c commit 33ae3fe

29 files changed

Lines changed: 1498 additions & 50 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,32 @@ jobs:
7777
- name: Build native addon
7878
run: npm run build:native
7979

80-
- name: Terminal e2e
80+
- name: Terminal e2e (Linux full)
8181
if: runner.os == 'Linux'
8282
run: npm run test:e2e
8383

84-
- name: Native smoke + worker integration
84+
- name: Terminal e2e equivalent (non-Linux reduced)
85+
if: runner.os != 'Linux'
86+
run: npm run test:e2e:reduced
87+
88+
- name: Native smoke + worker integration (Unix PTY)
89+
if: runner.os != 'Windows'
90+
run: |
91+
python3 - <<'PY'
92+
import os
93+
import pty
94+
import sys
95+
96+
status = pty.spawn(["bash", "-lc", "npm run test:native:smoke"])
97+
if os.WIFEXITED(status):
98+
sys.exit(os.WEXITSTATUS(status))
99+
if os.WIFSIGNALED(status):
100+
sys.exit(128 + os.WTERMSIG(status))
101+
sys.exit(1)
102+
PY
103+
104+
- name: Native smoke + worker integration (Windows)
105+
if: runner.os == 'Windows'
85106
run: npm run test:native:smoke
86107

87108
docs:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"check": "npm run check:core-portability && npm run check:docs && npm run check:unicode",
2929
"test": "node scripts/run-tests.mjs",
3030
"test:e2e": "node scripts/run-e2e.mjs",
31+
"test:e2e:reduced": "node scripts/run-e2e.mjs --profile reduced",
3132
"test:scripts": "node scripts/run-tests.mjs --scope scripts",
3233
"test:packages": "node scripts/run-tests.mjs --scope packages",
3334
"bench": "node --expose-gc packages/bench/dist/run.js",

packages/core/src/app/__tests__/widgetRenderer.integration.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,47 @@ describe("WidgetRenderer integration battery", () => {
278278
assert.deepEqual(events, ["enter:zone-1", "exit:zone-1", "enter:zone-2"]);
279279
});
280280

281+
test("focusZone restores latest focused widget when leaving and returning in same cycle", () => {
282+
const backend = createNoopBackend();
283+
const renderer = new WidgetRenderer<void>({
284+
backend,
285+
requestRender: () => {},
286+
});
287+
288+
const vnode = ui.column({}, [
289+
ui.focusZone({ id: "zone-1", navigation: "linear", wrapAround: true }, [
290+
ui.button({ id: "a", label: "A" }),
291+
ui.button({ id: "b", label: "B" }),
292+
]),
293+
ui.focusZone({ id: "zone-2", navigation: "linear", wrapAround: true }, [
294+
ui.button({ id: "c", label: "C" }),
295+
ui.button({ id: "d", label: "D" }),
296+
]),
297+
]);
298+
299+
const res = renderer.submitFrame(
300+
() => vnode,
301+
undefined,
302+
{ cols: 40, rows: 10 },
303+
defaultTheme,
304+
noRenderHooks(),
305+
);
306+
assert.ok(res.ok);
307+
308+
// Same interaction cycle: move within zone-1, leave to zone-2, then return via TAB wrap.
309+
renderer.routeEngineEvent(keyEvent(3 /* TAB */));
310+
assert.equal(renderer.getFocusedId(), "a");
311+
312+
renderer.routeEngineEvent(keyEvent(21 /* DOWN */));
313+
assert.equal(renderer.getFocusedId(), "b");
314+
315+
renderer.routeEngineEvent(keyEvent(3 /* TAB */));
316+
assert.equal(renderer.getFocusedId(), "c");
317+
318+
renderer.routeEngineEvent(keyEvent(3 /* TAB */));
319+
assert.equal(renderer.getFocusedId(), "b");
320+
});
321+
281322
test("focusZone onEnter/onExit swallow exceptions", () => {
282323
const backend = createNoopBackend();
283324
const renderer = new WidgetRenderer<void>({

packages/core/src/app/widgetRenderer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,6 +2105,7 @@ export class WidgetRenderer<S> {
21052105
activeZoneId: this.focusState.activeZoneId,
21062106
focusList: this.focusList,
21072107
zones: this.focusState.zones,
2108+
lastFocusedByZone: this.focusState.lastFocusedByZone,
21082109
traps: this.traps,
21092110
trapStack: this.focusState.trapStack,
21102111
enabledById,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { assert, describe, test } from "@rezi-ui/testkit";
2+
import { createDrawlistBuilderV1, createDrawlistBuilderV2 } from "../../index.js";
3+
4+
function u32(bytes: Uint8Array, off: number): number {
5+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
6+
return dv.getUint32(off, true);
7+
}
8+
9+
function textRunAttrs(bytes: Uint8Array, segmentIndex: number): number {
10+
const blobsBytesOffset = u32(bytes, 52);
11+
return u32(bytes, blobsBytesOffset + 4 + segmentIndex * 28 + 8);
12+
}
13+
14+
function firstCommandOffset(bytes: Uint8Array): number {
15+
return u32(bytes, 16);
16+
}
17+
18+
function drawTextAttrs(bytes: Uint8Array): number {
19+
return u32(bytes, firstCommandOffset(bytes) + 36);
20+
}
21+
22+
describe("drawlist style attrs encode dim", () => {
23+
test("v1 drawText attrs include dim without shifting existing bits", () => {
24+
const b = createDrawlistBuilderV1();
25+
b.drawText(0, 0, "dim", { dim: true });
26+
const res = b.build();
27+
assert.equal(res.ok, true);
28+
if (!res.ok) return;
29+
30+
assert.equal(drawTextAttrs(res.bytes), 1 << 4);
31+
});
32+
33+
test("v2 drawText attrs include dim without shifting existing bits", () => {
34+
const b = createDrawlistBuilderV2();
35+
b.drawText(0, 0, "dim", { dim: true });
36+
const res = b.build();
37+
assert.equal(res.ok, true);
38+
if (!res.ok) return;
39+
40+
assert.equal(drawTextAttrs(res.bytes), 1 << 4);
41+
});
42+
43+
test("v1 text-run attrs include dim without shifting existing bits", () => {
44+
const b = createDrawlistBuilderV1();
45+
const blobIndex = b.addTextRunBlob([
46+
{ text: "dim", style: { dim: true } },
47+
{ text: "base", style: { bold: true, italic: true, underline: true, inverse: true } },
48+
]);
49+
assert.equal(blobIndex, 0);
50+
if (blobIndex === null) return;
51+
52+
b.drawTextRun(0, 0, blobIndex);
53+
const res = b.build();
54+
assert.equal(res.ok, true);
55+
if (!res.ok) return;
56+
57+
assert.equal(textRunAttrs(res.bytes, 0), 1 << 4);
58+
assert.equal(textRunAttrs(res.bytes, 1), (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3));
59+
});
60+
61+
test("v2 text-run attrs include dim without shifting existing bits", () => {
62+
const b = createDrawlistBuilderV2();
63+
const blobIndex = b.addTextRunBlob([
64+
{ text: "dim", style: { dim: true } },
65+
{ text: "base", style: { bold: true, italic: true, underline: true, inverse: true } },
66+
]);
67+
assert.equal(blobIndex, 0);
68+
if (blobIndex === null) return;
69+
70+
b.drawTextRun(0, 0, blobIndex);
71+
const res = b.build();
72+
assert.equal(res.ok, true);
73+
if (!res.ok) return;
74+
75+
assert.equal(textRunAttrs(res.bytes, 0), 1 << 4);
76+
assert.equal(textRunAttrs(res.bytes, 1), (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3));
77+
});
78+
});

packages/core/src/drawlist/builder_v1.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ function packRgb(v: unknown): number {
141141

142142
/**
143143
* Encode TextStyle to binary format: packed fg/bg colors and attribute flags.
144-
* Attribute bits: 0=bold, 1=italic, 2=underline, 3=inverse
144+
* Attribute bits: 0=bold, 1=italic, 2=underline, 3=inverse, 4=dim
145145
*/
146146
function encodeStyle(style: TextStyle | undefined): EncodedStyle {
147147
if (!style) return { fg: 0, bg: 0, attrs: 0 };
@@ -154,6 +154,7 @@ function encodeStyle(style: TextStyle | undefined): EncodedStyle {
154154
if (style.italic) attrs |= 1 << 1;
155155
if (style.underline) attrs |= 1 << 2;
156156
if (style.inverse) attrs |= 1 << 3;
157+
if (style.dim) attrs |= 1 << 4;
157158

158159
return { fg, bg, attrs };
159160
}

packages/core/src/drawlist/builder_v2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ function encodeStyle(style: TextStyle | undefined): EncodedStyle {
130130
if (style.italic) attrs |= 1 << 1;
131131
if (style.underline) attrs |= 1 << 2;
132132
if (style.inverse) attrs |= 1 << 3;
133+
if (style.dim) attrs |= 1 << 4;
133134

134135
return { fg, bg, attrs };
135136
}

packages/core/src/layout/__tests__/constraints.golden.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,34 @@ describe("constraints (deterministic) - golden cases", () => {
6767
assert.equal(out.children[0]?.children[0]?.rect.h, 10);
6868
});
6969

70+
test("nested row with capped flex children does not starve sibling width", () => {
71+
const tree = ui.row({}, [
72+
ui.row({}, [
73+
ui.box({ border: "none", flex: 1, maxWidth: 3 }, []),
74+
ui.box({ border: "none", flex: 1, maxWidth: 3 }, []),
75+
]),
76+
ui.box({ border: "none" }, [ui.text("z")]),
77+
]);
78+
79+
const out = mustLayout(tree, 20, 5);
80+
assert.equal(out.children[0]?.rect.w, 6);
81+
assert.equal(out.children[1]?.rect.w, 1);
82+
});
83+
84+
test("nested column with capped flex children does not starve sibling height", () => {
85+
const tree = ui.column({}, [
86+
ui.column({}, [
87+
ui.box({ border: "none", flex: 1, maxHeight: 2 }, []),
88+
ui.box({ border: "none", flex: 1, maxHeight: 2 }, []),
89+
]),
90+
ui.box({ border: "none" }, [ui.text("y")]),
91+
]);
92+
93+
const out = mustLayout(tree, 10, 10);
94+
assert.equal(out.children[0]?.rect.h, 4);
95+
assert.equal(out.children[1]?.rect.h, 1);
96+
});
97+
7098
test("row child margin reserves outer space and offsets child rect", () => {
7199
const tree = ui.row({}, [
72100
ui.box({ border: "none", width: 4, height: 2, m: 1 }, [ui.text("x")]),

packages/core/src/layout/kinds/stack.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,21 @@ export function measureStackKinds(
7474
const forcedW = self.width === null ? null : clampWithin(self.width, minW, maxWCap);
7575
const forcedH = self.height === null ? null : clampWithin(self.height, minH, maxHCap);
7676

77-
const needsConstraintPass = vnode.children.some(
78-
(c) => childHasFlexInMainAxis(c, "row") || childHasPercentInMainAxis(c, "row"),
79-
);
80-
const fillMain = forcedW === null && needsConstraintPass;
77+
const hasFlexInMainAxis = vnode.children.some((c) => childHasFlexInMainAxis(c, "row"));
78+
const hasPercentInMainAxis = vnode.children.some((c) => childHasPercentInMainAxis(c, "row"));
79+
const needsConstraintPass = hasFlexInMainAxis || hasPercentInMainAxis;
80+
const fillMain = forcedW === null && hasPercentInMainAxis;
8181
const fillCross =
8282
forcedH === null && vnode.children.some((c) => childHasPercentInCrossAxis(c, "row"));
8383

84-
const outerWLimit = forcedW ?? (fillMain ? maxWCap : maxWCap);
84+
const outerWLimit = forcedW ?? maxWCap;
8585
const outerHLimit = forcedH ?? maxHCap;
8686

8787
const cw = clampNonNegative(outerWLimit - padX);
8888
const chLimit = clampNonNegative(outerHLimit - padY);
8989

9090
let maxChildH = 0;
91+
let usedMainInConstraintPass = 0;
9192

9293
if (needsConstraintPass) {
9394
const parentRect: Rect = { x: 0, y: 0, w: cw, h: chLimit };
@@ -193,6 +194,12 @@ export function measureStackKinds(
193194
const childH = align === "stretch" ? chLimit : sizeRes.value.h;
194195
if (childH > maxChildH) maxChildH = childH;
195196
}
197+
198+
for (let i = 0; i < mainSizes.length; i++) {
199+
usedMainInConstraintPass += mainSizes[i] ?? 0;
200+
}
201+
usedMainInConstraintPass +=
202+
vnode.children.length <= 1 ? 0 : gap * (vnode.children.length - 1);
196203
} else {
197204
let remainingWidth = cw;
198205
let cursorX = 0;
@@ -231,7 +238,8 @@ export function measureStackKinds(
231238
});
232239
}
233240

234-
const chosenW = forcedW ?? (fillMain ? maxWCap : maxWCap);
241+
const shrinkW = padX + Math.min(cw, usedMainInConstraintPass);
242+
const chosenW = forcedW ?? (fillMain ? maxWCap : Math.min(maxWCap, shrinkW));
235243
const chosenH = forcedH ?? (fillCross ? maxHCap : Math.min(maxHCap, padY + maxChildH));
236244
const innerW = clampWithin(chosenW, minW, maxWCap);
237245
const innerH = clampWithin(chosenH, minH, maxHCap);
@@ -268,20 +276,23 @@ export function measureStackKinds(
268276
const forcedW = self.width === null ? null : clampWithin(self.width, minW, maxWCap);
269277
const forcedH = self.height === null ? null : clampWithin(self.height, minH, maxHCap);
270278

271-
const needsConstraintPass = vnode.children.some(
272-
(c) => childHasFlexInMainAxis(c, "column") || childHasPercentInMainAxis(c, "column"),
279+
const hasFlexInMainAxis = vnode.children.some((c) => childHasFlexInMainAxis(c, "column"));
280+
const hasPercentInMainAxis = vnode.children.some((c) =>
281+
childHasPercentInMainAxis(c, "column"),
273282
);
274-
const fillMain = forcedH === null && needsConstraintPass;
283+
const needsConstraintPass = hasFlexInMainAxis || hasPercentInMainAxis;
284+
const fillMain = forcedH === null && hasPercentInMainAxis;
275285
const fillCross =
276286
forcedW === null && vnode.children.some((c) => childHasPercentInCrossAxis(c, "column"));
277287

278288
const outerWLimit = forcedW ?? maxWCap;
279-
const outerHLimit = forcedH ?? (fillMain ? maxHCap : maxHCap);
289+
const outerHLimit = forcedH ?? maxHCap;
280290

281291
const cw = clampNonNegative(outerWLimit - padX);
282292
const ch = clampNonNegative(outerHLimit - padY);
283293

284294
let maxChildW = 0;
295+
let usedMainInConstraintPass = 0;
285296

286297
if (needsConstraintPass) {
287298
const parentRect: Rect = { x: 0, y: 0, w: cw, h: ch };
@@ -387,6 +398,12 @@ export function measureStackKinds(
387398
const childW = align === "stretch" ? cw : sizeRes.value.w;
388399
if (childW > maxChildW) maxChildW = childW;
389400
}
401+
402+
for (let i = 0; i < mainSizes.length; i++) {
403+
usedMainInConstraintPass += mainSizes[i] ?? 0;
404+
}
405+
usedMainInConstraintPass +=
406+
vnode.children.length <= 1 ? 0 : gap * (vnode.children.length - 1);
390407
} else {
391408
let remainingHeight = ch;
392409
let cursorY = 0;
@@ -425,8 +442,9 @@ export function measureStackKinds(
425442
});
426443
}
427444

445+
const shrinkH = padY + Math.min(ch, usedMainInConstraintPass);
428446
const chosenW = forcedW ?? (fillCross ? maxWCap : Math.min(maxWCap, padX + maxChildW));
429-
const chosenH = forcedH ?? (fillMain ? maxHCap : maxHCap);
447+
const chosenH = forcedH ?? (fillMain ? maxHCap : Math.min(maxHCap, shrinkH));
430448
const innerW = clampWithin(chosenW, minW, maxWCap);
431449
const innerH = clampWithin(chosenH, minH, maxHCap);
432450
return ok({

0 commit comments

Comments
 (0)