@hypen-space/web 0.4.46 → 0.4.82
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 +4 -4
- package/dist/canvas/dirty.d.ts +40 -0
- package/dist/canvas/dirty.js +100 -0
- package/dist/canvas/dirty.js.map +1 -0
- package/dist/canvas/events.d.ts +5 -0
- package/dist/canvas/events.js +30 -3
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/index.d.ts +5 -1
- package/dist/canvas/index.js +4 -0
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.d.ts +12 -2
- package/dist/canvas/layout.js +593 -63
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.d.ts +2 -0
- package/dist/canvas/paint.js +88 -11
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +16 -0
- package/dist/canvas/renderer.js +154 -15
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/scroll.d.ts +82 -0
- package/dist/canvas/scroll.js +639 -0
- package/dist/canvas/scroll.js.map +1 -0
- package/dist/canvas/selection.d.ts +63 -0
- package/dist/canvas/selection.js +466 -0
- package/dist/canvas/selection.js.map +1 -0
- package/dist/canvas/text.d.ts +6 -3
- package/dist/canvas/text.js +82 -43
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +18 -0
- package/dist/canvas/utils.d.ts +8 -2
- package/dist/canvas/utils.js +42 -16
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/virtualize.d.ts +25 -0
- package/dist/canvas/virtualize.js +65 -0
- package/dist/canvas/virtualize.js.map +1 -0
- package/dist/dom/applicators/index.js +0 -1
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/applicators/margin.js +58 -17
- package/dist/dom/applicators/margin.js.map +1 -1
- package/dist/dom/applicators/padding.js +42 -17
- package/dist/dom/applicators/padding.js.map +1 -1
- package/dist/dom/components/icon.d.ts +9 -0
- package/dist/dom/components/icon.js +67 -0
- package/dist/dom/components/icon.js.map +1 -0
- package/dist/dom/components/index.js +2 -0
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/renderer.d.ts +19 -5
- package/dist/dom/renderer.js +147 -17
- package/dist/dom/renderer.js.map +1 -1
- package/package.json +4 -2
- package/src/canvas/QUICKSTART.md +5 -5
- package/src/canvas/dirty.ts +116 -0
- package/src/canvas/events.ts +34 -3
- package/src/canvas/index.ts +5 -0
- package/src/canvas/layout.ts +653 -100
- package/src/canvas/paint.ts +109 -11
- package/src/canvas/renderer.ts +189 -16
- package/src/canvas/scroll.ts +729 -0
- package/src/canvas/selection.ts +560 -0
- package/src/canvas/text.ts +97 -54
- package/src/canvas/types.ts +26 -0
- package/src/canvas/utils.ts +47 -17
- package/src/canvas/virtualize.ts +81 -0
- package/src/dom/applicators/index.ts +1 -1
- package/src/dom/applicators/margin.ts +57 -11
- package/src/dom/applicators/padding.ts +45 -11
- package/src/dom/components/icon.ts +84 -0
- package/src/dom/components/index.ts +2 -0
- package/src/dom/renderer.ts +155 -17
package/dist/canvas/layout.js
CHANGED
|
@@ -1,17 +1,595 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Layout Engine
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Uses Taffy (Rust/WASM) for spec-compliant CSS Flexbox, Grid, and Block
|
|
5
|
+
* layout. Falls back to a basic flexbox implementation in environments
|
|
6
|
+
* without WASM support (e.g. test runners).
|
|
5
7
|
*/
|
|
6
8
|
import { parseSpacing, parseSize } from "./utils.js";
|
|
7
9
|
import { measureText } from "./text.js";
|
|
10
|
+
// Taffy imports — loaded lazily to avoid hard failure when WASM unavailable
|
|
11
|
+
let taffy = null;
|
|
12
|
+
let taffyReady = false;
|
|
13
|
+
let taffyInitPromise = null;
|
|
8
14
|
/**
|
|
9
|
-
*
|
|
15
|
+
* Initialise the Taffy WASM module. Safe to call multiple times; only the
|
|
16
|
+
* first call actually loads WASM. Resolves immediately on subsequent calls.
|
|
17
|
+
*/
|
|
18
|
+
export async function initTaffyLayout() {
|
|
19
|
+
if (taffyReady)
|
|
20
|
+
return true;
|
|
21
|
+
if (taffyInitPromise) {
|
|
22
|
+
await taffyInitPromise;
|
|
23
|
+
return taffyReady;
|
|
24
|
+
}
|
|
25
|
+
taffyInitPromise = (async () => {
|
|
26
|
+
try {
|
|
27
|
+
taffy = await import("taffy-layout");
|
|
28
|
+
await taffy.loadTaffy();
|
|
29
|
+
taffyReady = true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
taffy = null;
|
|
33
|
+
taffyReady = false;
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
await taffyInitPromise;
|
|
37
|
+
return taffyReady;
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Helpers: map Hypen VirtualNode props → Taffy Style
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/** Parse a Hypen prop value into a Taffy Dimension ("auto" | number | "N%") */
|
|
43
|
+
function toDimension(value) {
|
|
44
|
+
if (value === undefined || value === null || value === "auto")
|
|
45
|
+
return "auto";
|
|
46
|
+
if (typeof value === "number")
|
|
47
|
+
return value;
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
if (value.endsWith("%"))
|
|
50
|
+
return value;
|
|
51
|
+
const n = parseFloat(value);
|
|
52
|
+
return isNaN(n) ? "auto" : n;
|
|
53
|
+
}
|
|
54
|
+
return "auto";
|
|
55
|
+
}
|
|
56
|
+
function toLengthPct(value) {
|
|
57
|
+
if (typeof value === "number")
|
|
58
|
+
return value;
|
|
59
|
+
if (typeof value === "string") {
|
|
60
|
+
if (value.endsWith("%"))
|
|
61
|
+
return value;
|
|
62
|
+
const n = parseFloat(value);
|
|
63
|
+
return isNaN(n) ? 0 : n;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
function toLengthPctAuto(value) {
|
|
68
|
+
if (value === "auto" || value === undefined || value === null)
|
|
69
|
+
return "auto";
|
|
70
|
+
return toLengthPct(value);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a Taffy Style from a VirtualNode's props. This centralises the
|
|
74
|
+
* mapping between Hypen's declarative property names and CSS-level concepts.
|
|
75
|
+
*/
|
|
76
|
+
function buildTaffyStyle(node) {
|
|
77
|
+
const T = taffy;
|
|
78
|
+
const props = node.props;
|
|
79
|
+
const type = node.type.toLowerCase();
|
|
80
|
+
const style = new T.Style();
|
|
81
|
+
// --- Display / direction ---------------------------------------------------
|
|
82
|
+
const isColumn = type === "column" || props.flexDirection === "column";
|
|
83
|
+
const isGrid = type === "grid" || props.display === "grid";
|
|
84
|
+
if (isGrid) {
|
|
85
|
+
style.display = T.Display.Grid;
|
|
86
|
+
// --- Grid template tracks ------------------------------------------------
|
|
87
|
+
if (props.gridTemplateColumns) {
|
|
88
|
+
style.gridTemplateColumns = parseGridTemplate(props.gridTemplateColumns);
|
|
89
|
+
}
|
|
90
|
+
if (props.gridTemplateRows) {
|
|
91
|
+
style.gridTemplateRows = parseGridTemplate(props.gridTemplateRows);
|
|
92
|
+
}
|
|
93
|
+
// --- Grid auto flow ------------------------------------------------------
|
|
94
|
+
if (props.gridAutoFlow) {
|
|
95
|
+
style.gridAutoFlow = mapGridAutoFlow(T, props.gridAutoFlow);
|
|
96
|
+
}
|
|
97
|
+
// --- Grid auto tracks ----------------------------------------------------
|
|
98
|
+
if (props.gridAutoColumns) {
|
|
99
|
+
style.gridAutoColumns = parseTrackSizingList(props.gridAutoColumns);
|
|
100
|
+
}
|
|
101
|
+
if (props.gridAutoRows) {
|
|
102
|
+
style.gridAutoRows = parseTrackSizingList(props.gridAutoRows);
|
|
103
|
+
}
|
|
104
|
+
// --- Grid template areas -------------------------------------------------
|
|
105
|
+
if (props.gridTemplateAreas) {
|
|
106
|
+
style.gridTemplateAreas = parseGridTemplateAreas(props.gridTemplateAreas);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
style.display = T.Display.Flex;
|
|
111
|
+
}
|
|
112
|
+
style.flexDirection = isColumn ? T.FlexDirection.Column : T.FlexDirection.Row;
|
|
113
|
+
// --- Grid child placement (applies regardless of parent display) ----------
|
|
114
|
+
if (props.gridColumn) {
|
|
115
|
+
style.gridColumn = parseGridLine(props.gridColumn);
|
|
116
|
+
}
|
|
117
|
+
if (props.gridRow) {
|
|
118
|
+
style.gridRow = parseGridLine(props.gridRow);
|
|
119
|
+
}
|
|
120
|
+
if (props.gridColumnStart !== undefined) {
|
|
121
|
+
style.gridColumnStart = parseGridPlacement(props.gridColumnStart);
|
|
122
|
+
}
|
|
123
|
+
if (props.gridColumnEnd !== undefined) {
|
|
124
|
+
style.gridColumnEnd = parseGridPlacement(props.gridColumnEnd);
|
|
125
|
+
}
|
|
126
|
+
if (props.gridRowStart !== undefined) {
|
|
127
|
+
style.gridRowStart = parseGridPlacement(props.gridRowStart);
|
|
128
|
+
}
|
|
129
|
+
if (props.gridRowEnd !== undefined) {
|
|
130
|
+
style.gridRowEnd = parseGridPlacement(props.gridRowEnd);
|
|
131
|
+
}
|
|
132
|
+
// --- Flex properties -------------------------------------------------------
|
|
133
|
+
style.flexGrow = parseFloat(props.flexGrow) || parseFloat(props.flex) || 0;
|
|
134
|
+
style.flexShrink = props.flexShrink !== undefined ? parseFloat(props.flexShrink) : 1;
|
|
135
|
+
if (props.flexBasis !== undefined)
|
|
136
|
+
style.flexBasis = toDimension(props.flexBasis);
|
|
137
|
+
if (props.flexWrap === "wrap")
|
|
138
|
+
style.flexWrap = T.FlexWrap.Wrap;
|
|
139
|
+
else if (props.flexWrap === "wrap-reverse")
|
|
140
|
+
style.flexWrap = T.FlexWrap.WrapReverse;
|
|
141
|
+
// --- Alignment (Hypen uses verticalAlignment / horizontalAlignment) --------
|
|
142
|
+
const justifyRaw = isColumn
|
|
143
|
+
? (props.verticalAlignment || props.justifyContent)
|
|
144
|
+
: (props.horizontalAlignment || props.justifyContent);
|
|
145
|
+
const alignRaw = isColumn
|
|
146
|
+
? (props.horizontalAlignment || props.alignItems)
|
|
147
|
+
: (props.verticalAlignment || props.alignItems);
|
|
148
|
+
if (justifyRaw)
|
|
149
|
+
style.justifyContent = mapJustify(T, justifyRaw);
|
|
150
|
+
if (alignRaw)
|
|
151
|
+
style.alignItems = mapAlign(T, alignRaw);
|
|
152
|
+
// --- Size ------------------------------------------------------------------
|
|
153
|
+
const w = parseSize(props.width);
|
|
154
|
+
const h = parseSize(props.height);
|
|
155
|
+
// Component-type defaults
|
|
156
|
+
if (type === "app" || type === "spacer") {
|
|
157
|
+
style.size = {
|
|
158
|
+
width: w !== null ? w : "100%",
|
|
159
|
+
height: h !== null ? h : "100%",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
else if (type === "divider" || type === "separator") {
|
|
163
|
+
const orientation = props.orientation || "horizontal";
|
|
164
|
+
const thickness = parseFloat(props.thickness) || 1;
|
|
165
|
+
if (orientation === "vertical") {
|
|
166
|
+
style.size = { width: w ?? thickness, height: h !== null ? h : "100%" };
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
style.size = { width: w !== null ? w : "100%", height: h ?? thickness };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (type === "checkbox" || type === "radio") {
|
|
173
|
+
const sz = parseFloat(props.size) || 20;
|
|
174
|
+
style.size = { width: w ?? sz, height: h ?? sz };
|
|
175
|
+
}
|
|
176
|
+
else if (type === "switch" || type === "toggle") {
|
|
177
|
+
style.size = { width: w ?? 44, height: h ?? 24 };
|
|
178
|
+
}
|
|
179
|
+
else if (type === "slider") {
|
|
180
|
+
style.size = { width: w ?? 200, height: h ?? 20 };
|
|
181
|
+
}
|
|
182
|
+
else if (type === "progress" || type === "progressbar") {
|
|
183
|
+
style.size = { width: w ?? 200, height: h ?? 8 };
|
|
184
|
+
}
|
|
185
|
+
else if (type === "spinner" || type === "loading") {
|
|
186
|
+
const sz = parseFloat(props.size) || 24;
|
|
187
|
+
style.size = { width: w ?? sz, height: h ?? sz };
|
|
188
|
+
}
|
|
189
|
+
else if (type === "badge") {
|
|
190
|
+
style.size = { width: w ?? 20, height: h ?? 20 };
|
|
191
|
+
}
|
|
192
|
+
else if (type === "avatar") {
|
|
193
|
+
const sz = parseFloat(props.size) || 40;
|
|
194
|
+
style.size = { width: w ?? sz, height: h ?? sz };
|
|
195
|
+
}
|
|
196
|
+
else if (type === "icon") {
|
|
197
|
+
const sz = parseFloat(props.size) || 24;
|
|
198
|
+
style.size = { width: w ?? sz, height: h ?? sz };
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
style.size = { width: toDimension(props.width), height: toDimension(props.height) };
|
|
202
|
+
}
|
|
203
|
+
// --- Min / Max constraints -------------------------------------------------
|
|
204
|
+
if (props.minWidth !== undefined || props.minHeight !== undefined) {
|
|
205
|
+
style.minSize = {
|
|
206
|
+
width: toDimension(props.minWidth),
|
|
207
|
+
height: toDimension(props.minHeight),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (props.maxWidth !== undefined || props.maxHeight !== undefined) {
|
|
211
|
+
style.maxSize = {
|
|
212
|
+
width: toDimension(props.maxWidth),
|
|
213
|
+
height: toDimension(props.maxHeight),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// --- Margin ----------------------------------------------------------------
|
|
217
|
+
const m = parseSpacing(props.margin || 0);
|
|
218
|
+
if (props.marginTop !== undefined)
|
|
219
|
+
m.top = parseFloat(props.marginTop) || 0;
|
|
220
|
+
if (props.marginRight !== undefined)
|
|
221
|
+
m.right = parseFloat(props.marginRight) || 0;
|
|
222
|
+
if (props.marginBottom !== undefined)
|
|
223
|
+
m.bottom = parseFloat(props.marginBottom) || 0;
|
|
224
|
+
if (props.marginLeft !== undefined)
|
|
225
|
+
m.left = parseFloat(props.marginLeft) || 0;
|
|
226
|
+
style.margin = { top: m.top, right: m.right, bottom: m.bottom, left: m.left };
|
|
227
|
+
// --- Padding ---------------------------------------------------------------
|
|
228
|
+
const p = parseSpacing(props.padding || 0);
|
|
229
|
+
if (props.paddingTop !== undefined)
|
|
230
|
+
p.top = parseFloat(props.paddingTop) || 0;
|
|
231
|
+
if (props.paddingRight !== undefined)
|
|
232
|
+
p.right = parseFloat(props.paddingRight) || 0;
|
|
233
|
+
if (props.paddingBottom !== undefined)
|
|
234
|
+
p.bottom = parseFloat(props.paddingBottom) || 0;
|
|
235
|
+
if (props.paddingLeft !== undefined)
|
|
236
|
+
p.left = parseFloat(props.paddingLeft) || 0;
|
|
237
|
+
style.padding = { top: p.top, right: p.right, bottom: p.bottom, left: p.left };
|
|
238
|
+
// --- Border ----------------------------------------------------------------
|
|
239
|
+
const bw = parseFloat(props.borderWidth) || 0;
|
|
240
|
+
if (bw > 0) {
|
|
241
|
+
style.border = { top: bw, right: bw, bottom: bw, left: bw };
|
|
242
|
+
}
|
|
243
|
+
// --- Gap -------------------------------------------------------------------
|
|
244
|
+
const gap = parseFloat(props.gap) || 0;
|
|
245
|
+
if (gap > 0) {
|
|
246
|
+
style.gap = { width: gap, height: gap };
|
|
247
|
+
}
|
|
248
|
+
// --- Position (absolute) ---------------------------------------------------
|
|
249
|
+
if (props.position === "absolute") {
|
|
250
|
+
style.position = T.Position.Absolute;
|
|
251
|
+
if (props.top !== undefined)
|
|
252
|
+
style.top = toLengthPctAuto(props.top);
|
|
253
|
+
if (props.left !== undefined)
|
|
254
|
+
style.left = toLengthPctAuto(props.left);
|
|
255
|
+
if (props.right !== undefined)
|
|
256
|
+
style.right = toLengthPctAuto(props.right);
|
|
257
|
+
if (props.bottom !== undefined)
|
|
258
|
+
style.bottom = toLengthPctAuto(props.bottom);
|
|
259
|
+
}
|
|
260
|
+
return style;
|
|
261
|
+
}
|
|
262
|
+
function mapJustify(T, value) {
|
|
263
|
+
switch (value) {
|
|
264
|
+
case "center": return T.JustifyContent.Center;
|
|
265
|
+
case "flex-end":
|
|
266
|
+
case "end": return T.JustifyContent.End;
|
|
267
|
+
case "space-between": return T.JustifyContent.SpaceBetween;
|
|
268
|
+
case "space-around": return T.JustifyContent.SpaceAround;
|
|
269
|
+
case "space-evenly": return T.JustifyContent.SpaceEvenly;
|
|
270
|
+
default: return T.JustifyContent.Start;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function mapAlign(T, value) {
|
|
274
|
+
switch (value) {
|
|
275
|
+
case "center": return T.AlignItems.Center;
|
|
276
|
+
case "flex-end":
|
|
277
|
+
case "end": return T.AlignItems.End;
|
|
278
|
+
case "stretch": return T.AlignItems.Stretch;
|
|
279
|
+
case "baseline": return T.AlignItems.Baseline;
|
|
280
|
+
default: return T.AlignItems.Start;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Grid helpers: parse Hypen prop values → Taffy grid types
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
/**
|
|
287
|
+
* Parse a single track sizing value like "100", "auto", "1fr", "50%",
|
|
288
|
+
* "min-content", "max-content" into a TrackSizingFunction.
|
|
289
|
+
*/
|
|
290
|
+
function parseTrackSizing(value) {
|
|
291
|
+
if (typeof value === "number") {
|
|
292
|
+
return { min: value, max: value };
|
|
293
|
+
}
|
|
294
|
+
const s = String(value).trim();
|
|
295
|
+
if (s === "auto")
|
|
296
|
+
return { min: "auto", max: "auto" };
|
|
297
|
+
if (s === "min-content")
|
|
298
|
+
return { min: "min-content", max: "min-content" };
|
|
299
|
+
if (s === "max-content")
|
|
300
|
+
return { min: "max-content", max: "max-content" };
|
|
301
|
+
// Fractional units e.g. "1fr", "2.5fr"
|
|
302
|
+
if (s.endsWith("fr")) {
|
|
303
|
+
return { min: "auto", max: s };
|
|
304
|
+
}
|
|
305
|
+
// Percentage e.g. "50%"
|
|
306
|
+
if (s.endsWith("%")) {
|
|
307
|
+
return { min: s, max: s };
|
|
308
|
+
}
|
|
309
|
+
// Plain number in a string
|
|
310
|
+
const n = parseFloat(s);
|
|
311
|
+
if (!isNaN(n)) {
|
|
312
|
+
return { min: n, max: n };
|
|
313
|
+
}
|
|
314
|
+
return { min: "auto", max: "auto" };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Parse a grid-template-columns / grid-template-rows value.
|
|
318
|
+
* Accepts:
|
|
319
|
+
* - An array of values: [100, "1fr", "auto"]
|
|
320
|
+
* - A space-separated string: "100 1fr auto"
|
|
321
|
+
* - A single value: "1fr"
|
|
322
|
+
*/
|
|
323
|
+
function parseGridTemplate(value) {
|
|
324
|
+
if (Array.isArray(value)) {
|
|
325
|
+
return value.map(parseTrackSizing);
|
|
326
|
+
}
|
|
327
|
+
if (typeof value === "string") {
|
|
328
|
+
return value.split(/\s+/).filter(Boolean).map(parseTrackSizing);
|
|
329
|
+
}
|
|
330
|
+
if (typeof value === "number") {
|
|
331
|
+
return [parseTrackSizing(value)];
|
|
332
|
+
}
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Parse grid-auto-columns / grid-auto-rows (TrackSizingFunction[]).
|
|
337
|
+
* Same format as template tracks but semantically for implicit tracks.
|
|
338
|
+
*/
|
|
339
|
+
function parseTrackSizingList(value) {
|
|
340
|
+
return parseGridTemplate(value);
|
|
341
|
+
}
|
|
342
|
+
/** Map a gridAutoFlow string to Taffy's GridAutoFlow enum. */
|
|
343
|
+
function mapGridAutoFlow(T, value) {
|
|
344
|
+
switch (value) {
|
|
345
|
+
case "column": return T.GridAutoFlow.Column;
|
|
346
|
+
case "row-dense":
|
|
347
|
+
case "dense": return T.GridAutoFlow.RowDense;
|
|
348
|
+
case "column-dense": return T.GridAutoFlow.ColumnDense;
|
|
349
|
+
default: return T.GridAutoFlow.Row;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Parse a single GridPlacement value.
|
|
354
|
+
* Accepts: "auto", a number (line index), or { span: N }.
|
|
355
|
+
*/
|
|
356
|
+
function parseGridPlacement(value) {
|
|
357
|
+
if (value === "auto" || value === undefined || value === null)
|
|
358
|
+
return "auto";
|
|
359
|
+
if (typeof value === "number")
|
|
360
|
+
return value;
|
|
361
|
+
if (typeof value === "string") {
|
|
362
|
+
const trimmed = value.trim();
|
|
363
|
+
if (trimmed === "auto")
|
|
364
|
+
return "auto";
|
|
365
|
+
// "span N"
|
|
366
|
+
const spanMatch = trimmed.match(/^span\s+(\d+)$/);
|
|
367
|
+
if (spanMatch)
|
|
368
|
+
return { span: parseInt(spanMatch[1], 10) };
|
|
369
|
+
const n = parseInt(trimmed, 10);
|
|
370
|
+
if (!isNaN(n))
|
|
371
|
+
return n;
|
|
372
|
+
return "auto";
|
|
373
|
+
}
|
|
374
|
+
// Already an object like { span: 2 } or { line: 1, ident: "foo" }
|
|
375
|
+
if (typeof value === "object")
|
|
376
|
+
return value;
|
|
377
|
+
return "auto";
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Parse a grid-column / grid-row shorthand into a Line<GridPlacement>.
|
|
381
|
+
* Accepts:
|
|
382
|
+
* - An object { start, end }
|
|
383
|
+
* - A string "1 / 3" or "1 / span 2"
|
|
384
|
+
*/
|
|
385
|
+
function parseGridLine(value) {
|
|
386
|
+
if (typeof value === "object" && value !== null && "start" in value) {
|
|
387
|
+
return {
|
|
388
|
+
start: parseGridPlacement(value.start),
|
|
389
|
+
end: parseGridPlacement(value.end),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (typeof value === "string") {
|
|
393
|
+
const parts = value.split("/").map((s) => s.trim());
|
|
394
|
+
return {
|
|
395
|
+
start: parseGridPlacement(parts[0]),
|
|
396
|
+
end: parts.length > 1 ? parseGridPlacement(parts[1]) : "auto",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return { start: "auto", end: "auto" };
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Parse grid-template-areas.
|
|
403
|
+
* Accepts an array of strings like ["header header", "sidebar main", "footer footer"].
|
|
404
|
+
* Each string is a row; each word is a cell name (or "." for empty).
|
|
405
|
+
* Returns an array of GridTemplateArea objects.
|
|
406
|
+
*/
|
|
407
|
+
function parseGridTemplateAreas(value) {
|
|
408
|
+
if (!Array.isArray(value))
|
|
409
|
+
return [];
|
|
410
|
+
const rows = value.map((row) => String(row).split(/\s+/).filter(Boolean));
|
|
411
|
+
if (rows.length === 0)
|
|
412
|
+
return [];
|
|
413
|
+
// Collect unique area names (skip "." which means empty)
|
|
414
|
+
const areaNames = new Set();
|
|
415
|
+
for (const row of rows) {
|
|
416
|
+
for (const cell of row) {
|
|
417
|
+
if (cell !== ".")
|
|
418
|
+
areaNames.add(cell);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// For each area name, find its bounding rectangle
|
|
422
|
+
const areas = [];
|
|
423
|
+
for (const name of areaNames) {
|
|
424
|
+
let rowStart = Infinity, rowEnd = -1, colStart = Infinity, colEnd = -1;
|
|
425
|
+
for (let r = 0; r < rows.length; r++) {
|
|
426
|
+
for (let c = 0; c < rows[r].length; c++) {
|
|
427
|
+
if (rows[r][c] === name) {
|
|
428
|
+
rowStart = Math.min(rowStart, r + 1);
|
|
429
|
+
rowEnd = Math.max(rowEnd, r + 2); // end line is exclusive
|
|
430
|
+
colStart = Math.min(colStart, c + 1);
|
|
431
|
+
colEnd = Math.max(colEnd, c + 2);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (rowEnd > 0) {
|
|
436
|
+
areas.push({ name, rowStart, rowEnd, columnStart: colStart, columnEnd: colEnd });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return areas;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Build a TaffyTree mirroring the VirtualNode tree. Returns the root Taffy
|
|
443
|
+
* node id and a mapping from Taffy node ids → VirtualNodes.
|
|
444
|
+
*/
|
|
445
|
+
function buildTree(tree, node) {
|
|
446
|
+
const T = taffy;
|
|
447
|
+
const props = node.props;
|
|
448
|
+
const style = buildTaffyStyle(node);
|
|
449
|
+
// Text leaf nodes — create with measurement context
|
|
450
|
+
if (node.type === "text" && props[0] && node.children.length === 0) {
|
|
451
|
+
const text = String(props[0] || "");
|
|
452
|
+
const fontSize = parseFloat(props.fontSize) || 16;
|
|
453
|
+
const fontWeight = props.fontWeight || "normal";
|
|
454
|
+
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
455
|
+
const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
|
|
456
|
+
const p = parseSpacing(props.padding || 0);
|
|
457
|
+
if (props.paddingTop !== undefined)
|
|
458
|
+
p.top = parseFloat(props.paddingTop) || 0;
|
|
459
|
+
if (props.paddingRight !== undefined)
|
|
460
|
+
p.right = parseFloat(props.paddingRight) || 0;
|
|
461
|
+
if (props.paddingBottom !== undefined)
|
|
462
|
+
p.bottom = parseFloat(props.paddingBottom) || 0;
|
|
463
|
+
if (props.paddingLeft !== undefined)
|
|
464
|
+
p.left = parseFloat(props.paddingLeft) || 0;
|
|
465
|
+
const ctx = {
|
|
466
|
+
text,
|
|
467
|
+
fontSize,
|
|
468
|
+
fontWeight,
|
|
469
|
+
fontFamily,
|
|
470
|
+
lineHeight,
|
|
471
|
+
paddingH: p.left + p.right,
|
|
472
|
+
paddingV: p.top + p.bottom,
|
|
473
|
+
};
|
|
474
|
+
return tree.newLeafWithContext(style, ctx);
|
|
475
|
+
}
|
|
476
|
+
// Container nodes
|
|
477
|
+
const childIds = [];
|
|
478
|
+
for (const child of node.children) {
|
|
479
|
+
childIds.push(buildTree(tree, child));
|
|
480
|
+
}
|
|
481
|
+
return tree.newWithChildren(style, childIds);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Walk the TaffyTree in the same order as the VirtualNode tree and write
|
|
485
|
+
* computed layout data back to each VirtualNode.
|
|
486
|
+
*
|
|
487
|
+
* @param isRoot - true for the root node, where Taffy's x/y are 0 and
|
|
488
|
+
* margin must be added manually to match the expected
|
|
489
|
+
* absolute-position convention.
|
|
490
|
+
*/
|
|
491
|
+
function writeLayout(tree, taffyId, node, parentX, parentY, isRoot = false) {
|
|
492
|
+
const tl = tree.getLayout(taffyId);
|
|
493
|
+
// Taffy positions children relative to parent's content area (margin
|
|
494
|
+
// included in x/y). For the root node there is no parent, so Taffy
|
|
495
|
+
// reports x=0/y=0 — we must add the margin ourselves.
|
|
496
|
+
const absX = parentX + tl.x + (isRoot ? tl.marginLeft : 0);
|
|
497
|
+
const absY = parentY + tl.y + (isRoot ? tl.marginTop : 0);
|
|
498
|
+
const borderColor = node.props.borderColor || "transparent";
|
|
499
|
+
const borderRadius = parseFloat(node.props.borderRadius) || 0;
|
|
500
|
+
// Taffy computes uniform border width per side; pick top as representative
|
|
501
|
+
const borderWidth = tl.borderTop;
|
|
502
|
+
const layout = {
|
|
503
|
+
x: absX,
|
|
504
|
+
y: absY,
|
|
505
|
+
width: tl.width,
|
|
506
|
+
height: tl.height,
|
|
507
|
+
margin: {
|
|
508
|
+
top: tl.marginTop,
|
|
509
|
+
right: tl.marginRight,
|
|
510
|
+
bottom: tl.marginBottom,
|
|
511
|
+
left: tl.marginLeft,
|
|
512
|
+
},
|
|
513
|
+
padding: {
|
|
514
|
+
top: tl.paddingTop,
|
|
515
|
+
right: tl.paddingRight,
|
|
516
|
+
bottom: tl.paddingBottom,
|
|
517
|
+
left: tl.paddingLeft,
|
|
518
|
+
},
|
|
519
|
+
border: {
|
|
520
|
+
width: borderWidth,
|
|
521
|
+
color: borderColor,
|
|
522
|
+
radius: borderRadius,
|
|
523
|
+
},
|
|
524
|
+
contentX: tl.paddingLeft + borderWidth,
|
|
525
|
+
contentY: tl.paddingTop + borderWidth,
|
|
526
|
+
contentWidth: tl.width - tl.paddingLeft - tl.paddingRight - borderWidth * 2,
|
|
527
|
+
contentHeight: tl.height - tl.paddingTop - tl.paddingBottom - borderWidth * 2,
|
|
528
|
+
};
|
|
529
|
+
node.layout = layout;
|
|
530
|
+
// Recurse children (same order as buildTree)
|
|
531
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
532
|
+
const childTaffyId = tree.getChildAtIndex(taffyId, i);
|
|
533
|
+
writeLayout(tree, childTaffyId, node.children[i], absX, absY);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Compute layout for a virtual node tree using Taffy.
|
|
538
|
+
* Requires `initTaffyLayout()` to have been called and resolved.
|
|
539
|
+
*/
|
|
540
|
+
function computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y) {
|
|
541
|
+
const T = taffy;
|
|
542
|
+
const tree = new T.TaffyTree();
|
|
543
|
+
try {
|
|
544
|
+
const rootId = buildTree(tree, node);
|
|
545
|
+
// Compute layout with a measure function for text leaf nodes
|
|
546
|
+
tree.computeLayoutWithMeasure(rootId, { width: availableWidth, height: availableHeight }, (knownDimensions, availableSpace, _nodeId, context, _style) => {
|
|
547
|
+
const tctx = context;
|
|
548
|
+
if (!tctx?.text) {
|
|
549
|
+
return { width: knownDimensions.width ?? 0, height: knownDimensions.height ?? 0 };
|
|
550
|
+
}
|
|
551
|
+
const maxWidth = typeof availableSpace.width === "number"
|
|
552
|
+
? availableSpace.width - tctx.paddingH
|
|
553
|
+
: undefined;
|
|
554
|
+
const metrics = measureText(ctx, tctx.text, {
|
|
555
|
+
fontSize: tctx.fontSize,
|
|
556
|
+
fontWeight: tctx.fontWeight,
|
|
557
|
+
fontFamily: tctx.fontFamily,
|
|
558
|
+
lineHeight: tctx.lineHeight,
|
|
559
|
+
}, maxWidth);
|
|
560
|
+
return {
|
|
561
|
+
width: knownDimensions.width ?? (metrics.width + tctx.paddingH),
|
|
562
|
+
height: knownDimensions.height ?? (metrics.height + tctx.paddingV),
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
writeLayout(tree, rootId, node, x, y, /* isRoot */ true);
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
tree.free();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
// Public API
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
/**
|
|
575
|
+
* Compute layout for a virtual node tree.
|
|
576
|
+
*
|
|
577
|
+
* Uses Taffy (WASM) when available for spec-compliant CSS Flexbox/Grid/Block
|
|
578
|
+
* layout. Falls back to a basic JS flexbox implementation otherwise.
|
|
10
579
|
*/
|
|
11
580
|
export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0, y = 0) {
|
|
12
|
-
|
|
581
|
+
if (taffyReady && taffy) {
|
|
582
|
+
computeLayoutTaffy(ctx, node, availableWidth, availableHeight, x, y);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
computeLayoutFallback(ctx, node, availableWidth, availableHeight, x, y);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
// Fallback: original JS flexbox implementation (used in tests / non-WASM)
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
function computeLayoutFallback(ctx, node, availableWidth, availableHeight, x = 0, y = 0) {
|
|
13
592
|
const props = node.props;
|
|
14
|
-
// Support individual margins (marginTop, marginRight, etc.)
|
|
15
593
|
let margin = parseSpacing(props.margin || 0);
|
|
16
594
|
if (props.marginTop !== undefined)
|
|
17
595
|
margin.top = parseFloat(props.marginTop) || 0;
|
|
@@ -21,7 +599,6 @@ export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0,
|
|
|
21
599
|
margin.bottom = parseFloat(props.marginBottom) || 0;
|
|
22
600
|
if (props.marginLeft !== undefined)
|
|
23
601
|
margin.left = parseFloat(props.marginLeft) || 0;
|
|
24
|
-
// Support individual padding (paddingTop, paddingRight, etc.)
|
|
25
602
|
let padding = parseSpacing(props.padding || 0);
|
|
26
603
|
if (props.paddingTop !== undefined)
|
|
27
604
|
padding.top = parseFloat(props.paddingTop) || 0;
|
|
@@ -34,25 +611,20 @@ export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0,
|
|
|
34
611
|
const borderWidth = parseFloat(props.borderWidth) || 0;
|
|
35
612
|
const borderColor = props.borderColor || "transparent";
|
|
36
613
|
const borderRadius = parseFloat(props.borderRadius) || 0;
|
|
37
|
-
// Calculate available space after margin
|
|
38
614
|
const availableAfterMargin = {
|
|
39
615
|
width: availableWidth - margin.left - margin.right,
|
|
40
616
|
height: availableHeight - margin.top - margin.bottom,
|
|
41
617
|
};
|
|
42
|
-
// Determine size
|
|
43
618
|
let width = parseSize(props.width);
|
|
44
619
|
let height = parseSize(props.height);
|
|
45
|
-
// Default sizing for specific component types
|
|
46
620
|
const type = node.type.toLowerCase();
|
|
47
621
|
if (type === "app") {
|
|
48
|
-
// App fills the full available space by default
|
|
49
622
|
if (width === null)
|
|
50
623
|
width = availableAfterMargin.width;
|
|
51
624
|
if (height === null)
|
|
52
625
|
height = availableAfterMargin.height;
|
|
53
626
|
}
|
|
54
627
|
else if (type === "spacer") {
|
|
55
|
-
// Spacer fills available space by default
|
|
56
628
|
if (width === null)
|
|
57
629
|
width = availableAfterMargin.width;
|
|
58
630
|
if (height === null)
|
|
@@ -126,7 +698,6 @@ export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0,
|
|
|
126
698
|
if (height === null)
|
|
127
699
|
height = size;
|
|
128
700
|
}
|
|
129
|
-
// Intrinsic sizing for text nodes
|
|
130
701
|
if (node.type === "text" && node.props[0]) {
|
|
131
702
|
const text = String(node.props[0] || "");
|
|
132
703
|
const fontSize = parseFloat(props.fontSize) || 16;
|
|
@@ -140,12 +711,10 @@ export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0,
|
|
|
140
711
|
if (!height)
|
|
141
712
|
height = metrics.height + padding.top + padding.bottom;
|
|
142
713
|
}
|
|
143
|
-
// Default to available size if not specified
|
|
144
714
|
if (width === null)
|
|
145
715
|
width = availableAfterMargin.width;
|
|
146
716
|
if (height === null)
|
|
147
717
|
height = availableAfterMargin.height;
|
|
148
|
-
// Apply constraints
|
|
149
718
|
const minWidth = parseSize(props.minWidth);
|
|
150
719
|
const maxWidth = parseSize(props.maxWidth);
|
|
151
720
|
const minHeight = parseSize(props.minHeight);
|
|
@@ -158,7 +727,6 @@ export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0,
|
|
|
158
727
|
height = Math.max(height, minHeight);
|
|
159
728
|
if (maxHeight !== null)
|
|
160
729
|
height = Math.min(height, maxHeight);
|
|
161
|
-
// Create layout object
|
|
162
730
|
const layout = {
|
|
163
731
|
x: x + margin.left,
|
|
164
732
|
y: y + margin.top,
|
|
@@ -177,25 +745,19 @@ export function computeLayout(ctx, node, availableWidth, availableHeight, x = 0,
|
|
|
177
745
|
contentHeight: height - padding.top - padding.bottom - borderWidth * 2,
|
|
178
746
|
};
|
|
179
747
|
node.layout = layout;
|
|
180
|
-
// Layout children if container
|
|
181
748
|
if (node.children.length > 0) {
|
|
182
|
-
|
|
749
|
+
layoutChildrenFallback(ctx, node);
|
|
183
750
|
}
|
|
184
751
|
}
|
|
185
|
-
|
|
186
|
-
* Layout children using flexbox-like rules
|
|
187
|
-
*/
|
|
188
|
-
function layoutChildren(ctx, parent) {
|
|
752
|
+
function layoutChildrenFallback(ctx, parent) {
|
|
189
753
|
const layout = parent.layout;
|
|
190
754
|
const props = parent.props;
|
|
191
|
-
// Stack components overlay children on top of each other
|
|
192
755
|
if (parent.type.toLowerCase() === "stack") {
|
|
193
|
-
|
|
756
|
+
layoutStackChildrenFallback(ctx, parent);
|
|
194
757
|
return;
|
|
195
758
|
}
|
|
196
759
|
const flexDirection = props.flexDirection || (parent.type === "column" ? "column" : "row");
|
|
197
760
|
const isColumn = flexDirection === "column";
|
|
198
|
-
// Resolve verticalAlignment/horizontalAlignment to main/cross axis based on direction
|
|
199
761
|
const justifyContent = isColumn
|
|
200
762
|
? (props.verticalAlignment || "flex-start")
|
|
201
763
|
: (props.horizontalAlignment || "flex-start");
|
|
@@ -205,7 +767,6 @@ function layoutChildren(ctx, parent) {
|
|
|
205
767
|
const gap = parseFloat(props.gap) || 0;
|
|
206
768
|
const availableWidth = layout.contentWidth;
|
|
207
769
|
const availableHeight = layout.contentHeight;
|
|
208
|
-
// First pass: compute intrinsic sizes and collect flex info
|
|
209
770
|
const childInfo = [];
|
|
210
771
|
let totalMainSize = 0;
|
|
211
772
|
let totalFlexGrow = 0;
|
|
@@ -214,11 +775,9 @@ function layoutChildren(ctx, parent) {
|
|
|
214
775
|
const flexGrow = parseFloat(child.props.flexGrow) || parseFloat(child.props.flex) || 0;
|
|
215
776
|
const flexShrink = parseFloat(child.props.flexShrink) || 1;
|
|
216
777
|
const flexBasis = parseSize(child.props.flexBasis);
|
|
217
|
-
|
|
218
|
-
computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
|
|
778
|
+
computeLayoutFallback(ctx, child, availableWidth, availableHeight, 0, 0);
|
|
219
779
|
const childLayout = child.layout;
|
|
220
780
|
let mainSize = isColumn ? childLayout.height : childLayout.width;
|
|
221
|
-
// Use flexBasis if specified
|
|
222
781
|
if (flexBasis !== null) {
|
|
223
782
|
mainSize = flexBasis;
|
|
224
783
|
if (isColumn) {
|
|
@@ -228,60 +787,45 @@ function layoutChildren(ctx, parent) {
|
|
|
228
787
|
childLayout.width = flexBasis;
|
|
229
788
|
}
|
|
230
789
|
}
|
|
231
|
-
childInfo.push({
|
|
232
|
-
width: childLayout.width,
|
|
233
|
-
height: childLayout.height,
|
|
234
|
-
flexGrow,
|
|
235
|
-
flexShrink,
|
|
236
|
-
flexBasis,
|
|
237
|
-
});
|
|
790
|
+
childInfo.push({ width: childLayout.width, height: childLayout.height, flexGrow, flexShrink, flexBasis });
|
|
238
791
|
totalMainSize += mainSize;
|
|
239
792
|
totalFlexGrow += flexGrow;
|
|
240
793
|
totalFlexShrink += flexShrink;
|
|
241
794
|
}
|
|
242
|
-
// Add gaps
|
|
243
795
|
const totalGap = gap * (parent.children.length - 1);
|
|
244
796
|
totalMainSize += totalGap;
|
|
245
|
-
// Apply flex grow/shrink
|
|
246
797
|
const availableMain = isColumn ? availableHeight : availableWidth;
|
|
247
798
|
let remainingSpace = availableMain - totalMainSize;
|
|
248
|
-
// Distribute remaining space to flex-grow children
|
|
249
799
|
if (remainingSpace > 0 && totalFlexGrow > 0) {
|
|
250
800
|
const spacePerFlex = remainingSpace / totalFlexGrow;
|
|
251
801
|
for (let i = 0; i < parent.children.length; i++) {
|
|
252
802
|
const info = childInfo[i];
|
|
253
803
|
if (info.flexGrow > 0) {
|
|
254
804
|
const extraSpace = spacePerFlex * info.flexGrow;
|
|
255
|
-
if (isColumn)
|
|
805
|
+
if (isColumn)
|
|
256
806
|
info.height += extraSpace;
|
|
257
|
-
|
|
258
|
-
else {
|
|
807
|
+
else
|
|
259
808
|
info.width += extraSpace;
|
|
260
|
-
}
|
|
261
809
|
totalMainSize += extraSpace;
|
|
262
810
|
}
|
|
263
811
|
}
|
|
264
812
|
remainingSpace = 0;
|
|
265
813
|
}
|
|
266
|
-
// Apply flex shrink if needed (content is too large)
|
|
267
814
|
if (remainingSpace < 0 && totalFlexShrink > 0) {
|
|
268
815
|
const shrinkPerFlex = Math.abs(remainingSpace) / totalFlexShrink;
|
|
269
816
|
for (let i = 0; i < parent.children.length; i++) {
|
|
270
817
|
const info = childInfo[i];
|
|
271
818
|
if (info.flexShrink > 0) {
|
|
272
819
|
const shrinkSpace = Math.min(shrinkPerFlex * info.flexShrink, isColumn ? info.height : info.width);
|
|
273
|
-
if (isColumn)
|
|
820
|
+
if (isColumn)
|
|
274
821
|
info.height = Math.max(0, info.height - shrinkSpace);
|
|
275
|
-
|
|
276
|
-
else {
|
|
822
|
+
else
|
|
277
823
|
info.width = Math.max(0, info.width - shrinkSpace);
|
|
278
|
-
}
|
|
279
824
|
totalMainSize -= shrinkSpace;
|
|
280
825
|
}
|
|
281
826
|
}
|
|
282
827
|
remainingSpace = availableMain - totalMainSize;
|
|
283
828
|
}
|
|
284
|
-
// Calculate starting position based on justifyContent
|
|
285
829
|
let mainStart = 0;
|
|
286
830
|
let spacing = 0;
|
|
287
831
|
if (justifyContent === "center") {
|
|
@@ -297,16 +841,13 @@ function layoutChildren(ctx, parent) {
|
|
|
297
841
|
spacing = remainingSpace / parent.children.length;
|
|
298
842
|
mainStart = spacing / 2;
|
|
299
843
|
}
|
|
300
|
-
// Position children
|
|
301
844
|
let currentMain = mainStart;
|
|
302
845
|
for (let i = 0; i < parent.children.length; i++) {
|
|
303
846
|
const child = parent.children[i];
|
|
304
847
|
const childLayout = child.layout;
|
|
305
848
|
const info = childInfo[i];
|
|
306
|
-
// Update child layout with flex-adjusted size
|
|
307
849
|
childLayout.width = info.width;
|
|
308
850
|
childLayout.height = info.height;
|
|
309
|
-
// Calculate cross position based on alignItems
|
|
310
851
|
let crossStart = 0;
|
|
311
852
|
const availableCross = isColumn ? availableWidth : availableHeight;
|
|
312
853
|
const childCross = isColumn ? info.width : info.height;
|
|
@@ -316,7 +857,6 @@ function layoutChildren(ctx, parent) {
|
|
|
316
857
|
else if (alignItems === "flex-end") {
|
|
317
858
|
crossStart = availableCross - childCross;
|
|
318
859
|
}
|
|
319
|
-
// Update child position relative to parent content area
|
|
320
860
|
if (isColumn) {
|
|
321
861
|
childLayout.x = layout.x + layout.contentX + crossStart;
|
|
322
862
|
childLayout.y = layout.y + layout.contentY + currentMain;
|
|
@@ -332,40 +872,30 @@ function layoutChildren(ctx, parent) {
|
|
|
332
872
|
}
|
|
333
873
|
}
|
|
334
874
|
}
|
|
335
|
-
|
|
336
|
-
* Layout Stack children - overlays children on top of each other
|
|
337
|
-
*/
|
|
338
|
-
function layoutStackChildren(ctx, parent) {
|
|
875
|
+
function layoutStackChildrenFallback(ctx, parent) {
|
|
339
876
|
const layout = parent.layout;
|
|
340
877
|
const props = parent.props;
|
|
341
|
-
// Stack uses horizontalAlignment/verticalAlignment directly
|
|
342
878
|
const horizontalAlignment = props.horizontalAlignment || "flex-start";
|
|
343
879
|
const verticalAlignment = props.verticalAlignment || "flex-start";
|
|
344
880
|
const availableWidth = layout.contentWidth;
|
|
345
881
|
const availableHeight = layout.contentHeight;
|
|
346
|
-
// Layout all children at the same position (stacked on top of each other)
|
|
347
882
|
for (const child of parent.children) {
|
|
348
|
-
|
|
349
|
-
computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
|
|
883
|
+
computeLayoutFallback(ctx, child, availableWidth, availableHeight, 0, 0);
|
|
350
884
|
const childLayout = child.layout;
|
|
351
|
-
// Calculate position based on alignment
|
|
352
885
|
let x = 0;
|
|
353
886
|
let y = 0;
|
|
354
|
-
// Horizontal alignment
|
|
355
887
|
if (horizontalAlignment === "center") {
|
|
356
888
|
x = (availableWidth - childLayout.width) / 2;
|
|
357
889
|
}
|
|
358
890
|
else if (horizontalAlignment === "flex-end") {
|
|
359
891
|
x = availableWidth - childLayout.width;
|
|
360
892
|
}
|
|
361
|
-
// Vertical alignment
|
|
362
893
|
if (verticalAlignment === "center") {
|
|
363
894
|
y = (availableHeight - childLayout.height) / 2;
|
|
364
895
|
}
|
|
365
896
|
else if (verticalAlignment === "flex-end") {
|
|
366
897
|
y = availableHeight - childLayout.height;
|
|
367
898
|
}
|
|
368
|
-
// Update child position relative to parent content area
|
|
369
899
|
childLayout.x = layout.x + layout.contentX + x;
|
|
370
900
|
childLayout.y = layout.y + layout.contentY + y;
|
|
371
901
|
}
|