@bindtty/layout 0.1.0-alpha.1 → 0.1.0-alpha.3

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