Skip to content

Commit 0ffd0f4

Browse files
Merge pull request #72 from RtlZeroMemory/create-overflow-scroll-support
feat(layout): add overflow and scroll support to layout and renderer
2 parents ee25c95 + f35ac21 commit 0ffd0f4

16 files changed

Lines changed: 1980 additions & 80 deletions

File tree

docs/guide/layout.md

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -339,22 +339,36 @@ ui.row({ gap: 1 }, [
339339
]);
340340
```
341341

342-
## Overflow
342+
## Overflow and scroll
343343

344-
Rezi clips rendering to each widget’s allocated rect. Overflow is handled per-widget:
344+
For `row`, `column`, and `box`, `overflow` supports:
345345

346-
- `ui.text` supports `textOverflow: "clip" | "ellipsis" | "middle"` (and `maxWidth`)
347-
- Containers clip their children to the padded/bordered content area
346+
- `"visible"` (default)
347+
- `"hidden"`
348+
- `"scroll"`
348349

349-
Example: ellipsis truncation
350+
Layout overflow metadata uses these fields (all non-negative cells):
350351

351-
```typescript
352-
import { ui } from "@rezi-ui/core";
352+
- `scrollX`, `scrollY` - active scroll offset (clamped to available range)
353+
- `contentWidth`, `contentHeight` - measured content footprint
354+
- `viewportWidth`, `viewportHeight` - scrollable viewport size
353355

354-
ui.box({ width: 20, border: "single", p: 1 }, [
355-
ui.text("This is a long line that will truncate", { textOverflow: "ellipsis" }),
356-
]);
357-
```
356+
Clipping semantics (current behavior):
357+
358+
- Rendering clips children to the container content rect (after border/padding).
359+
- `"scroll"` additionally clips to a reduced scroll viewport and applies `scrollX`/`scrollY` offsets.
360+
- Hit-testing keeps legacy `"visible"` overlap behavior (container layout-rect clip), while `"hidden"` and `"scroll"` use stricter content/viewport clipping.
361+
362+
Scrollbar occupancy in `"scroll"` mode:
363+
364+
- Vertical scrollbar consumes 1 column on the right.
365+
- Horizontal scrollbar consumes 1 row at the bottom.
366+
- When both are shown, they share the bottom-right corner cell.
367+
368+
Collection migration behavior (current):
369+
370+
- `VirtualList`, `Table`, and `Tree` still maintain runtime scroll state.
371+
- Their effective runtime scroll values are mirrored into layout metadata (`scrollX`, `scrollY`, `contentWidth`, `contentHeight`, `viewportWidth`, `viewportHeight`) during rendering.
358372

359373
## Overlap hit-testing
360374

packages/core/src/layout/__tests__/layout.edgecases.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,120 @@ describe("layout edge cases", () => {
9595
);
9696
});
9797

98+
test("row overflow metadata clamps scrollX and shifts children", () => {
99+
const tree: VNode = {
100+
kind: "row",
101+
props: { width: 5, overflow: "scroll", scrollX: 99 },
102+
children: Object.freeze<readonly VNode[]>([
103+
{
104+
kind: "box",
105+
props: { border: "none", mr: -4 },
106+
children: Object.freeze<readonly VNode[]>([
107+
{ kind: "text", text: "123456789", props: {} },
108+
]),
109+
},
110+
]),
111+
};
112+
113+
const laidOut = mustLayout(tree, 5, 2);
114+
assert.deepEqual(laidOut.meta, {
115+
scrollX: 4,
116+
scrollY: 0,
117+
contentWidth: 9,
118+
contentHeight: 1,
119+
viewportWidth: 5,
120+
viewportHeight: 1,
121+
});
122+
assert.deepEqual(laidOut.children[0]?.rect, { x: -4, y: 0, w: 9, h: 1 });
123+
});
124+
125+
test("column overflow metadata clamps scrollY and shifts children", () => {
126+
const tree: VNode = {
127+
kind: "column",
128+
props: { height: 3, overflow: "scroll", scrollY: 99 },
129+
children: Object.freeze<readonly VNode[]>([
130+
{
131+
kind: "box",
132+
props: { border: "none", mb: -1 },
133+
children: Object.freeze<readonly VNode[]>([
134+
{ kind: "text", text: "a", props: {} },
135+
{ kind: "text", text: "b", props: {} },
136+
{ kind: "text", text: "c", props: {} },
137+
{ kind: "text", text: "d", props: {} },
138+
]),
139+
},
140+
]),
141+
};
142+
143+
const laidOut = mustLayout(tree, 6, 3);
144+
assert.deepEqual(laidOut.meta, {
145+
scrollX: 0,
146+
scrollY: 1,
147+
contentWidth: 1,
148+
contentHeight: 4,
149+
viewportWidth: 1,
150+
viewportHeight: 3,
151+
});
152+
assert.deepEqual(laidOut.children[0]?.rect, { x: 0, y: -1, w: 1, h: 4 });
153+
});
154+
155+
test("box overflow metadata clamps both axes and shifts children", () => {
156+
const tree: VNode = {
157+
kind: "box",
158+
props: { border: "none", width: 5, height: 3, overflow: "scroll", scrollX: 99, scrollY: 99 },
159+
children: Object.freeze<readonly VNode[]>([
160+
{
161+
kind: "box",
162+
props: { border: "none", mr: -4, mb: -1 },
163+
children: Object.freeze<readonly VNode[]>([
164+
{ kind: "text", text: "123456789", props: {} },
165+
{ kind: "text", text: "line2", props: {} },
166+
{ kind: "text", text: "line3", props: {} },
167+
{ kind: "text", text: "line4", props: {} },
168+
]),
169+
},
170+
]),
171+
};
172+
173+
const laidOut = mustLayout(tree, 5, 3);
174+
assert.deepEqual(laidOut.meta, {
175+
scrollX: 4,
176+
scrollY: 1,
177+
contentWidth: 9,
178+
contentHeight: 4,
179+
viewportWidth: 5,
180+
viewportHeight: 3,
181+
});
182+
assert.deepEqual(laidOut.children[0]?.rect, { x: -4, y: -1, w: 9, h: 4 });
183+
});
184+
185+
test("scroll clamp uses content-origin extent when children start after origin", () => {
186+
const tree: VNode = {
187+
kind: "row",
188+
props: { width: 5, overflow: "scroll", scrollX: 99 },
189+
children: Object.freeze<readonly VNode[]>([
190+
{
191+
kind: "box",
192+
props: { border: "none", ml: 2, mr: -8 },
193+
children: Object.freeze<readonly VNode[]>([
194+
{ kind: "text", text: "123456789", props: {} },
195+
]),
196+
},
197+
]),
198+
};
199+
200+
const laidOut = mustLayout(tree, 5, 2);
201+
assert.deepEqual(laidOut.meta, {
202+
scrollX: 6,
203+
scrollY: 0,
204+
contentWidth: 11,
205+
contentHeight: 1,
206+
viewportWidth: 5,
207+
viewportHeight: 1,
208+
});
209+
assert.deepEqual(laidOut.children[0]?.rect, { x: -4, y: 0, w: 9, h: 1 });
210+
});
211+
98212
test("button fractional px is handled deterministically", () => {
99213
const button: VNode = {
100214
kind: "button",

0 commit comments

Comments
 (0)