@bindtty/layout 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/basic-engine.d.ts +2 -0
- package/dist/basic-engine.js +418 -0
- package/dist/engine.d.ts +3 -0
- package/dist/engine.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/intrinsic.d.ts +5 -0
- package/dist/intrinsic.js +1 -0
- package/dist/layout.d.ts +3 -0
- package/dist/layout.js +7 -0
- package/dist/measure.d.ts +4 -0
- package/dist/measure.js +16 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.js +1 -0
- package/dist/yoga-engine.d.ts +2 -0
- package/dist/yoga-engine.js +521 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @bindtty/layout
|
|
2
|
+
|
|
3
|
+
Layout tree generation for BindTTY mounted nodes.
|
|
4
|
+
|
|
5
|
+
将 `MountedNode` 树转换为带绝对坐标的 `LayoutNode` 树,供 `@bindtty/renderer-terminal` 绘制。
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { layoutRoot, createBasicLayoutEngine } from "@bindtty/layout";
|
|
11
|
+
|
|
12
|
+
const layoutTree = layoutRoot(runtime.root, {
|
|
13
|
+
viewport: { width: 80, height: 24 }
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 已支持的 intrinsic 布局
|
|
18
|
+
|
|
19
|
+
`screen`、`box`、`vstack`、`hstack`、`text`、`spacer`,以及 `fragment` / `show` / `for` 结构节点。
|
|
20
|
+
|
|
21
|
+
intrinsic `button` / `input` 在 schema 有定义,但 layout 会抛 `Unsupported layout element`;交互控件请使用 `@bindtty/widgets` 的组合实现。
|
|
22
|
+
|
|
23
|
+
高级 layout props(width、height、gap、flex、clip/scroll)见 [doc/specs/YOGA_AND_TEXT.md](../../doc/specs/YOGA_AND_TEXT.md) 与 [doc/packages/LAYOUT.md](../../doc/packages/LAYOUT.md)。默认 engine 为 `YogaLayoutEngine`。
|
|
24
|
+
|
|
25
|
+
## 文档
|
|
26
|
+
|
|
27
|
+
- [doc/packages/LAYOUT.md](../../doc/packages/LAYOUT.md) — 包落地设计
|
|
28
|
+
- [doc/specs/YOGA_AND_TEXT.md](../../doc/specs/YOGA_AND_TEXT.md) — text + Yoga 摘要
|
|
29
|
+
- [doc/specs/SCROLL_VIEWPORT.md](../../doc/specs/SCROLL_VIEWPORT.md) — clip / scroll
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { layoutText, readTextWrapMode } from "@bindtty/text";
|
|
2
|
+
import { clampNonNegative, toNonNegativeNumber } from "./measure.js";
|
|
3
|
+
const supportedPropsByTag = {
|
|
4
|
+
screen: new Set(),
|
|
5
|
+
vstack: new Set(),
|
|
6
|
+
hstack: new Set(),
|
|
7
|
+
box: new Set([
|
|
8
|
+
"padding",
|
|
9
|
+
"border",
|
|
10
|
+
"height",
|
|
11
|
+
"width",
|
|
12
|
+
"overflow",
|
|
13
|
+
"scrollX",
|
|
14
|
+
"scrollY"
|
|
15
|
+
]),
|
|
16
|
+
text: new Set(["value", "wrap", "color", "bold"]),
|
|
17
|
+
spacer: new Set(["size"]),
|
|
18
|
+
button: new Set(["value", "disabled"]),
|
|
19
|
+
input: new Set(["value", "placeholder"])
|
|
20
|
+
};
|
|
21
|
+
const futureLayoutProps = new Set([
|
|
22
|
+
"width",
|
|
23
|
+
"height",
|
|
24
|
+
"minWidth",
|
|
25
|
+
"minHeight",
|
|
26
|
+
"maxWidth",
|
|
27
|
+
"maxHeight",
|
|
28
|
+
"paddingX",
|
|
29
|
+
"paddingY",
|
|
30
|
+
"paddingTop",
|
|
31
|
+
"paddingRight",
|
|
32
|
+
"paddingBottom",
|
|
33
|
+
"paddingLeft",
|
|
34
|
+
"margin",
|
|
35
|
+
"marginX",
|
|
36
|
+
"marginY",
|
|
37
|
+
"marginTop",
|
|
38
|
+
"marginRight",
|
|
39
|
+
"marginBottom",
|
|
40
|
+
"marginLeft",
|
|
41
|
+
"gap",
|
|
42
|
+
"flexDirection",
|
|
43
|
+
"flexWrap",
|
|
44
|
+
"justifyContent",
|
|
45
|
+
"alignItems",
|
|
46
|
+
"flexGrow",
|
|
47
|
+
"flexShrink"
|
|
48
|
+
]);
|
|
49
|
+
const layoutPropAliases = new Map([
|
|
50
|
+
["padding-top", "paddingTop"],
|
|
51
|
+
["padding-right", "paddingRight"],
|
|
52
|
+
["padding-bottom", "paddingBottom"],
|
|
53
|
+
["padding-left", "paddingLeft"],
|
|
54
|
+
["padding-x", "paddingX"],
|
|
55
|
+
["padding-y", "paddingY"],
|
|
56
|
+
["margin-top", "marginTop"],
|
|
57
|
+
["margin-right", "marginRight"],
|
|
58
|
+
["margin-bottom", "marginBottom"],
|
|
59
|
+
["margin-left", "marginLeft"],
|
|
60
|
+
["margin-x", "marginX"],
|
|
61
|
+
["margin-y", "marginY"],
|
|
62
|
+
["flex-direction", "flexDirection"],
|
|
63
|
+
["flex-wrap", "flexWrap"],
|
|
64
|
+
["justify-content", "justifyContent"],
|
|
65
|
+
["align-items", "alignItems"],
|
|
66
|
+
["flex-grow", "flexGrow"],
|
|
67
|
+
["flex-shrink", "flexShrink"],
|
|
68
|
+
["min-width", "minWidth"],
|
|
69
|
+
["min-height", "minHeight"],
|
|
70
|
+
["max-width", "maxWidth"],
|
|
71
|
+
["max-height", "maxHeight"]
|
|
72
|
+
]);
|
|
73
|
+
const nonLayoutProps = new Set([
|
|
74
|
+
"id",
|
|
75
|
+
"focusStyle",
|
|
76
|
+
"onKey",
|
|
77
|
+
"onFocusChange"
|
|
78
|
+
]);
|
|
79
|
+
export function createBasicLayoutEngine() {
|
|
80
|
+
return {
|
|
81
|
+
layout(root, options) {
|
|
82
|
+
if (!root) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const constraint = {
|
|
86
|
+
width: options.viewport.width,
|
|
87
|
+
height: options.viewport.height,
|
|
88
|
+
flow: "column"
|
|
89
|
+
};
|
|
90
|
+
const measured = measureNode(root, constraint);
|
|
91
|
+
const rect = createRootRect(root, measured, options);
|
|
92
|
+
return arrangeNode(root, rect, constraint);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function measureNode(node, constraint) {
|
|
97
|
+
switch (node.kind) {
|
|
98
|
+
case "element":
|
|
99
|
+
return measureElement(node, constraint);
|
|
100
|
+
case "fragment":
|
|
101
|
+
case "show":
|
|
102
|
+
case "for":
|
|
103
|
+
return measureFlowChildren(getStructureChildren(node), constraint.flow, constraint);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function measureElement(node, constraint) {
|
|
107
|
+
validateElementProps(node);
|
|
108
|
+
switch (node.tag) {
|
|
109
|
+
case "screen":
|
|
110
|
+
return {
|
|
111
|
+
width: constraint.width,
|
|
112
|
+
height: constraint.height
|
|
113
|
+
};
|
|
114
|
+
case "text":
|
|
115
|
+
return measureTextElement(node, constraint);
|
|
116
|
+
case "spacer":
|
|
117
|
+
return measureSpacer(node, constraint);
|
|
118
|
+
case "vstack":
|
|
119
|
+
return measureFlowChildren(node.children, "column", constraint);
|
|
120
|
+
case "hstack":
|
|
121
|
+
return measureFlowChildren(node.children, "row", constraint);
|
|
122
|
+
case "box":
|
|
123
|
+
return measureBox(node, constraint);
|
|
124
|
+
case "button":
|
|
125
|
+
case "input":
|
|
126
|
+
throw new Error(`Unsupported layout element: ${node.tag}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function measureTextElement(node, constraint) {
|
|
130
|
+
const text = String(node.props.value ?? "");
|
|
131
|
+
const wrap = readTextWrapMode(node.props.wrap);
|
|
132
|
+
const layout = layoutText(text, {
|
|
133
|
+
width: wrap === "legacy" ? undefined : constraint.width,
|
|
134
|
+
wrap
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
width: layout.width,
|
|
138
|
+
height: layout.height
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function measureSpacer(node, constraint) {
|
|
142
|
+
const size = toNonNegativeNumber(node.props.size);
|
|
143
|
+
if (constraint.flow === "row") {
|
|
144
|
+
return {
|
|
145
|
+
width: size,
|
|
146
|
+
height: constraint.height
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
width: constraint.width,
|
|
151
|
+
height: size
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function measureBox(node, constraint) {
|
|
155
|
+
const edges = getBoxEdges(node);
|
|
156
|
+
const inset = getBoxInset(edges);
|
|
157
|
+
const width = readOptionalSize(node.props.width);
|
|
158
|
+
const height = readOptionalSize(node.props.height);
|
|
159
|
+
const contentConstraint = {
|
|
160
|
+
...shrinkConstraint(constraint, edges),
|
|
161
|
+
...(width === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { width: clampNonNegative(width - inset * 2) }),
|
|
164
|
+
...(height === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { height: clampNonNegative(height - inset * 2) })
|
|
167
|
+
};
|
|
168
|
+
const childrenSize = measureFlowChildren(node.children, "column", contentConstraint);
|
|
169
|
+
return {
|
|
170
|
+
width: width ?? childrenSize.width + inset * 2,
|
|
171
|
+
height: height ?? childrenSize.height + inset * 2
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function measureFlowChildren(children, flow, constraint) {
|
|
175
|
+
let width = 0;
|
|
176
|
+
let height = 0;
|
|
177
|
+
for (const child of children) {
|
|
178
|
+
const childSize = measureNode(child, {
|
|
179
|
+
...constraint,
|
|
180
|
+
flow
|
|
181
|
+
});
|
|
182
|
+
if (flow === "row") {
|
|
183
|
+
width += childSize.width;
|
|
184
|
+
height = Math.max(height, childSize.height);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
width = Math.max(width, childSize.width);
|
|
188
|
+
height += childSize.height;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { width, height };
|
|
192
|
+
}
|
|
193
|
+
function arrangeNode(node, rect, constraint) {
|
|
194
|
+
switch (node.kind) {
|
|
195
|
+
case "element":
|
|
196
|
+
return arrangeElement(node, rect, constraint);
|
|
197
|
+
case "fragment":
|
|
198
|
+
case "show":
|
|
199
|
+
case "for":
|
|
200
|
+
return arrangeStructureNode(node, rect, constraint);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function arrangeStructureNode(node, rect, constraint) {
|
|
204
|
+
return {
|
|
205
|
+
mounted: node,
|
|
206
|
+
rect,
|
|
207
|
+
contentRect: rect,
|
|
208
|
+
children: arrangeFlowChildren(getStructureChildren(node), rect, constraint.flow, constraint)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function arrangeElement(node, rect, constraint) {
|
|
212
|
+
validateElementProps(node);
|
|
213
|
+
switch (node.tag) {
|
|
214
|
+
case "screen":
|
|
215
|
+
return arrangeFlowElement(node, rect, rect, "column", constraint);
|
|
216
|
+
case "vstack":
|
|
217
|
+
return arrangeFlowElement(node, rect, rect, "column", constraint);
|
|
218
|
+
case "hstack":
|
|
219
|
+
return arrangeFlowElement(node, rect, rect, "row", constraint);
|
|
220
|
+
case "box":
|
|
221
|
+
return arrangeBox(node, rect, constraint);
|
|
222
|
+
case "text":
|
|
223
|
+
case "spacer":
|
|
224
|
+
return createLeafLayout(node, rect);
|
|
225
|
+
case "button":
|
|
226
|
+
case "input":
|
|
227
|
+
throw new Error(`Unsupported layout element: ${node.tag}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function arrangeBox(node, rect, constraint) {
|
|
231
|
+
const edges = getBoxEdges(node);
|
|
232
|
+
const inset = getBoxInset(edges);
|
|
233
|
+
const contentRect = {
|
|
234
|
+
x: rect.x + inset,
|
|
235
|
+
y: rect.y + inset,
|
|
236
|
+
width: clampNonNegative(rect.width - inset * 2),
|
|
237
|
+
height: clampNonNegative(rect.height - inset * 2)
|
|
238
|
+
};
|
|
239
|
+
const contentConstraint = {
|
|
240
|
+
...constraint,
|
|
241
|
+
width: contentRect.width,
|
|
242
|
+
height: contentRect.height
|
|
243
|
+
};
|
|
244
|
+
const layout = arrangeFlowElement(node, rect, contentRect, "column", contentConstraint);
|
|
245
|
+
const overflow = readOverflow(node.props.overflow);
|
|
246
|
+
const hasScrollX = hasOwn(node.props, "scrollX");
|
|
247
|
+
const hasScrollY = hasOwn(node.props, "scrollY");
|
|
248
|
+
if (overflow === "clip") {
|
|
249
|
+
layout.clip = contentRect;
|
|
250
|
+
}
|
|
251
|
+
if (overflow === "clip" || hasScrollX || hasScrollY) {
|
|
252
|
+
const naturalContentSize = measureFlowChildren(node.children, "column", contentConstraint);
|
|
253
|
+
layout.contentSize = {
|
|
254
|
+
width: Math.max(contentRect.width, naturalContentSize.width),
|
|
255
|
+
height: Math.max(contentRect.height, naturalContentSize.height)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (hasScrollX || hasScrollY) {
|
|
259
|
+
const contentSize = layout.contentSize ?? {
|
|
260
|
+
width: contentRect.width,
|
|
261
|
+
height: contentRect.height
|
|
262
|
+
};
|
|
263
|
+
const maxY = clampNonNegative(contentSize.height - contentRect.height);
|
|
264
|
+
layout.scrollOffset = {
|
|
265
|
+
x: 0,
|
|
266
|
+
y: clamp(readScrollOffset(node.props.scrollY), 0, maxY)
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return layout;
|
|
270
|
+
}
|
|
271
|
+
function arrangeFlowElement(node, rect, contentRect, flow, constraint) {
|
|
272
|
+
return {
|
|
273
|
+
mounted: node,
|
|
274
|
+
rect,
|
|
275
|
+
contentRect,
|
|
276
|
+
children: arrangeFlowChildren(node.children, contentRect, flow, constraint)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function arrangeFlowChildren(children, contentRect, flow, constraint) {
|
|
280
|
+
const arrangedChildren = [];
|
|
281
|
+
let cursorX = contentRect.x;
|
|
282
|
+
let cursorY = contentRect.y;
|
|
283
|
+
for (const child of children) {
|
|
284
|
+
const childConstraint = {
|
|
285
|
+
width: contentRect.width,
|
|
286
|
+
height: contentRect.height,
|
|
287
|
+
flow
|
|
288
|
+
};
|
|
289
|
+
const childSize = measureNode(child, childConstraint);
|
|
290
|
+
const childRect = flow === "row"
|
|
291
|
+
? {
|
|
292
|
+
x: cursorX,
|
|
293
|
+
y: contentRect.y,
|
|
294
|
+
width: childSize.width,
|
|
295
|
+
height: childSize.height
|
|
296
|
+
}
|
|
297
|
+
: {
|
|
298
|
+
x: contentRect.x,
|
|
299
|
+
y: cursorY,
|
|
300
|
+
width: childSize.width,
|
|
301
|
+
height: childSize.height
|
|
302
|
+
};
|
|
303
|
+
arrangedChildren.push(arrangeNode(child, childRect, childConstraint));
|
|
304
|
+
if (flow === "row") {
|
|
305
|
+
cursorX += childSize.width;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
cursorY += childSize.height;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return arrangedChildren;
|
|
312
|
+
}
|
|
313
|
+
function createLeafLayout(node, rect) {
|
|
314
|
+
return {
|
|
315
|
+
mounted: node,
|
|
316
|
+
rect,
|
|
317
|
+
contentRect: rect,
|
|
318
|
+
children: []
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function createRootRect(root, measured, options) {
|
|
322
|
+
if (root.kind === "element" && root.tag === "screen") {
|
|
323
|
+
return {
|
|
324
|
+
x: 0,
|
|
325
|
+
y: 0,
|
|
326
|
+
width: options.viewport.width,
|
|
327
|
+
height: options.viewport.height
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
x: 0,
|
|
332
|
+
y: 0,
|
|
333
|
+
width: measured.width,
|
|
334
|
+
height: measured.height
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function getBoxEdges(node) {
|
|
338
|
+
return {
|
|
339
|
+
border: node.props.border ? 1 : 0,
|
|
340
|
+
padding: toNonNegativeNumber(node.props.padding)
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function getBoxInset(edges) {
|
|
344
|
+
return edges.border + edges.padding;
|
|
345
|
+
}
|
|
346
|
+
function shrinkConstraint(constraint, edges) {
|
|
347
|
+
const inset = getBoxInset(edges) * 2;
|
|
348
|
+
return {
|
|
349
|
+
...constraint,
|
|
350
|
+
width: clampNonNegative(constraint.width - inset),
|
|
351
|
+
height: clampNonNegative(constraint.height - inset)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function getStructureChildren(node) {
|
|
355
|
+
switch (node.kind) {
|
|
356
|
+
case "fragment":
|
|
357
|
+
return node.children;
|
|
358
|
+
case "show":
|
|
359
|
+
return node.activeBranch ? [node.activeBranch] : [];
|
|
360
|
+
case "for":
|
|
361
|
+
return node.items.map((item) => item.node);
|
|
362
|
+
case "element":
|
|
363
|
+
return node.children;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function validateElementProps(node) {
|
|
367
|
+
const supportedProps = supportedPropsByTag[node.tag];
|
|
368
|
+
const seenCanonicalProps = new Map();
|
|
369
|
+
const canonicalProps = [];
|
|
370
|
+
for (const propName of Object.keys(node.props)) {
|
|
371
|
+
if (nonLayoutProps.has(propName)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const canonicalName = layoutPropAliases.get(propName) ?? propName;
|
|
375
|
+
const previousName = seenCanonicalProps.get(canonicalName);
|
|
376
|
+
if (previousName && previousName !== propName) {
|
|
377
|
+
throw new Error(`Duplicate layout prop: ${canonicalName} / ${propName}`);
|
|
378
|
+
}
|
|
379
|
+
seenCanonicalProps.set(canonicalName, propName);
|
|
380
|
+
canonicalProps.push(canonicalName);
|
|
381
|
+
}
|
|
382
|
+
for (const canonicalName of canonicalProps) {
|
|
383
|
+
if (!supportedProps.has(canonicalName) &&
|
|
384
|
+
futureLayoutProps.has(canonicalName)) {
|
|
385
|
+
throw new Error(`Unsupported layout prop: ${canonicalName}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (node.tag === "box") {
|
|
389
|
+
readOverflow(node.props.overflow);
|
|
390
|
+
}
|
|
391
|
+
if (node.tag === "text") {
|
|
392
|
+
readTextWrapMode(node.props.wrap);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function readOptionalSize(value) {
|
|
396
|
+
if (value === null || value === undefined) {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
return toNonNegativeNumber(value);
|
|
400
|
+
}
|
|
401
|
+
function readOverflow(value) {
|
|
402
|
+
if (value === null || value === undefined) {
|
|
403
|
+
return "visible";
|
|
404
|
+
}
|
|
405
|
+
if (value === "visible" || value === "clip") {
|
|
406
|
+
return value;
|
|
407
|
+
}
|
|
408
|
+
throw new Error(`Unsupported overflow value: ${String(value)}`);
|
|
409
|
+
}
|
|
410
|
+
function readScrollOffset(value) {
|
|
411
|
+
return Math.floor(toNonNegativeNumber(value));
|
|
412
|
+
}
|
|
413
|
+
function clamp(value, min, max) {
|
|
414
|
+
return Math.min(Math.max(value, min), max);
|
|
415
|
+
}
|
|
416
|
+
function hasOwn(object, key) {
|
|
417
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
418
|
+
}
|
package/dist/engine.d.ts
ADDED
package/dist/engine.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createBasicLayoutEngine } from "./basic-engine.js";
|
|
2
|
+
export { createYogaLayoutEngine } from "./yoga-engine.js";
|
|
3
|
+
export { layoutRoot } from "./layout.js";
|
|
4
|
+
export { createZeroRect } from "./measure.js";
|
|
5
|
+
export type { LayoutFlow } from "./intrinsic.js";
|
|
6
|
+
export type { LayoutEngine, LayoutEngineOptions, LayoutNode, LayoutOptions, LayoutRect, LayoutScrollOffset, LayoutSize, LayoutViewport } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/layout.d.ts
ADDED
package/dist/layout.js
ADDED
package/dist/measure.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function createZeroRect() {
|
|
2
|
+
return {
|
|
3
|
+
x: 0,
|
|
4
|
+
y: 0,
|
|
5
|
+
width: 0,
|
|
6
|
+
height: 0
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function clampNonNegative(value) {
|
|
10
|
+
return Math.max(0, value);
|
|
11
|
+
}
|
|
12
|
+
export function toNonNegativeNumber(value, fallback = 0) {
|
|
13
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
14
|
+
? Math.max(0, value)
|
|
15
|
+
: fallback;
|
|
16
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MountedNode } from "@bindtty/vnode";
|
|
2
|
+
export interface LayoutRect {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
export interface LayoutViewport {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LayoutSize {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
export interface LayoutScrollOffset {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
}
|
|
20
|
+
export interface LayoutNode {
|
|
21
|
+
mounted: MountedNode;
|
|
22
|
+
rect: LayoutRect;
|
|
23
|
+
contentRect: LayoutRect;
|
|
24
|
+
clip?: LayoutRect;
|
|
25
|
+
scrollOffset?: LayoutScrollOffset;
|
|
26
|
+
contentSize?: LayoutSize;
|
|
27
|
+
children: LayoutNode[];
|
|
28
|
+
}
|
|
29
|
+
export interface LayoutEngineOptions {
|
|
30
|
+
viewport: LayoutViewport;
|
|
31
|
+
}
|
|
32
|
+
export interface LayoutEngine {
|
|
33
|
+
layout(root: MountedNode | null, options: LayoutEngineOptions): LayoutNode | null;
|
|
34
|
+
}
|
|
35
|
+
export interface LayoutOptions extends LayoutEngineOptions {
|
|
36
|
+
engine?: LayoutEngine;
|
|
37
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import Yoga from "yoga-layout";
|
|
2
|
+
import { layoutText, readTextWrapMode } from "@bindtty/text";
|
|
3
|
+
import { clampNonNegative, toNonNegativeNumber } from "./measure.js";
|
|
4
|
+
const supportedPropsByTag = {
|
|
5
|
+
screen: new Set([
|
|
6
|
+
"gap",
|
|
7
|
+
"flexWrap",
|
|
8
|
+
"justifyContent",
|
|
9
|
+
"alignItems",
|
|
10
|
+
"flexGrow",
|
|
11
|
+
"flexShrink"
|
|
12
|
+
]),
|
|
13
|
+
vstack: new Set([
|
|
14
|
+
"gap",
|
|
15
|
+
"flexWrap",
|
|
16
|
+
"justifyContent",
|
|
17
|
+
"alignItems",
|
|
18
|
+
"flexGrow",
|
|
19
|
+
"flexShrink"
|
|
20
|
+
]),
|
|
21
|
+
hstack: new Set([
|
|
22
|
+
"gap",
|
|
23
|
+
"flexWrap",
|
|
24
|
+
"justifyContent",
|
|
25
|
+
"alignItems",
|
|
26
|
+
"flexGrow",
|
|
27
|
+
"flexShrink"
|
|
28
|
+
]),
|
|
29
|
+
box: new Set([
|
|
30
|
+
"padding",
|
|
31
|
+
"border",
|
|
32
|
+
"height",
|
|
33
|
+
"width",
|
|
34
|
+
"overflow",
|
|
35
|
+
"scrollX",
|
|
36
|
+
"scrollY",
|
|
37
|
+
"gap",
|
|
38
|
+
"flexWrap",
|
|
39
|
+
"justifyContent",
|
|
40
|
+
"alignItems",
|
|
41
|
+
"flexGrow",
|
|
42
|
+
"flexShrink"
|
|
43
|
+
]),
|
|
44
|
+
text: new Set(["value", "wrap", "color", "bold", "flexGrow", "flexShrink"]),
|
|
45
|
+
spacer: new Set(["size", "flexGrow", "flexShrink"]),
|
|
46
|
+
button: new Set(["value", "disabled", "flexGrow", "flexShrink"]),
|
|
47
|
+
input: new Set(["value", "placeholder", "flexGrow", "flexShrink"])
|
|
48
|
+
};
|
|
49
|
+
const futureLayoutProps = new Set([
|
|
50
|
+
"width",
|
|
51
|
+
"height",
|
|
52
|
+
"minWidth",
|
|
53
|
+
"minHeight",
|
|
54
|
+
"maxWidth",
|
|
55
|
+
"maxHeight",
|
|
56
|
+
"paddingX",
|
|
57
|
+
"paddingY",
|
|
58
|
+
"paddingTop",
|
|
59
|
+
"paddingRight",
|
|
60
|
+
"paddingBottom",
|
|
61
|
+
"paddingLeft",
|
|
62
|
+
"margin",
|
|
63
|
+
"marginX",
|
|
64
|
+
"marginY",
|
|
65
|
+
"marginTop",
|
|
66
|
+
"marginRight",
|
|
67
|
+
"marginBottom",
|
|
68
|
+
"marginLeft",
|
|
69
|
+
"gap",
|
|
70
|
+
"flexDirection",
|
|
71
|
+
"flexWrap",
|
|
72
|
+
"justifyContent",
|
|
73
|
+
"alignItems",
|
|
74
|
+
"flexGrow",
|
|
75
|
+
"flexShrink"
|
|
76
|
+
]);
|
|
77
|
+
const layoutPropAliases = new Map([
|
|
78
|
+
["padding-top", "paddingTop"],
|
|
79
|
+
["padding-right", "paddingRight"],
|
|
80
|
+
["padding-bottom", "paddingBottom"],
|
|
81
|
+
["padding-left", "paddingLeft"],
|
|
82
|
+
["padding-x", "paddingX"],
|
|
83
|
+
["padding-y", "paddingY"],
|
|
84
|
+
["margin-top", "marginTop"],
|
|
85
|
+
["margin-right", "marginRight"],
|
|
86
|
+
["margin-bottom", "marginBottom"],
|
|
87
|
+
["margin-left", "marginLeft"],
|
|
88
|
+
["margin-x", "marginX"],
|
|
89
|
+
["margin-y", "marginY"],
|
|
90
|
+
["flex-direction", "flexDirection"],
|
|
91
|
+
["flex-wrap", "flexWrap"],
|
|
92
|
+
["justify-content", "justifyContent"],
|
|
93
|
+
["align-items", "alignItems"],
|
|
94
|
+
["flex-grow", "flexGrow"],
|
|
95
|
+
["flex-shrink", "flexShrink"],
|
|
96
|
+
["min-width", "minWidth"],
|
|
97
|
+
["min-height", "minHeight"],
|
|
98
|
+
["max-width", "maxWidth"],
|
|
99
|
+
["max-height", "maxHeight"]
|
|
100
|
+
]);
|
|
101
|
+
const nonLayoutProps = new Set([
|
|
102
|
+
"id",
|
|
103
|
+
"focusStyle",
|
|
104
|
+
"onKey",
|
|
105
|
+
"onFocusChange"
|
|
106
|
+
]);
|
|
107
|
+
const defaultYogaAdapter = {
|
|
108
|
+
createNode() {
|
|
109
|
+
return Yoga.Node.create();
|
|
110
|
+
},
|
|
111
|
+
calculateLayout(node, width, height) {
|
|
112
|
+
node.calculateLayout(width, height, Yoga.DIRECTION_LTR);
|
|
113
|
+
},
|
|
114
|
+
freeRecursive(node) {
|
|
115
|
+
node.freeRecursive();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
export function createYogaLayoutEngine() {
|
|
119
|
+
return {
|
|
120
|
+
layout(root, options) {
|
|
121
|
+
if (!root) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const entry = buildYogaTree(root, "column", options, defaultYogaAdapter);
|
|
125
|
+
try {
|
|
126
|
+
defaultYogaAdapter.calculateLayout(entry.yogaNode, getRootLayoutWidth(root, options), getRootLayoutHeight(root, options));
|
|
127
|
+
return readLayoutTree(entry, options);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
defaultYogaAdapter.freeRecursive(entry.yogaNode);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function buildYogaTree(node, inheritedFlow, options, adapter) {
|
|
136
|
+
const yogaNode = adapter.createNode();
|
|
137
|
+
const entry = {
|
|
138
|
+
mounted: node,
|
|
139
|
+
yogaNode,
|
|
140
|
+
children: []
|
|
141
|
+
};
|
|
142
|
+
try {
|
|
143
|
+
configureYogaNode(entry, inheritedFlow, options);
|
|
144
|
+
const childFlow = getChildFlow(node, inheritedFlow);
|
|
145
|
+
const children = getStructureChildren(node);
|
|
146
|
+
for (const child of children) {
|
|
147
|
+
const childEntry = buildYogaTree(child, childFlow, options, adapter);
|
|
148
|
+
entry.children.push(childEntry);
|
|
149
|
+
yogaNode.insertChild(childEntry.yogaNode, entry.children.length - 1);
|
|
150
|
+
}
|
|
151
|
+
return entry;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
yogaNode.freeRecursive();
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function configureYogaNode(entry, inheritedFlow, options) {
|
|
159
|
+
const { mounted: node, yogaNode } = entry;
|
|
160
|
+
yogaNode.setBoxSizing(Yoga.BOX_SIZING_BORDER_BOX);
|
|
161
|
+
switch (node.kind) {
|
|
162
|
+
case "fragment":
|
|
163
|
+
case "show":
|
|
164
|
+
case "for":
|
|
165
|
+
yogaNode.setFlexDirection(readYogaFlexDirection(inheritedFlow));
|
|
166
|
+
return;
|
|
167
|
+
case "element":
|
|
168
|
+
configureYogaElement(node, yogaNode, inheritedFlow, options);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function configureYogaElement(node, yogaNode, inheritedFlow, options) {
|
|
173
|
+
validateElementProps(node);
|
|
174
|
+
applyYogaItemProps(node, yogaNode);
|
|
175
|
+
switch (node.tag) {
|
|
176
|
+
case "screen":
|
|
177
|
+
yogaNode.setWidth(options.viewport.width);
|
|
178
|
+
yogaNode.setHeight(options.viewport.height);
|
|
179
|
+
yogaNode.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN);
|
|
180
|
+
applyYogaContainerProps(node, yogaNode);
|
|
181
|
+
return;
|
|
182
|
+
case "vstack":
|
|
183
|
+
yogaNode.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN);
|
|
184
|
+
applyYogaContainerProps(node, yogaNode);
|
|
185
|
+
return;
|
|
186
|
+
case "hstack":
|
|
187
|
+
yogaNode.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
|
188
|
+
applyYogaContainerProps(node, yogaNode);
|
|
189
|
+
return;
|
|
190
|
+
case "box":
|
|
191
|
+
configureYogaBox(node, yogaNode);
|
|
192
|
+
return;
|
|
193
|
+
case "text":
|
|
194
|
+
configureYogaText(node, yogaNode);
|
|
195
|
+
return;
|
|
196
|
+
case "spacer":
|
|
197
|
+
configureYogaSpacer(node, yogaNode, inheritedFlow);
|
|
198
|
+
return;
|
|
199
|
+
case "button":
|
|
200
|
+
case "input":
|
|
201
|
+
throw new Error(`Unsupported layout element: ${node.tag}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function configureYogaBox(node, yogaNode) {
|
|
205
|
+
const border = node.props.border ? 1 : 0;
|
|
206
|
+
const padding = toNonNegativeNumber(node.props.padding);
|
|
207
|
+
const width = readOptionalSize(node.props.width);
|
|
208
|
+
const height = readOptionalSize(node.props.height);
|
|
209
|
+
const overflow = readOverflow(node.props.overflow);
|
|
210
|
+
yogaNode.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN);
|
|
211
|
+
applyYogaContainerProps(node, yogaNode);
|
|
212
|
+
if (width !== undefined) {
|
|
213
|
+
yogaNode.setWidth(width);
|
|
214
|
+
}
|
|
215
|
+
if (height !== undefined) {
|
|
216
|
+
yogaNode.setHeight(height);
|
|
217
|
+
}
|
|
218
|
+
if (border > 0) {
|
|
219
|
+
yogaNode.setBorder(Yoga.EDGE_ALL, border);
|
|
220
|
+
}
|
|
221
|
+
if (padding > 0) {
|
|
222
|
+
yogaNode.setPadding(Yoga.EDGE_ALL, padding);
|
|
223
|
+
}
|
|
224
|
+
if (overflow === "clip" || hasOwn(node.props, "scrollX") || hasOwn(node.props, "scrollY")) {
|
|
225
|
+
yogaNode.setOverflow(Yoga.OVERFLOW_HIDDEN);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function configureYogaText(node, yogaNode) {
|
|
229
|
+
const text = String(node.props.value ?? "");
|
|
230
|
+
const wrap = readTextWrapMode(node.props.wrap);
|
|
231
|
+
yogaNode.setMeasureFunc((width, widthMode, _height, _heightMode) => {
|
|
232
|
+
const measured = layoutText(text, {
|
|
233
|
+
width: getMeasureWidth(width, widthMode, wrap),
|
|
234
|
+
wrap
|
|
235
|
+
});
|
|
236
|
+
return measured;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function configureYogaSpacer(node, yogaNode, inheritedFlow) {
|
|
240
|
+
const size = toNonNegativeNumber(node.props.size);
|
|
241
|
+
if (inheritedFlow === "row") {
|
|
242
|
+
yogaNode.setWidth(size);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
yogaNode.setHeight(size);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function readLayoutTree(entry, options) {
|
|
249
|
+
const rootRect = createRootRect(entry, options);
|
|
250
|
+
return readLayoutEntry(entry, rootRect);
|
|
251
|
+
}
|
|
252
|
+
function readLayoutEntry(entry, rect) {
|
|
253
|
+
const contentRect = readContentRect(entry, rect);
|
|
254
|
+
const children = entry.children.map((child) => {
|
|
255
|
+
const childRect = readChildRect(child, rect);
|
|
256
|
+
return readLayoutEntry(child, childRect);
|
|
257
|
+
});
|
|
258
|
+
const layout = {
|
|
259
|
+
mounted: entry.mounted,
|
|
260
|
+
rect,
|
|
261
|
+
contentRect,
|
|
262
|
+
children
|
|
263
|
+
};
|
|
264
|
+
if (entry.mounted.kind === "element" && entry.mounted.tag === "box") {
|
|
265
|
+
applyBoxMetadata(entry.mounted, layout);
|
|
266
|
+
}
|
|
267
|
+
return layout;
|
|
268
|
+
}
|
|
269
|
+
function createRootRect(entry, options) {
|
|
270
|
+
if (entry.mounted.kind === "element" && entry.mounted.tag === "screen") {
|
|
271
|
+
return {
|
|
272
|
+
x: 0,
|
|
273
|
+
y: 0,
|
|
274
|
+
width: options.viewport.width,
|
|
275
|
+
height: options.viewport.height
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
x: 0,
|
|
280
|
+
y: 0,
|
|
281
|
+
width: entry.yogaNode.getComputedWidth(),
|
|
282
|
+
height: entry.yogaNode.getComputedHeight()
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function readChildRect(entry, parentRect) {
|
|
286
|
+
return {
|
|
287
|
+
x: parentRect.x + entry.yogaNode.getComputedLeft(),
|
|
288
|
+
y: parentRect.y + entry.yogaNode.getComputedTop(),
|
|
289
|
+
width: entry.yogaNode.getComputedWidth(),
|
|
290
|
+
height: entry.yogaNode.getComputedHeight()
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function readContentRect(entry, rect) {
|
|
294
|
+
if (entry.mounted.kind !== "element" || entry.mounted.tag !== "box") {
|
|
295
|
+
return rect;
|
|
296
|
+
}
|
|
297
|
+
const inset = getBoxInset(entry.mounted);
|
|
298
|
+
return {
|
|
299
|
+
x: rect.x + inset,
|
|
300
|
+
y: rect.y + inset,
|
|
301
|
+
width: clampNonNegative(rect.width - inset * 2),
|
|
302
|
+
height: clampNonNegative(rect.height - inset * 2)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function applyBoxMetadata(node, layout) {
|
|
306
|
+
const overflow = readOverflow(node.props.overflow);
|
|
307
|
+
const hasScrollX = hasOwn(node.props, "scrollX");
|
|
308
|
+
const hasScrollY = hasOwn(node.props, "scrollY");
|
|
309
|
+
if (overflow === "clip") {
|
|
310
|
+
layout.clip = layout.contentRect;
|
|
311
|
+
}
|
|
312
|
+
if (overflow === "clip" || hasScrollX || hasScrollY) {
|
|
313
|
+
layout.contentSize = readContentSize(layout);
|
|
314
|
+
}
|
|
315
|
+
if (hasScrollX || hasScrollY) {
|
|
316
|
+
const contentSize = layout.contentSize ?? {
|
|
317
|
+
width: layout.contentRect.width,
|
|
318
|
+
height: layout.contentRect.height
|
|
319
|
+
};
|
|
320
|
+
layout.scrollOffset = {
|
|
321
|
+
x: clamp(readScrollOffset(node.props.scrollX), 0, clampNonNegative(contentSize.width - layout.contentRect.width)),
|
|
322
|
+
y: clamp(readScrollOffset(node.props.scrollY), 0, clampNonNegative(contentSize.height - layout.contentRect.height))
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function readContentSize(layout) {
|
|
327
|
+
let width = layout.contentRect.width;
|
|
328
|
+
let height = layout.contentRect.height;
|
|
329
|
+
for (const child of layout.children) {
|
|
330
|
+
width = Math.max(width, child.rect.x + child.rect.width - layout.contentRect.x);
|
|
331
|
+
height = Math.max(height, child.rect.y + child.rect.height - layout.contentRect.y);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
width: clampNonNegative(width),
|
|
335
|
+
height: clampNonNegative(height)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function getChildFlow(node, inheritedFlow) {
|
|
339
|
+
if (node.kind !== "element") {
|
|
340
|
+
return inheritedFlow;
|
|
341
|
+
}
|
|
342
|
+
if (node.tag === "hstack") {
|
|
343
|
+
return "row";
|
|
344
|
+
}
|
|
345
|
+
if (node.tag === "screen" || node.tag === "vstack" || node.tag === "box") {
|
|
346
|
+
return "column";
|
|
347
|
+
}
|
|
348
|
+
return inheritedFlow;
|
|
349
|
+
}
|
|
350
|
+
function getStructureChildren(node) {
|
|
351
|
+
switch (node.kind) {
|
|
352
|
+
case "fragment":
|
|
353
|
+
return node.children;
|
|
354
|
+
case "show":
|
|
355
|
+
return node.activeBranch ? [node.activeBranch] : [];
|
|
356
|
+
case "for":
|
|
357
|
+
return node.items.map((item) => item.node);
|
|
358
|
+
case "element":
|
|
359
|
+
return node.children;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function validateElementProps(node) {
|
|
363
|
+
const supportedProps = supportedPropsByTag[node.tag];
|
|
364
|
+
const seenCanonicalProps = new Map();
|
|
365
|
+
const canonicalProps = [];
|
|
366
|
+
for (const propName of Object.keys(node.props)) {
|
|
367
|
+
if (nonLayoutProps.has(propName)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const canonicalName = layoutPropAliases.get(propName) ?? propName;
|
|
371
|
+
const previousName = seenCanonicalProps.get(canonicalName);
|
|
372
|
+
if (previousName && previousName !== propName) {
|
|
373
|
+
throw new Error(`Duplicate layout prop: ${canonicalName} / ${propName}`);
|
|
374
|
+
}
|
|
375
|
+
seenCanonicalProps.set(canonicalName, propName);
|
|
376
|
+
canonicalProps.push(canonicalName);
|
|
377
|
+
}
|
|
378
|
+
for (const canonicalName of canonicalProps) {
|
|
379
|
+
if (!supportedProps.has(canonicalName) &&
|
|
380
|
+
futureLayoutProps.has(canonicalName)) {
|
|
381
|
+
throw new Error(`Unsupported layout prop: ${canonicalName}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (node.tag === "box") {
|
|
385
|
+
readOverflow(node.props.overflow);
|
|
386
|
+
}
|
|
387
|
+
if (node.tag === "text") {
|
|
388
|
+
readTextWrapMode(node.props.wrap);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function getMeasureWidth(width, widthMode, wrap) {
|
|
392
|
+
if (wrap === "legacy" || widthMode === Yoga.MEASURE_MODE_UNDEFINED) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
return width;
|
|
396
|
+
}
|
|
397
|
+
function readYogaFlexDirection(flow) {
|
|
398
|
+
return flow === "row" ? Yoga.FLEX_DIRECTION_ROW : Yoga.FLEX_DIRECTION_COLUMN;
|
|
399
|
+
}
|
|
400
|
+
function getRootLayoutWidth(root, options) {
|
|
401
|
+
return root.kind === "element" && root.tag === "screen"
|
|
402
|
+
? options.viewport.width
|
|
403
|
+
: undefined;
|
|
404
|
+
}
|
|
405
|
+
function getRootLayoutHeight(root, options) {
|
|
406
|
+
return root.kind === "element" && root.tag === "screen"
|
|
407
|
+
? options.viewport.height
|
|
408
|
+
: undefined;
|
|
409
|
+
}
|
|
410
|
+
function applyYogaItemProps(node, yogaNode) {
|
|
411
|
+
const flexGrow = readOptionalSize(readLayoutProp(node.props, "flexGrow"));
|
|
412
|
+
const flexShrink = readOptionalSize(readLayoutProp(node.props, "flexShrink"));
|
|
413
|
+
if (flexGrow !== undefined) {
|
|
414
|
+
yogaNode.setFlexGrow(flexGrow);
|
|
415
|
+
}
|
|
416
|
+
if (flexShrink !== undefined) {
|
|
417
|
+
yogaNode.setFlexShrink(flexShrink);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function applyYogaContainerProps(node, yogaNode) {
|
|
421
|
+
const gap = readOptionalSize(readLayoutProp(node.props, "gap"));
|
|
422
|
+
const flexWrap = readLayoutProp(node.props, "flexWrap");
|
|
423
|
+
const alignItems = readLayoutProp(node.props, "alignItems");
|
|
424
|
+
const justifyContent = readLayoutProp(node.props, "justifyContent");
|
|
425
|
+
if (gap !== undefined) {
|
|
426
|
+
yogaNode.setGap(Yoga.GUTTER_ALL, gap);
|
|
427
|
+
}
|
|
428
|
+
if (flexWrap !== undefined) {
|
|
429
|
+
yogaNode.setFlexWrap(readYogaFlexWrap(flexWrap));
|
|
430
|
+
}
|
|
431
|
+
if (alignItems !== undefined) {
|
|
432
|
+
yogaNode.setAlignItems(readYogaAlignItems(alignItems));
|
|
433
|
+
}
|
|
434
|
+
if (justifyContent !== undefined) {
|
|
435
|
+
yogaNode.setJustifyContent(readYogaJustifyContent(justifyContent));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function readYogaFlexWrap(value) {
|
|
439
|
+
switch (value) {
|
|
440
|
+
case "nowrap":
|
|
441
|
+
return Yoga.WRAP_NO_WRAP;
|
|
442
|
+
case "wrap":
|
|
443
|
+
return Yoga.WRAP_WRAP;
|
|
444
|
+
case "wrap-reverse":
|
|
445
|
+
return Yoga.WRAP_WRAP_REVERSE;
|
|
446
|
+
default:
|
|
447
|
+
throw new Error(`Unsupported flexWrap value: ${String(value)}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function readYogaAlignItems(value) {
|
|
451
|
+
switch (value) {
|
|
452
|
+
case "stretch":
|
|
453
|
+
return Yoga.ALIGN_STRETCH;
|
|
454
|
+
case "flex-start":
|
|
455
|
+
return Yoga.ALIGN_FLEX_START;
|
|
456
|
+
case "center":
|
|
457
|
+
return Yoga.ALIGN_CENTER;
|
|
458
|
+
case "flex-end":
|
|
459
|
+
return Yoga.ALIGN_FLEX_END;
|
|
460
|
+
case "baseline":
|
|
461
|
+
return Yoga.ALIGN_BASELINE;
|
|
462
|
+
default:
|
|
463
|
+
throw new Error(`Unsupported alignItems value: ${String(value)}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function readYogaJustifyContent(value) {
|
|
467
|
+
switch (value) {
|
|
468
|
+
case "flex-start":
|
|
469
|
+
return Yoga.JUSTIFY_FLEX_START;
|
|
470
|
+
case "center":
|
|
471
|
+
return Yoga.JUSTIFY_CENTER;
|
|
472
|
+
case "flex-end":
|
|
473
|
+
return Yoga.JUSTIFY_FLEX_END;
|
|
474
|
+
case "space-between":
|
|
475
|
+
return Yoga.JUSTIFY_SPACE_BETWEEN;
|
|
476
|
+
case "space-around":
|
|
477
|
+
return Yoga.JUSTIFY_SPACE_AROUND;
|
|
478
|
+
case "space-evenly":
|
|
479
|
+
return Yoga.JUSTIFY_SPACE_EVENLY;
|
|
480
|
+
default:
|
|
481
|
+
throw new Error(`Unsupported justifyContent value: ${String(value)}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function readLayoutProp(props, canonicalName) {
|
|
485
|
+
if (hasOwn(props, canonicalName)) {
|
|
486
|
+
return props[canonicalName];
|
|
487
|
+
}
|
|
488
|
+
for (const [alias, canonical] of layoutPropAliases) {
|
|
489
|
+
if (canonical === canonicalName && hasOwn(props, alias)) {
|
|
490
|
+
return props[alias];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
function getBoxInset(node) {
|
|
496
|
+
return (node.props.border ? 1 : 0) + toNonNegativeNumber(node.props.padding);
|
|
497
|
+
}
|
|
498
|
+
function readOptionalSize(value) {
|
|
499
|
+
if (value === null || value === undefined) {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
return toNonNegativeNumber(value);
|
|
503
|
+
}
|
|
504
|
+
function readOverflow(value) {
|
|
505
|
+
if (value === null || value === undefined) {
|
|
506
|
+
return "visible";
|
|
507
|
+
}
|
|
508
|
+
if (value === "visible" || value === "clip") {
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
throw new Error(`Unsupported overflow value: ${String(value)}`);
|
|
512
|
+
}
|
|
513
|
+
function readScrollOffset(value) {
|
|
514
|
+
return Math.floor(toNonNegativeNumber(value));
|
|
515
|
+
}
|
|
516
|
+
function clamp(value, min, max) {
|
|
517
|
+
return Math.min(Math.max(value, min), max);
|
|
518
|
+
}
|
|
519
|
+
function hasOwn(object, key) {
|
|
520
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
521
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bindtty/layout",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Layout tree generation for BindTTY mounted nodes.",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "npm run build --workspace @bindtty/text && npm run build --workspace @bindtty/vnode && npm run build --workspace @bindtty/jsx-runtime && npm run build --workspace @bindtty/runtime && npm run build && tsc -p test/tsconfig.json && node --test test/dist/*.test.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@bindtty/text": "0.1.0-alpha.1",
|
|
25
|
+
"@bindtty/vnode": "0.1.0-alpha.1",
|
|
26
|
+
"yoga-layout": "3.2.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@bindtty/jsx-runtime": "0.1.0-alpha.1",
|
|
30
|
+
"@bindtty/runtime": "0.1.0-alpha.1",
|
|
31
|
+
"@bindtty/signal": "0.1.0-alpha.1",
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"typescript": "^5.5.0"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/lithdoo/BindTTY.git",
|
|
45
|
+
"directory": "packages/layout"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/lithdoo/BindTTY/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/lithdoo/BindTTY/tree/main/packages/layout#readme"
|
|
51
|
+
}
|