@bindtty/layout 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +2 -1
- package/dist/basic-engine.js +3 -116
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/layout-props.d.ts +25 -0
- package/dist/layout-props.js +291 -0
- package/dist/yoga-engine.js +64 -167
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -20,10 +20,11 @@ const layoutTree = layoutRoot(runtime.root, {
|
|
|
20
20
|
|
|
21
21
|
intrinsic `button` / `input` 在 schema 有定义,但 layout 会抛 `Unsupported layout element`;交互控件请使用 `@bindtty/widgets` 的组合实现。
|
|
22
22
|
|
|
23
|
-
高级 layout props
|
|
23
|
+
高级 layout props 见 [doc/specs/LAYOUT_PROPS.md](../../doc/specs/LAYOUT_PROPS.md)(支持矩阵;运行 `npm run gen:layout-props` 同步文档)。默认 engine 为 `YogaLayoutEngine`。
|
|
24
24
|
|
|
25
25
|
## 文档
|
|
26
26
|
|
|
27
|
+
- [doc/specs/LAYOUT_PROPS.md](../../doc/specs/LAYOUT_PROPS.md) — layout prop 支持矩阵
|
|
27
28
|
- [doc/packages/LAYOUT.md](../../doc/packages/LAYOUT.md) — 包落地设计
|
|
28
29
|
- [doc/specs/YOGA_AND_TEXT.md](../../doc/specs/YOGA_AND_TEXT.md) — text + Yoga 摘要
|
|
29
30
|
- [doc/specs/SCROLL_VIEWPORT.md](../../doc/specs/SCROLL_VIEWPORT.md) — clip / scroll
|
package/dist/basic-engine.js
CHANGED
|
@@ -1,81 +1,6 @@
|
|
|
1
1
|
import { layoutText, readTextWrapMode } from "@bindtty/text";
|
|
2
|
+
import { basicSupportedPropsByTag, readOverflow, validateElementProps } from "./layout-props.js";
|
|
2
3
|
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
4
|
export function createBasicLayoutEngine() {
|
|
80
5
|
return {
|
|
81
6
|
layout(root, options) {
|
|
@@ -104,7 +29,7 @@ function measureNode(node, constraint) {
|
|
|
104
29
|
}
|
|
105
30
|
}
|
|
106
31
|
function measureElement(node, constraint) {
|
|
107
|
-
validateElementProps(node);
|
|
32
|
+
validateElementProps(node, basicSupportedPropsByTag[node.tag]);
|
|
108
33
|
switch (node.tag) {
|
|
109
34
|
case "screen":
|
|
110
35
|
return {
|
|
@@ -209,7 +134,7 @@ function arrangeStructureNode(node, rect, constraint) {
|
|
|
209
134
|
};
|
|
210
135
|
}
|
|
211
136
|
function arrangeElement(node, rect, constraint) {
|
|
212
|
-
validateElementProps(node);
|
|
137
|
+
validateElementProps(node, basicSupportedPropsByTag[node.tag]);
|
|
213
138
|
switch (node.tag) {
|
|
214
139
|
case "screen":
|
|
215
140
|
return arrangeFlowElement(node, rect, rect, "column", constraint);
|
|
@@ -363,50 +288,12 @@ function getStructureChildren(node) {
|
|
|
363
288
|
return node.children;
|
|
364
289
|
}
|
|
365
290
|
}
|
|
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
291
|
function readOptionalSize(value) {
|
|
396
292
|
if (value === null || value === undefined) {
|
|
397
293
|
return undefined;
|
|
398
294
|
}
|
|
399
295
|
return toNonNegativeNumber(value);
|
|
400
296
|
}
|
|
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
297
|
function readScrollOffset(value) {
|
|
411
298
|
return Math.floor(toNonNegativeNumber(value));
|
|
412
299
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { createBasicLayoutEngine } from "./basic-engine.js";
|
|
2
2
|
export { createYogaLayoutEngine } from "./yoga-engine.js";
|
|
3
3
|
export { layoutRoot } from "./layout.js";
|
|
4
|
+
export { basicSupportedPropsByTag, futureLayoutProps, getLayoutPropMatrixStatus, matrixLayoutProps, nonLayoutElementTags, resolveMargin, resolvePadding, yogaSupportedPropsByTag } from "./layout-props.js";
|
|
5
|
+
export type { BoxMarginEdges, BoxPaddingEdges, LayoutElementTag, LayoutPropMatrixStatus, MatrixLayoutProp } from "./layout-props.js";
|
|
4
6
|
export { createZeroRect } from "./measure.js";
|
|
5
7
|
export type { LayoutFlow } from "./intrinsic.js";
|
|
6
8
|
export type { LayoutEngine, LayoutEngineOptions, LayoutNode, LayoutOptions, LayoutRect, LayoutScrollOffset, LayoutSize, LayoutViewport } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createBasicLayoutEngine } from "./basic-engine.js";
|
|
2
2
|
export { createYogaLayoutEngine } from "./yoga-engine.js";
|
|
3
3
|
export { layoutRoot } from "./layout.js";
|
|
4
|
+
export { basicSupportedPropsByTag, futureLayoutProps, getLayoutPropMatrixStatus, matrixLayoutProps, nonLayoutElementTags, resolveMargin, resolvePadding, yogaSupportedPropsByTag } from "./layout-props.js";
|
|
4
5
|
export { createZeroRect } from "./measure.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MountedElementNode } from "@bindtty/vnode";
|
|
2
|
+
export type LayoutOverflow = "visible" | "clip";
|
|
3
|
+
export type LayoutElementTag = MountedElementNode["tag"];
|
|
4
|
+
export declare const yogaSupportedPropsByTag: Record<LayoutElementTag, ReadonlySet<string>>;
|
|
5
|
+
export declare const nonLayoutElementTags: Set<import("@bindtty/vnode").IntrinsicElementTag>;
|
|
6
|
+
export declare const basicSupportedPropsByTag: Record<LayoutElementTag, ReadonlySet<string>>;
|
|
7
|
+
export declare const futureLayoutProps: Set<string>;
|
|
8
|
+
export declare const layoutPropAliases: Map<string, string>;
|
|
9
|
+
export declare const nonLayoutProps: Set<string>;
|
|
10
|
+
export declare const matrixLayoutProps: readonly ["width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight", "padding", "paddingX", "paddingY", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "margin", "marginX", "marginY", "marginTop", "marginRight", "marginBottom", "marginLeft", "border", "overflow", "scrollX", "scrollY", "gap", "flexWrap", "justifyContent", "alignItems", "flexGrow", "flexShrink", "flexDirection"];
|
|
11
|
+
export type MatrixLayoutProp = (typeof matrixLayoutProps)[number];
|
|
12
|
+
export type LayoutPropMatrixStatus = "supported" | "future" | "na";
|
|
13
|
+
export interface BoxPaddingEdges {
|
|
14
|
+
top: number;
|
|
15
|
+
right: number;
|
|
16
|
+
bottom: number;
|
|
17
|
+
left: number;
|
|
18
|
+
}
|
|
19
|
+
export type BoxMarginEdges = BoxPaddingEdges;
|
|
20
|
+
export declare function resolvePadding(props: Record<string, unknown>): BoxPaddingEdges;
|
|
21
|
+
export declare function resolveMargin(props: Record<string, unknown>): BoxMarginEdges;
|
|
22
|
+
export declare function getLayoutPropMatrixStatus(tag: LayoutElementTag, prop: MatrixLayoutProp, engine?: "yoga" | "basic"): LayoutPropMatrixStatus;
|
|
23
|
+
export declare function readLayoutProp(props: Record<string, unknown>, canonicalName: string): unknown;
|
|
24
|
+
export declare function readOverflow(value: unknown): LayoutOverflow;
|
|
25
|
+
export declare function validateElementProps(node: MountedElementNode, supportedProps: ReadonlySet<string>): void;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { readTextWrapMode } from "@bindtty/text";
|
|
2
|
+
import { toNonNegativeNumber } from "./measure.js";
|
|
3
|
+
const yogaMinMaxSizeProps = [
|
|
4
|
+
"minWidth",
|
|
5
|
+
"minHeight",
|
|
6
|
+
"maxWidth",
|
|
7
|
+
"maxHeight"
|
|
8
|
+
];
|
|
9
|
+
const yogaBoxEdgePaddingProps = [
|
|
10
|
+
"paddingX",
|
|
11
|
+
"paddingY",
|
|
12
|
+
"paddingTop",
|
|
13
|
+
"paddingRight",
|
|
14
|
+
"paddingBottom",
|
|
15
|
+
"paddingLeft"
|
|
16
|
+
];
|
|
17
|
+
const yogaMarginProps = [
|
|
18
|
+
"margin",
|
|
19
|
+
"marginX",
|
|
20
|
+
"marginY",
|
|
21
|
+
"marginTop",
|
|
22
|
+
"marginRight",
|
|
23
|
+
"marginBottom",
|
|
24
|
+
"marginLeft"
|
|
25
|
+
];
|
|
26
|
+
const yogaLayoutItemTags = [
|
|
27
|
+
...yogaMinMaxSizeProps,
|
|
28
|
+
...yogaMarginProps
|
|
29
|
+
];
|
|
30
|
+
export const yogaSupportedPropsByTag = {
|
|
31
|
+
screen: new Set([
|
|
32
|
+
"gap",
|
|
33
|
+
"flexWrap",
|
|
34
|
+
"justifyContent",
|
|
35
|
+
"alignItems",
|
|
36
|
+
"flexGrow",
|
|
37
|
+
"flexShrink"
|
|
38
|
+
]),
|
|
39
|
+
vstack: new Set([
|
|
40
|
+
"gap",
|
|
41
|
+
"flexWrap",
|
|
42
|
+
"justifyContent",
|
|
43
|
+
"alignItems",
|
|
44
|
+
"flexGrow",
|
|
45
|
+
"flexShrink",
|
|
46
|
+
...yogaLayoutItemTags
|
|
47
|
+
]),
|
|
48
|
+
hstack: new Set([
|
|
49
|
+
"gap",
|
|
50
|
+
"flexWrap",
|
|
51
|
+
"justifyContent",
|
|
52
|
+
"alignItems",
|
|
53
|
+
"flexGrow",
|
|
54
|
+
"flexShrink",
|
|
55
|
+
...yogaLayoutItemTags
|
|
56
|
+
]),
|
|
57
|
+
box: new Set([
|
|
58
|
+
"padding",
|
|
59
|
+
"border",
|
|
60
|
+
"height",
|
|
61
|
+
"width",
|
|
62
|
+
"overflow",
|
|
63
|
+
"scrollX",
|
|
64
|
+
"scrollY",
|
|
65
|
+
"gap",
|
|
66
|
+
"flexWrap",
|
|
67
|
+
"justifyContent",
|
|
68
|
+
"alignItems",
|
|
69
|
+
"flexGrow",
|
|
70
|
+
"flexShrink",
|
|
71
|
+
...yogaLayoutItemTags,
|
|
72
|
+
...yogaBoxEdgePaddingProps
|
|
73
|
+
]),
|
|
74
|
+
text: new Set([
|
|
75
|
+
"value",
|
|
76
|
+
"wrap",
|
|
77
|
+
"color",
|
|
78
|
+
"bold",
|
|
79
|
+
"flexGrow",
|
|
80
|
+
"flexShrink",
|
|
81
|
+
...yogaLayoutItemTags
|
|
82
|
+
]),
|
|
83
|
+
spacer: new Set(["size", "flexGrow", "flexShrink", ...yogaLayoutItemTags]),
|
|
84
|
+
button: new Set(["value", "disabled", "flexGrow", "flexShrink"]),
|
|
85
|
+
input: new Set(["value", "placeholder", "flexGrow", "flexShrink"])
|
|
86
|
+
};
|
|
87
|
+
export const nonLayoutElementTags = new Set(["button", "input"]);
|
|
88
|
+
export const basicSupportedPropsByTag = {
|
|
89
|
+
screen: new Set(),
|
|
90
|
+
vstack: new Set(),
|
|
91
|
+
hstack: new Set(),
|
|
92
|
+
box: new Set([
|
|
93
|
+
"padding",
|
|
94
|
+
"border",
|
|
95
|
+
"height",
|
|
96
|
+
"width",
|
|
97
|
+
"overflow",
|
|
98
|
+
"scrollX",
|
|
99
|
+
"scrollY"
|
|
100
|
+
]),
|
|
101
|
+
text: new Set(["value", "wrap", "color", "bold"]),
|
|
102
|
+
spacer: new Set(["size"]),
|
|
103
|
+
button: new Set(["value", "disabled"]),
|
|
104
|
+
input: new Set(["value", "placeholder"])
|
|
105
|
+
};
|
|
106
|
+
export const futureLayoutProps = new Set([
|
|
107
|
+
"width",
|
|
108
|
+
"height",
|
|
109
|
+
"minWidth",
|
|
110
|
+
"minHeight",
|
|
111
|
+
"maxWidth",
|
|
112
|
+
"maxHeight",
|
|
113
|
+
"paddingX",
|
|
114
|
+
"paddingY",
|
|
115
|
+
"paddingTop",
|
|
116
|
+
"paddingRight",
|
|
117
|
+
"paddingBottom",
|
|
118
|
+
"paddingLeft",
|
|
119
|
+
"margin",
|
|
120
|
+
"marginX",
|
|
121
|
+
"marginY",
|
|
122
|
+
"marginTop",
|
|
123
|
+
"marginRight",
|
|
124
|
+
"marginBottom",
|
|
125
|
+
"marginLeft",
|
|
126
|
+
"gap",
|
|
127
|
+
"flexDirection",
|
|
128
|
+
"flexWrap",
|
|
129
|
+
"justifyContent",
|
|
130
|
+
"alignItems",
|
|
131
|
+
"flexGrow",
|
|
132
|
+
"flexShrink"
|
|
133
|
+
]);
|
|
134
|
+
export const layoutPropAliases = new Map([
|
|
135
|
+
["padding-top", "paddingTop"],
|
|
136
|
+
["padding-right", "paddingRight"],
|
|
137
|
+
["padding-bottom", "paddingBottom"],
|
|
138
|
+
["padding-left", "paddingLeft"],
|
|
139
|
+
["padding-x", "paddingX"],
|
|
140
|
+
["padding-y", "paddingY"],
|
|
141
|
+
["margin-top", "marginTop"],
|
|
142
|
+
["margin-right", "marginRight"],
|
|
143
|
+
["margin-bottom", "marginBottom"],
|
|
144
|
+
["margin-left", "marginLeft"],
|
|
145
|
+
["margin-x", "marginX"],
|
|
146
|
+
["margin-y", "marginY"],
|
|
147
|
+
["flex-direction", "flexDirection"],
|
|
148
|
+
["flex-wrap", "flexWrap"],
|
|
149
|
+
["justify-content", "justifyContent"],
|
|
150
|
+
["align-items", "alignItems"],
|
|
151
|
+
["flex-grow", "flexGrow"],
|
|
152
|
+
["flex-shrink", "flexShrink"],
|
|
153
|
+
["min-width", "minWidth"],
|
|
154
|
+
["min-height", "minHeight"],
|
|
155
|
+
["max-width", "maxWidth"],
|
|
156
|
+
["max-height", "maxHeight"]
|
|
157
|
+
]);
|
|
158
|
+
export const nonLayoutProps = new Set([
|
|
159
|
+
"id",
|
|
160
|
+
"focusStyle",
|
|
161
|
+
"onKey",
|
|
162
|
+
"onFocusChange"
|
|
163
|
+
]);
|
|
164
|
+
export const matrixLayoutProps = [
|
|
165
|
+
"width",
|
|
166
|
+
"height",
|
|
167
|
+
"minWidth",
|
|
168
|
+
"minHeight",
|
|
169
|
+
"maxWidth",
|
|
170
|
+
"maxHeight",
|
|
171
|
+
"padding",
|
|
172
|
+
"paddingX",
|
|
173
|
+
"paddingY",
|
|
174
|
+
"paddingTop",
|
|
175
|
+
"paddingRight",
|
|
176
|
+
"paddingBottom",
|
|
177
|
+
"paddingLeft",
|
|
178
|
+
"margin",
|
|
179
|
+
"marginX",
|
|
180
|
+
"marginY",
|
|
181
|
+
"marginTop",
|
|
182
|
+
"marginRight",
|
|
183
|
+
"marginBottom",
|
|
184
|
+
"marginLeft",
|
|
185
|
+
"border",
|
|
186
|
+
"overflow",
|
|
187
|
+
"scrollX",
|
|
188
|
+
"scrollY",
|
|
189
|
+
"gap",
|
|
190
|
+
"flexWrap",
|
|
191
|
+
"justifyContent",
|
|
192
|
+
"alignItems",
|
|
193
|
+
"flexGrow",
|
|
194
|
+
"flexShrink",
|
|
195
|
+
"flexDirection"
|
|
196
|
+
];
|
|
197
|
+
export function resolvePadding(props) {
|
|
198
|
+
const base = toNonNegativeNumber(readLayoutProp(props, "padding"));
|
|
199
|
+
const paddingX = readLayoutProp(props, "paddingX");
|
|
200
|
+
const paddingY = readLayoutProp(props, "paddingY");
|
|
201
|
+
const axisX = paddingX !== undefined ? toNonNegativeNumber(paddingX) : base;
|
|
202
|
+
const axisY = paddingY !== undefined ? toNonNegativeNumber(paddingY) : base;
|
|
203
|
+
const top = readLayoutProp(props, "paddingTop");
|
|
204
|
+
const right = readLayoutProp(props, "paddingRight");
|
|
205
|
+
const bottom = readLayoutProp(props, "paddingBottom");
|
|
206
|
+
const left = readLayoutProp(props, "paddingLeft");
|
|
207
|
+
return {
|
|
208
|
+
top: top !== undefined ? toNonNegativeNumber(top) : axisY,
|
|
209
|
+
right: right !== undefined ? toNonNegativeNumber(right) : axisX,
|
|
210
|
+
bottom: bottom !== undefined ? toNonNegativeNumber(bottom) : axisY,
|
|
211
|
+
left: left !== undefined ? toNonNegativeNumber(left) : axisX
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
export function resolveMargin(props) {
|
|
215
|
+
const base = toNonNegativeNumber(readLayoutProp(props, "margin"));
|
|
216
|
+
const marginX = readLayoutProp(props, "marginX");
|
|
217
|
+
const marginY = readLayoutProp(props, "marginY");
|
|
218
|
+
const axisX = marginX !== undefined ? toNonNegativeNumber(marginX) : base;
|
|
219
|
+
const axisY = marginY !== undefined ? toNonNegativeNumber(marginY) : base;
|
|
220
|
+
const top = readLayoutProp(props, "marginTop");
|
|
221
|
+
const right = readLayoutProp(props, "marginRight");
|
|
222
|
+
const bottom = readLayoutProp(props, "marginBottom");
|
|
223
|
+
const left = readLayoutProp(props, "marginLeft");
|
|
224
|
+
return {
|
|
225
|
+
top: top !== undefined ? toNonNegativeNumber(top) : axisY,
|
|
226
|
+
right: right !== undefined ? toNonNegativeNumber(right) : axisX,
|
|
227
|
+
bottom: bottom !== undefined ? toNonNegativeNumber(bottom) : axisY,
|
|
228
|
+
left: left !== undefined ? toNonNegativeNumber(left) : axisX
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export function getLayoutPropMatrixStatus(tag, prop, engine = "yoga") {
|
|
232
|
+
const supportedProps = engine === "yoga" ? yogaSupportedPropsByTag[tag] : basicSupportedPropsByTag[tag];
|
|
233
|
+
if (supportedProps.has(prop)) {
|
|
234
|
+
return "supported";
|
|
235
|
+
}
|
|
236
|
+
if (futureLayoutProps.has(prop)) {
|
|
237
|
+
return "future";
|
|
238
|
+
}
|
|
239
|
+
return "na";
|
|
240
|
+
}
|
|
241
|
+
export function readLayoutProp(props, canonicalName) {
|
|
242
|
+
if (hasOwn(props, canonicalName)) {
|
|
243
|
+
return props[canonicalName];
|
|
244
|
+
}
|
|
245
|
+
for (const [alias, canonical] of layoutPropAliases) {
|
|
246
|
+
if (canonical === canonicalName && hasOwn(props, alias)) {
|
|
247
|
+
return props[alias];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
export function readOverflow(value) {
|
|
253
|
+
if (value === null || value === undefined) {
|
|
254
|
+
return "visible";
|
|
255
|
+
}
|
|
256
|
+
if (value === "visible" || value === "clip") {
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`Unsupported overflow value: ${String(value)}`);
|
|
260
|
+
}
|
|
261
|
+
export function validateElementProps(node, supportedProps) {
|
|
262
|
+
const seenCanonicalProps = new Map();
|
|
263
|
+
const canonicalProps = [];
|
|
264
|
+
for (const propName of Object.keys(node.props)) {
|
|
265
|
+
if (nonLayoutProps.has(propName)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const canonicalName = layoutPropAliases.get(propName) ?? propName;
|
|
269
|
+
const previousName = seenCanonicalProps.get(canonicalName);
|
|
270
|
+
if (previousName && previousName !== propName) {
|
|
271
|
+
throw new Error(`Duplicate layout prop: ${canonicalName} / ${propName}`);
|
|
272
|
+
}
|
|
273
|
+
seenCanonicalProps.set(canonicalName, propName);
|
|
274
|
+
canonicalProps.push(canonicalName);
|
|
275
|
+
}
|
|
276
|
+
for (const canonicalName of canonicalProps) {
|
|
277
|
+
if (!supportedProps.has(canonicalName) &&
|
|
278
|
+
futureLayoutProps.has(canonicalName)) {
|
|
279
|
+
throw new Error(`Unsupported layout prop: ${canonicalName}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (node.tag === "box") {
|
|
283
|
+
readOverflow(node.props.overflow);
|
|
284
|
+
}
|
|
285
|
+
if (node.tag === "text") {
|
|
286
|
+
readTextWrapMode(node.props.wrap);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function hasOwn(object, key) {
|
|
290
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
291
|
+
}
|
package/dist/yoga-engine.js
CHANGED
|
@@ -1,109 +1,7 @@
|
|
|
1
1
|
import Yoga from "yoga-layout";
|
|
2
2
|
import { layoutText, readTextWrapMode } from "@bindtty/text";
|
|
3
|
+
import { readLayoutProp, readOverflow, resolveMargin, resolvePadding, validateElementProps, yogaSupportedPropsByTag } from "./layout-props.js";
|
|
3
4
|
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
5
|
const defaultYogaAdapter = {
|
|
108
6
|
createNode() {
|
|
109
7
|
return Yoga.Node.create();
|
|
@@ -170,8 +68,10 @@ function configureYogaNode(entry, inheritedFlow, options) {
|
|
|
170
68
|
}
|
|
171
69
|
}
|
|
172
70
|
function configureYogaElement(node, yogaNode, inheritedFlow, options) {
|
|
173
|
-
validateElementProps(node);
|
|
71
|
+
validateElementProps(node, yogaSupportedPropsByTag[node.tag]);
|
|
174
72
|
applyYogaItemProps(node, yogaNode);
|
|
73
|
+
applyYogaSizeProps(node, yogaNode);
|
|
74
|
+
applyYogaMarginProps(node, yogaNode);
|
|
175
75
|
switch (node.tag) {
|
|
176
76
|
case "screen":
|
|
177
77
|
yogaNode.setWidth(options.viewport.width);
|
|
@@ -203,7 +103,6 @@ function configureYogaElement(node, yogaNode, inheritedFlow, options) {
|
|
|
203
103
|
}
|
|
204
104
|
function configureYogaBox(node, yogaNode) {
|
|
205
105
|
const border = node.props.border ? 1 : 0;
|
|
206
|
-
const padding = toNonNegativeNumber(node.props.padding);
|
|
207
106
|
const width = readOptionalSize(node.props.width);
|
|
208
107
|
const height = readOptionalSize(node.props.height);
|
|
209
108
|
const overflow = readOverflow(node.props.overflow);
|
|
@@ -218,13 +117,26 @@ function configureYogaBox(node, yogaNode) {
|
|
|
218
117
|
if (border > 0) {
|
|
219
118
|
yogaNode.setBorder(Yoga.EDGE_ALL, border);
|
|
220
119
|
}
|
|
221
|
-
|
|
222
|
-
yogaNode.setPadding(Yoga.EDGE_ALL, padding);
|
|
223
|
-
}
|
|
120
|
+
applyYogaBoxPadding(node, yogaNode);
|
|
224
121
|
if (overflow === "clip" || hasOwn(node.props, "scrollX") || hasOwn(node.props, "scrollY")) {
|
|
225
122
|
yogaNode.setOverflow(Yoga.OVERFLOW_HIDDEN);
|
|
226
123
|
}
|
|
227
124
|
}
|
|
125
|
+
function applyYogaBoxPadding(node, yogaNode) {
|
|
126
|
+
const padding = resolvePadding(node.props);
|
|
127
|
+
if (padding.top > 0) {
|
|
128
|
+
yogaNode.setPadding(Yoga.EDGE_TOP, padding.top);
|
|
129
|
+
}
|
|
130
|
+
if (padding.right > 0) {
|
|
131
|
+
yogaNode.setPadding(Yoga.EDGE_RIGHT, padding.right);
|
|
132
|
+
}
|
|
133
|
+
if (padding.bottom > 0) {
|
|
134
|
+
yogaNode.setPadding(Yoga.EDGE_BOTTOM, padding.bottom);
|
|
135
|
+
}
|
|
136
|
+
if (padding.left > 0) {
|
|
137
|
+
yogaNode.setPadding(Yoga.EDGE_LEFT, padding.left);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
228
140
|
function configureYogaText(node, yogaNode) {
|
|
229
141
|
const text = String(node.props.value ?? "");
|
|
230
142
|
const wrap = readTextWrapMode(node.props.wrap);
|
|
@@ -294,12 +206,13 @@ function readContentRect(entry, rect) {
|
|
|
294
206
|
if (entry.mounted.kind !== "element" || entry.mounted.tag !== "box") {
|
|
295
207
|
return rect;
|
|
296
208
|
}
|
|
297
|
-
const
|
|
209
|
+
const borderSize = entry.mounted.props.border ? 1 : 0;
|
|
210
|
+
const padding = resolvePadding(entry.mounted.props);
|
|
298
211
|
return {
|
|
299
|
-
x: rect.x +
|
|
300
|
-
y: rect.y +
|
|
301
|
-
width: clampNonNegative(rect.width -
|
|
302
|
-
height: clampNonNegative(rect.height -
|
|
212
|
+
x: rect.x + borderSize + padding.left,
|
|
213
|
+
y: rect.y + borderSize + padding.top,
|
|
214
|
+
width: clampNonNegative(rect.width - borderSize * 2 - padding.left - padding.right),
|
|
215
|
+
height: clampNonNegative(rect.height - borderSize * 2 - padding.top - padding.bottom)
|
|
303
216
|
};
|
|
304
217
|
}
|
|
305
218
|
function applyBoxMetadata(node, layout) {
|
|
@@ -327,8 +240,11 @@ function readContentSize(layout) {
|
|
|
327
240
|
let width = layout.contentRect.width;
|
|
328
241
|
let height = layout.contentRect.height;
|
|
329
242
|
for (const child of layout.children) {
|
|
330
|
-
|
|
331
|
-
|
|
243
|
+
const margin = child.mounted.kind === "element"
|
|
244
|
+
? resolveMargin(child.mounted.props)
|
|
245
|
+
: { top: 0, right: 0, bottom: 0, left: 0 };
|
|
246
|
+
width = Math.max(width, child.rect.x + child.rect.width + margin.right - layout.contentRect.x);
|
|
247
|
+
height = Math.max(height, child.rect.y + child.rect.height + margin.bottom - layout.contentRect.y);
|
|
332
248
|
}
|
|
333
249
|
return {
|
|
334
250
|
width: clampNonNegative(width),
|
|
@@ -359,35 +275,6 @@ function getStructureChildren(node) {
|
|
|
359
275
|
return node.children;
|
|
360
276
|
}
|
|
361
277
|
}
|
|
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
278
|
function getMeasureWidth(width, widthMode, wrap) {
|
|
392
279
|
if (wrap === "legacy" || widthMode === Yoga.MEASURE_MODE_UNDEFINED) {
|
|
393
280
|
return undefined;
|
|
@@ -417,6 +304,39 @@ function applyYogaItemProps(node, yogaNode) {
|
|
|
417
304
|
yogaNode.setFlexShrink(flexShrink);
|
|
418
305
|
}
|
|
419
306
|
}
|
|
307
|
+
function applyYogaSizeProps(node, yogaNode) {
|
|
308
|
+
const minWidth = readOptionalSize(readLayoutProp(node.props, "minWidth"));
|
|
309
|
+
const minHeight = readOptionalSize(readLayoutProp(node.props, "minHeight"));
|
|
310
|
+
const maxWidth = readOptionalSize(readLayoutProp(node.props, "maxWidth"));
|
|
311
|
+
const maxHeight = readOptionalSize(readLayoutProp(node.props, "maxHeight"));
|
|
312
|
+
if (minWidth !== undefined) {
|
|
313
|
+
yogaNode.setMinWidth(minWidth);
|
|
314
|
+
}
|
|
315
|
+
if (minHeight !== undefined) {
|
|
316
|
+
yogaNode.setMinHeight(minHeight);
|
|
317
|
+
}
|
|
318
|
+
if (maxWidth !== undefined) {
|
|
319
|
+
yogaNode.setMaxWidth(maxWidth);
|
|
320
|
+
}
|
|
321
|
+
if (maxHeight !== undefined) {
|
|
322
|
+
yogaNode.setMaxHeight(maxHeight);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function applyYogaMarginProps(node, yogaNode) {
|
|
326
|
+
const margin = resolveMargin(node.props);
|
|
327
|
+
if (margin.top > 0) {
|
|
328
|
+
yogaNode.setMargin(Yoga.EDGE_TOP, margin.top);
|
|
329
|
+
}
|
|
330
|
+
if (margin.right > 0) {
|
|
331
|
+
yogaNode.setMargin(Yoga.EDGE_RIGHT, margin.right);
|
|
332
|
+
}
|
|
333
|
+
if (margin.bottom > 0) {
|
|
334
|
+
yogaNode.setMargin(Yoga.EDGE_BOTTOM, margin.bottom);
|
|
335
|
+
}
|
|
336
|
+
if (margin.left > 0) {
|
|
337
|
+
yogaNode.setMargin(Yoga.EDGE_LEFT, margin.left);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
420
340
|
function applyYogaContainerProps(node, yogaNode) {
|
|
421
341
|
const gap = readOptionalSize(readLayoutProp(node.props, "gap"));
|
|
422
342
|
const flexWrap = readLayoutProp(node.props, "flexWrap");
|
|
@@ -481,35 +401,12 @@ function readYogaJustifyContent(value) {
|
|
|
481
401
|
throw new Error(`Unsupported justifyContent value: ${String(value)}`);
|
|
482
402
|
}
|
|
483
403
|
}
|
|
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
404
|
function readOptionalSize(value) {
|
|
499
405
|
if (value === null || value === undefined) {
|
|
500
406
|
return undefined;
|
|
501
407
|
}
|
|
502
408
|
return toNonNegativeNumber(value);
|
|
503
409
|
}
|
|
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
410
|
function readScrollOffset(value) {
|
|
514
411
|
return Math.floor(toNonNegativeNumber(value));
|
|
515
412
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bindtty/layout",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Layout tree generation for BindTTY mounted nodes.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,14 +21,14 @@
|
|
|
21
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
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@bindtty/text": "0.1.0-alpha.
|
|
25
|
-
"@bindtty/vnode": "0.1.0-alpha.
|
|
24
|
+
"@bindtty/text": "0.1.0-alpha.2",
|
|
25
|
+
"@bindtty/vnode": "0.1.0-alpha.2",
|
|
26
26
|
"yoga-layout": "3.2.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@bindtty/jsx-runtime": "0.1.0-alpha.
|
|
30
|
-
"@bindtty/runtime": "0.1.0-alpha.
|
|
31
|
-
"@bindtty/signal": "0.1.0-alpha.
|
|
29
|
+
"@bindtty/jsx-runtime": "0.1.0-alpha.2",
|
|
30
|
+
"@bindtty/runtime": "0.1.0-alpha.2",
|
|
31
|
+
"@bindtty/signal": "0.1.0-alpha.2",
|
|
32
32
|
"@types/node": "^22.0.0",
|
|
33
33
|
"typescript": "^5.5.0"
|
|
34
34
|
},
|