@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 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(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`。
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
@@ -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
+ }
@@ -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
- if (padding > 0) {
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 inset = getBoxInset(entry.mounted);
209
+ const borderSize = entry.mounted.props.border ? 1 : 0;
210
+ const padding = resolvePadding(entry.mounted.props);
298
211
  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)
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
- 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);
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.1",
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.1",
25
- "@bindtty/vnode": "0.1.0-alpha.1",
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.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.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
  },