@simonklee/yoga 0.2.24
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 +87 -0
- package/dist/darwin-arm64/libyoga.dylib +0 -0
- package/dist/darwin-x64/libyoga.dylib +0 -0
- package/dist/index.d.ts +575 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1502 -0
- package/dist/linux-arm64/libyoga.so +0 -0
- package/dist/linux-x64/libyoga.so +0 -0
- package/dist/windows-x64/yoga.dll +0 -0
- package/package.json +43 -0
- package/src/index.test.ts +939 -0
- package/src/index.ts +1648 -0
- package/src/yoga_ffi.zig +1324 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test";
|
|
2
|
+
import Yoga, {
|
|
3
|
+
Node,
|
|
4
|
+
Config,
|
|
5
|
+
MeasureMode,
|
|
6
|
+
Direction,
|
|
7
|
+
FlexDirection,
|
|
8
|
+
Edge,
|
|
9
|
+
Align,
|
|
10
|
+
BoxSizing,
|
|
11
|
+
Errata,
|
|
12
|
+
ExperimentalFeature,
|
|
13
|
+
Gutter,
|
|
14
|
+
Unit,
|
|
15
|
+
} from "./index";
|
|
16
|
+
|
|
17
|
+
describe("Node", () => {
|
|
18
|
+
test("create and free", () => {
|
|
19
|
+
const node = Node.create();
|
|
20
|
+
expect(node).toBeDefined();
|
|
21
|
+
node.free();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("create with config", () => {
|
|
25
|
+
const config = Config.create();
|
|
26
|
+
const node = Node.create(config);
|
|
27
|
+
expect(node).toBeDefined();
|
|
28
|
+
node.free();
|
|
29
|
+
config.free();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("basic layout", () => {
|
|
33
|
+
const root = Node.create();
|
|
34
|
+
root.setWidth(100);
|
|
35
|
+
root.setHeight(100);
|
|
36
|
+
root.setFlexDirection(FlexDirection.Row);
|
|
37
|
+
|
|
38
|
+
const child1 = Node.create();
|
|
39
|
+
child1.setFlexGrow(1);
|
|
40
|
+
root.insertChild(child1, 0);
|
|
41
|
+
|
|
42
|
+
const child2 = Node.create();
|
|
43
|
+
child2.setFlexGrow(1);
|
|
44
|
+
root.insertChild(child2, 1);
|
|
45
|
+
|
|
46
|
+
root.calculateLayout(100, 100, Direction.LTR);
|
|
47
|
+
|
|
48
|
+
const layout = root.getComputedLayout();
|
|
49
|
+
expect(layout.width).toBe(100);
|
|
50
|
+
expect(layout.height).toBe(100);
|
|
51
|
+
|
|
52
|
+
expect(child1.getComputedWidth()).toBe(50);
|
|
53
|
+
expect(child2.getComputedWidth()).toBe(50);
|
|
54
|
+
|
|
55
|
+
root.freeRecursive();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("MeasureFunc", () => {
|
|
60
|
+
test("setMeasureFunc and hasMeasureFunc", () => {
|
|
61
|
+
const node = Node.create();
|
|
62
|
+
|
|
63
|
+
expect(node.hasMeasureFunc()).toBe(false);
|
|
64
|
+
|
|
65
|
+
node.setMeasureFunc((width, widthMode, height, heightMode) => {
|
|
66
|
+
return { width: 100, height: 50 };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(node.hasMeasureFunc()).toBe(true);
|
|
70
|
+
|
|
71
|
+
node.unsetMeasureFunc();
|
|
72
|
+
expect(node.hasMeasureFunc()).toBe(false);
|
|
73
|
+
|
|
74
|
+
node.free();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("measure function is called during layout", () => {
|
|
78
|
+
const root = Node.create();
|
|
79
|
+
root.setWidth(200);
|
|
80
|
+
root.setHeight(200);
|
|
81
|
+
|
|
82
|
+
const child = Node.create();
|
|
83
|
+
// Prevent default stretch behavior so child uses measured size
|
|
84
|
+
child.setAlignSelf(Align.FlexStart);
|
|
85
|
+
let measureCalled = false;
|
|
86
|
+
|
|
87
|
+
child.setMeasureFunc((width, widthMode, height, heightMode) => {
|
|
88
|
+
measureCalled = true;
|
|
89
|
+
return { width: 50, height: 25 };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
root.insertChild(child, 0);
|
|
93
|
+
root.calculateLayout(200, 200, Direction.LTR);
|
|
94
|
+
|
|
95
|
+
expect(measureCalled).toBe(true);
|
|
96
|
+
expect(child.getComputedWidth()).toBe(50);
|
|
97
|
+
expect(child.getComputedHeight()).toBe(25);
|
|
98
|
+
|
|
99
|
+
root.freeRecursive();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("measure function receives correct modes", () => {
|
|
103
|
+
const root = Node.create();
|
|
104
|
+
root.setWidth(100);
|
|
105
|
+
root.setFlexDirection(FlexDirection.Row);
|
|
106
|
+
|
|
107
|
+
const child = Node.create();
|
|
108
|
+
// Don't use flexGrow - let child determine its own size via measure
|
|
109
|
+
child.setAlignSelf(Align.FlexStart);
|
|
110
|
+
|
|
111
|
+
let receivedWidthMode = -1;
|
|
112
|
+
let receivedHeightMode = -1;
|
|
113
|
+
|
|
114
|
+
child.setMeasureFunc((width, widthMode, height, heightMode) => {
|
|
115
|
+
receivedWidthMode = widthMode;
|
|
116
|
+
receivedHeightMode = heightMode;
|
|
117
|
+
return { width: 50, height: 30 };
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
root.insertChild(child, 0);
|
|
121
|
+
root.calculateLayout(100, undefined, Direction.LTR);
|
|
122
|
+
|
|
123
|
+
// Width should be AtMost because parent has fixed width
|
|
124
|
+
expect(receivedWidthMode).toBe(MeasureMode.AtMost);
|
|
125
|
+
// Height should be Undefined because no height constraint
|
|
126
|
+
expect(receivedHeightMode).toBe(MeasureMode.Undefined);
|
|
127
|
+
|
|
128
|
+
root.freeRecursive();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("DirtiedFunc", () => {
|
|
133
|
+
test("setDirtiedFunc callback is called", () => {
|
|
134
|
+
const root = Node.create();
|
|
135
|
+
root.setWidth(100);
|
|
136
|
+
root.setHeight(100);
|
|
137
|
+
|
|
138
|
+
let dirtiedCalled = false;
|
|
139
|
+
|
|
140
|
+
// Only nodes with measure function can be marked dirty
|
|
141
|
+
root.setMeasureFunc(() => ({ width: 100, height: 100 }));
|
|
142
|
+
|
|
143
|
+
// Must calculate layout first so node becomes "clean"
|
|
144
|
+
// Dirtied callback only fires when transitioning from clean to dirty
|
|
145
|
+
root.calculateLayout(100, 100, Direction.LTR);
|
|
146
|
+
|
|
147
|
+
root.setDirtiedFunc(() => {
|
|
148
|
+
dirtiedCalled = true;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
root.markDirty();
|
|
152
|
+
|
|
153
|
+
expect(dirtiedCalled).toBe(true);
|
|
154
|
+
|
|
155
|
+
root.free();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("hasDirtiedFunc reflects callback state", () => {
|
|
159
|
+
const node = Node.create();
|
|
160
|
+
expect(node.hasDirtiedFunc()).toBe(false);
|
|
161
|
+
|
|
162
|
+
node.setMeasureFunc(() => ({ width: 10, height: 10 }));
|
|
163
|
+
node.setDirtiedFunc(() => {});
|
|
164
|
+
expect(node.hasDirtiedFunc()).toBe(true);
|
|
165
|
+
|
|
166
|
+
node.unsetDirtiedFunc();
|
|
167
|
+
expect(node.hasDirtiedFunc()).toBe(false);
|
|
168
|
+
|
|
169
|
+
node.free();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("Margins, Padding, Border", () => {
|
|
174
|
+
test("margins affect layout", () => {
|
|
175
|
+
const root = Node.create();
|
|
176
|
+
root.setWidth(100);
|
|
177
|
+
root.setHeight(100);
|
|
178
|
+
root.setFlexDirection(FlexDirection.Column);
|
|
179
|
+
|
|
180
|
+
const child = Node.create();
|
|
181
|
+
child.setWidth(50);
|
|
182
|
+
child.setHeight(50);
|
|
183
|
+
child.setMargin(Edge.Left, 10);
|
|
184
|
+
child.setMargin(Edge.Top, 5);
|
|
185
|
+
|
|
186
|
+
root.insertChild(child, 0);
|
|
187
|
+
root.calculateLayout(100, 100, Direction.LTR);
|
|
188
|
+
|
|
189
|
+
expect(child.getComputedLeft()).toBe(10);
|
|
190
|
+
expect(child.getComputedTop()).toBe(5);
|
|
191
|
+
|
|
192
|
+
root.freeRecursive();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("padding affects children", () => {
|
|
196
|
+
const root = Node.create();
|
|
197
|
+
root.setWidth(100);
|
|
198
|
+
root.setHeight(100);
|
|
199
|
+
root.setPadding(Edge.All, 10);
|
|
200
|
+
|
|
201
|
+
const child = Node.create();
|
|
202
|
+
child.setWidth(50);
|
|
203
|
+
child.setHeight(50);
|
|
204
|
+
|
|
205
|
+
root.insertChild(child, 0);
|
|
206
|
+
root.calculateLayout(100, 100, Direction.LTR);
|
|
207
|
+
|
|
208
|
+
expect(child.getComputedLeft()).toBe(10);
|
|
209
|
+
expect(child.getComputedTop()).toBe(10);
|
|
210
|
+
|
|
211
|
+
root.freeRecursive();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("border affects layout", () => {
|
|
215
|
+
const root = Node.create();
|
|
216
|
+
root.setWidth(100);
|
|
217
|
+
root.setHeight(100);
|
|
218
|
+
root.setBorder(Edge.All, 5);
|
|
219
|
+
|
|
220
|
+
const child = Node.create();
|
|
221
|
+
child.setFlexGrow(1);
|
|
222
|
+
|
|
223
|
+
root.insertChild(child, 0);
|
|
224
|
+
root.calculateLayout(100, 100, Direction.LTR);
|
|
225
|
+
|
|
226
|
+
// Child should be offset by border
|
|
227
|
+
expect(child.getComputedLeft()).toBe(5);
|
|
228
|
+
expect(child.getComputedTop()).toBe(5);
|
|
229
|
+
// Child should be smaller due to borders
|
|
230
|
+
expect(child.getComputedWidth()).toBe(90);
|
|
231
|
+
expect(child.getComputedHeight()).toBe(90);
|
|
232
|
+
|
|
233
|
+
root.freeRecursive();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("Max/Fit/Stretch units", () => {
|
|
238
|
+
test("Unit includes max/fit/stretch", () => {
|
|
239
|
+
expect(Unit.MaxContent).toBe(4);
|
|
240
|
+
expect(Unit.FitContent).toBe(5);
|
|
241
|
+
expect(Unit.Stretch).toBe(6);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("setters update value units", () => {
|
|
245
|
+
const node = Node.create();
|
|
246
|
+
|
|
247
|
+
const cases = [
|
|
248
|
+
{ set: () => node.setFlexBasisMaxContent(), get: () => node.getFlexBasis(), unit: Unit.MaxContent },
|
|
249
|
+
{ set: () => node.setFlexBasisFitContent(), get: () => node.getFlexBasis(), unit: Unit.FitContent },
|
|
250
|
+
{ set: () => node.setFlexBasisStretch(), get: () => node.getFlexBasis(), unit: Unit.Stretch },
|
|
251
|
+
{ set: () => node.setWidthMaxContent(), get: () => node.getWidth(), unit: Unit.MaxContent },
|
|
252
|
+
{ set: () => node.setWidthFitContent(), get: () => node.getWidth(), unit: Unit.FitContent },
|
|
253
|
+
{ set: () => node.setWidthStretch(), get: () => node.getWidth(), unit: Unit.Stretch },
|
|
254
|
+
{ set: () => node.setHeightMaxContent(), get: () => node.getHeight(), unit: Unit.MaxContent },
|
|
255
|
+
{ set: () => node.setHeightFitContent(), get: () => node.getHeight(), unit: Unit.FitContent },
|
|
256
|
+
{ set: () => node.setHeightStretch(), get: () => node.getHeight(), unit: Unit.Stretch },
|
|
257
|
+
{ set: () => node.setMinWidthMaxContent(), get: () => node.getMinWidth(), unit: Unit.MaxContent },
|
|
258
|
+
{ set: () => node.setMinWidthFitContent(), get: () => node.getMinWidth(), unit: Unit.FitContent },
|
|
259
|
+
{ set: () => node.setMinWidthStretch(), get: () => node.getMinWidth(), unit: Unit.Stretch },
|
|
260
|
+
{ set: () => node.setMinHeightMaxContent(), get: () => node.getMinHeight(), unit: Unit.MaxContent },
|
|
261
|
+
{ set: () => node.setMinHeightFitContent(), get: () => node.getMinHeight(), unit: Unit.FitContent },
|
|
262
|
+
{ set: () => node.setMinHeightStretch(), get: () => node.getMinHeight(), unit: Unit.Stretch },
|
|
263
|
+
{ set: () => node.setMaxWidthMaxContent(), get: () => node.getMaxWidth(), unit: Unit.MaxContent },
|
|
264
|
+
{ set: () => node.setMaxWidthFitContent(), get: () => node.getMaxWidth(), unit: Unit.FitContent },
|
|
265
|
+
{ set: () => node.setMaxWidthStretch(), get: () => node.getMaxWidth(), unit: Unit.Stretch },
|
|
266
|
+
{ set: () => node.setMaxHeightMaxContent(), get: () => node.getMaxHeight(), unit: Unit.MaxContent },
|
|
267
|
+
{ set: () => node.setMaxHeightFitContent(), get: () => node.getMaxHeight(), unit: Unit.FitContent },
|
|
268
|
+
{ set: () => node.setMaxHeightStretch(), get: () => node.getMaxHeight(), unit: Unit.Stretch },
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
for (const { set, get, unit } of cases) {
|
|
272
|
+
set();
|
|
273
|
+
expect(get().unit).toBe(unit);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
node.free();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("BaselineFunc", () => {
|
|
281
|
+
test("setBaselineFunc callback affects layout", () => {
|
|
282
|
+
const root = Node.create();
|
|
283
|
+
root.setWidth(200);
|
|
284
|
+
root.setHeight(100);
|
|
285
|
+
root.setFlexDirection(FlexDirection.Row);
|
|
286
|
+
root.setAlignItems(Align.Baseline);
|
|
287
|
+
|
|
288
|
+
const child1 = Node.create();
|
|
289
|
+
child1.setWidth(50);
|
|
290
|
+
child1.setHeight(40);
|
|
291
|
+
let baselineCalled = false;
|
|
292
|
+
child1.setBaselineFunc((width, height) => {
|
|
293
|
+
baselineCalled = true;
|
|
294
|
+
return 30; // baseline at 30px from top
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const child2 = Node.create();
|
|
298
|
+
child2.setWidth(50);
|
|
299
|
+
child2.setHeight(60);
|
|
300
|
+
|
|
301
|
+
root.insertChild(child1, 0);
|
|
302
|
+
root.insertChild(child2, 1);
|
|
303
|
+
root.calculateLayout(200, 100, Direction.LTR);
|
|
304
|
+
|
|
305
|
+
expect(baselineCalled).toBe(true);
|
|
306
|
+
// child1 should be offset down to align its baseline (30px) with child2's baseline
|
|
307
|
+
expect(child1.getComputedTop()).toBe(30);
|
|
308
|
+
expect(child2.getComputedTop()).toBe(0);
|
|
309
|
+
|
|
310
|
+
root.freeRecursive();
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("Gap", () => {
|
|
315
|
+
test("row gap", () => {
|
|
316
|
+
const root = Node.create();
|
|
317
|
+
root.setWidth(100);
|
|
318
|
+
root.setHeight(100);
|
|
319
|
+
root.setFlexDirection(FlexDirection.Column);
|
|
320
|
+
root.setGap(Gutter.Row, 10);
|
|
321
|
+
|
|
322
|
+
const child1 = Node.create();
|
|
323
|
+
child1.setHeight(20);
|
|
324
|
+
root.insertChild(child1, 0);
|
|
325
|
+
|
|
326
|
+
const child2 = Node.create();
|
|
327
|
+
child2.setHeight(20);
|
|
328
|
+
root.insertChild(child2, 1);
|
|
329
|
+
|
|
330
|
+
root.calculateLayout(100, 100, Direction.LTR);
|
|
331
|
+
|
|
332
|
+
expect(child1.getComputedTop()).toBe(0);
|
|
333
|
+
expect(child2.getComputedTop()).toBe(30); // 20 + 10 gap
|
|
334
|
+
|
|
335
|
+
root.freeRecursive();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe("Percent values", () => {
|
|
340
|
+
test("setWidth with percent string", () => {
|
|
341
|
+
const root = Node.create();
|
|
342
|
+
root.setWidth(200);
|
|
343
|
+
root.setHeight(100);
|
|
344
|
+
|
|
345
|
+
const child = Node.create();
|
|
346
|
+
child.setWidth("50%");
|
|
347
|
+
child.setHeight(50);
|
|
348
|
+
|
|
349
|
+
root.insertChild(child, 0);
|
|
350
|
+
root.calculateLayout(200, 100, Direction.LTR);
|
|
351
|
+
|
|
352
|
+
expect(child.getComputedWidth()).toBe(100); // 50% of 200
|
|
353
|
+
|
|
354
|
+
root.freeRecursive();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("setHeight with percent string", () => {
|
|
358
|
+
const root = Node.create();
|
|
359
|
+
root.setWidth(100);
|
|
360
|
+
root.setHeight(200);
|
|
361
|
+
|
|
362
|
+
const child = Node.create();
|
|
363
|
+
child.setWidth(50);
|
|
364
|
+
child.setHeight("25%");
|
|
365
|
+
|
|
366
|
+
root.insertChild(child, 0);
|
|
367
|
+
root.calculateLayout(100, 200, Direction.LTR);
|
|
368
|
+
|
|
369
|
+
expect(child.getComputedHeight()).toBe(50); // 25% of 200
|
|
370
|
+
|
|
371
|
+
root.freeRecursive();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("setMargin with auto", () => {
|
|
375
|
+
const root = Node.create();
|
|
376
|
+
root.setWidth(200);
|
|
377
|
+
root.setHeight(100);
|
|
378
|
+
root.setFlexDirection(FlexDirection.Row);
|
|
379
|
+
root.setJustifyContent(Yoga.Justify.FlexStart);
|
|
380
|
+
|
|
381
|
+
const child = Node.create();
|
|
382
|
+
child.setWidth(50);
|
|
383
|
+
child.setHeight(50);
|
|
384
|
+
child.setMargin(Edge.Left, "auto");
|
|
385
|
+
|
|
386
|
+
root.insertChild(child, 0);
|
|
387
|
+
root.calculateLayout(200, 100, Direction.LTR);
|
|
388
|
+
|
|
389
|
+
// With auto margin, child should be pushed to the right
|
|
390
|
+
expect(child.getComputedLeft()).toBe(150); // 200 - 50
|
|
391
|
+
|
|
392
|
+
root.freeRecursive();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("setMargin with percent string", () => {
|
|
396
|
+
const root = Node.create();
|
|
397
|
+
root.setWidth(200);
|
|
398
|
+
root.setHeight(100);
|
|
399
|
+
|
|
400
|
+
const child = Node.create();
|
|
401
|
+
child.setWidth(50);
|
|
402
|
+
child.setHeight(50);
|
|
403
|
+
child.setMargin(Edge.Left, "10%");
|
|
404
|
+
|
|
405
|
+
root.insertChild(child, 0);
|
|
406
|
+
root.calculateLayout(200, 100, Direction.LTR);
|
|
407
|
+
|
|
408
|
+
expect(child.getComputedLeft()).toBe(20); // 10% of 200
|
|
409
|
+
|
|
410
|
+
root.freeRecursive();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("setPadding with percent string", () => {
|
|
414
|
+
const root = Node.create();
|
|
415
|
+
root.setWidth(200);
|
|
416
|
+
root.setHeight(100);
|
|
417
|
+
root.setPadding(Edge.All, "10%");
|
|
418
|
+
|
|
419
|
+
const child = Node.create();
|
|
420
|
+
child.setFlexGrow(1);
|
|
421
|
+
|
|
422
|
+
root.insertChild(child, 0);
|
|
423
|
+
root.calculateLayout(200, 100, Direction.LTR);
|
|
424
|
+
|
|
425
|
+
// Padding percent is based on width dimension in CSS/Yoga
|
|
426
|
+
// 10% of 200 = 20 on all sides
|
|
427
|
+
expect(child.getComputedLeft()).toBe(20);
|
|
428
|
+
expect(child.getComputedTop()).toBe(20);
|
|
429
|
+
expect(child.getComputedWidth()).toBe(160); // 200 - 20 - 20
|
|
430
|
+
expect(child.getComputedHeight()).toBe(60); // 100 - 20 - 20
|
|
431
|
+
|
|
432
|
+
root.freeRecursive();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("setFlexBasis with percent string", () => {
|
|
436
|
+
const root = Node.create();
|
|
437
|
+
root.setWidth(200);
|
|
438
|
+
root.setHeight(100);
|
|
439
|
+
root.setFlexDirection(FlexDirection.Row);
|
|
440
|
+
|
|
441
|
+
const child = Node.create();
|
|
442
|
+
child.setFlexBasis("50%");
|
|
443
|
+
|
|
444
|
+
root.insertChild(child, 0);
|
|
445
|
+
root.calculateLayout(200, 100, Direction.LTR);
|
|
446
|
+
|
|
447
|
+
expect(child.getComputedWidth()).toBe(100); // 50% of 200
|
|
448
|
+
|
|
449
|
+
root.freeRecursive();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("setGap with percent string", () => {
|
|
453
|
+
const root = Node.create();
|
|
454
|
+
root.setWidth(100);
|
|
455
|
+
root.setHeight(200);
|
|
456
|
+
root.setFlexDirection(FlexDirection.Column);
|
|
457
|
+
root.setGap(Gutter.Row, "10%");
|
|
458
|
+
|
|
459
|
+
const child1 = Node.create();
|
|
460
|
+
child1.setHeight(50);
|
|
461
|
+
root.insertChild(child1, 0);
|
|
462
|
+
|
|
463
|
+
const child2 = Node.create();
|
|
464
|
+
child2.setHeight(50);
|
|
465
|
+
root.insertChild(child2, 1);
|
|
466
|
+
|
|
467
|
+
root.calculateLayout(100, 200, Direction.LTR);
|
|
468
|
+
|
|
469
|
+
expect(child1.getComputedTop()).toBe(0);
|
|
470
|
+
expect(child2.getComputedTop()).toBe(70); // 50 + 10% of 200 = 50 + 20
|
|
471
|
+
|
|
472
|
+
root.freeRecursive();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("setMinWidth and setMaxWidth with percent", () => {
|
|
476
|
+
const root = Node.create();
|
|
477
|
+
root.setWidth(200);
|
|
478
|
+
root.setHeight(100);
|
|
479
|
+
|
|
480
|
+
const child = Node.create();
|
|
481
|
+
child.setMinWidth("25%");
|
|
482
|
+
child.setMaxWidth("75%");
|
|
483
|
+
child.setWidth("100%");
|
|
484
|
+
child.setHeight(50);
|
|
485
|
+
|
|
486
|
+
root.insertChild(child, 0);
|
|
487
|
+
root.calculateLayout(200, 100, Direction.LTR);
|
|
488
|
+
|
|
489
|
+
// Width should be capped at maxWidth (75% of 200 = 150)
|
|
490
|
+
expect(child.getComputedWidth()).toBe(150);
|
|
491
|
+
|
|
492
|
+
root.freeRecursive();
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe("New API methods", () => {
|
|
497
|
+
test("copyStyle", () => {
|
|
498
|
+
const node1 = Node.create();
|
|
499
|
+
node1.setWidth(100);
|
|
500
|
+
node1.setHeight(200);
|
|
501
|
+
node1.setFlexDirection(FlexDirection.Row);
|
|
502
|
+
|
|
503
|
+
const node2 = Node.create();
|
|
504
|
+
node2.copyStyle(node1);
|
|
505
|
+
|
|
506
|
+
expect(node2.getFlexDirection()).toBe(FlexDirection.Row);
|
|
507
|
+
|
|
508
|
+
node1.free();
|
|
509
|
+
node2.free();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("setBoxSizing and getBoxSizing", () => {
|
|
513
|
+
const node = Node.create();
|
|
514
|
+
|
|
515
|
+
node.setBoxSizing(BoxSizing.ContentBox);
|
|
516
|
+
expect(node.getBoxSizing()).toBe(BoxSizing.ContentBox);
|
|
517
|
+
|
|
518
|
+
node.setBoxSizing(BoxSizing.BorderBox);
|
|
519
|
+
expect(node.getBoxSizing()).toBe(BoxSizing.BorderBox);
|
|
520
|
+
|
|
521
|
+
node.free();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("setIsReferenceBaseline and isReferenceBaseline", () => {
|
|
525
|
+
const node = Node.create();
|
|
526
|
+
|
|
527
|
+
expect(node.isReferenceBaseline()).toBe(false);
|
|
528
|
+
|
|
529
|
+
node.setIsReferenceBaseline(true);
|
|
530
|
+
expect(node.isReferenceBaseline()).toBe(true);
|
|
531
|
+
|
|
532
|
+
node.setIsReferenceBaseline(false);
|
|
533
|
+
expect(node.isReferenceBaseline()).toBe(false);
|
|
534
|
+
|
|
535
|
+
node.free();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("setAlwaysFormsContainingBlock", () => {
|
|
539
|
+
const node = Node.create();
|
|
540
|
+
// Just verify it doesn't throw
|
|
541
|
+
node.setAlwaysFormsContainingBlock(true);
|
|
542
|
+
node.setAlwaysFormsContainingBlock(false);
|
|
543
|
+
node.free();
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("Value getters", () => {
|
|
548
|
+
test("getWidth returns Value with unit and value", () => {
|
|
549
|
+
const node = Node.create();
|
|
550
|
+
|
|
551
|
+
node.setWidth(100);
|
|
552
|
+
const width = node.getWidth();
|
|
553
|
+
expect(width.unit).toBe(Yoga.Unit.Point);
|
|
554
|
+
expect(width.value).toBe(100);
|
|
555
|
+
|
|
556
|
+
node.setWidth("50%");
|
|
557
|
+
const widthPercent = node.getWidth();
|
|
558
|
+
expect(widthPercent.unit).toBe(Yoga.Unit.Percent);
|
|
559
|
+
expect(widthPercent.value).toBe(50);
|
|
560
|
+
|
|
561
|
+
node.setWidth("auto");
|
|
562
|
+
const widthAuto = node.getWidth();
|
|
563
|
+
expect(widthAuto.unit).toBe(Yoga.Unit.Auto);
|
|
564
|
+
|
|
565
|
+
node.free();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("getMargin returns Value with unit and value", () => {
|
|
569
|
+
const node = Node.create();
|
|
570
|
+
|
|
571
|
+
node.setMargin(Edge.Left, 20);
|
|
572
|
+
const margin = node.getMargin(Edge.Left);
|
|
573
|
+
expect(margin.unit).toBe(Yoga.Unit.Point);
|
|
574
|
+
expect(margin.value).toBe(20);
|
|
575
|
+
|
|
576
|
+
node.setMargin(Edge.Top, "10%");
|
|
577
|
+
const marginPercent = node.getMargin(Edge.Top);
|
|
578
|
+
expect(marginPercent.unit).toBe(Yoga.Unit.Percent);
|
|
579
|
+
expect(marginPercent.value).toBe(10);
|
|
580
|
+
|
|
581
|
+
node.free();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("getFlexBasis returns Value", () => {
|
|
585
|
+
const node = Node.create();
|
|
586
|
+
|
|
587
|
+
node.setFlexBasis(50);
|
|
588
|
+
const basis = node.getFlexBasis();
|
|
589
|
+
expect(basis.unit).toBe(Yoga.Unit.Point);
|
|
590
|
+
expect(basis.value).toBe(50);
|
|
591
|
+
|
|
592
|
+
node.setFlexBasis("auto");
|
|
593
|
+
const basisAuto = node.getFlexBasis();
|
|
594
|
+
expect(basisAuto.unit).toBe(Yoga.Unit.Auto);
|
|
595
|
+
|
|
596
|
+
node.free();
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
describe("DirtiedFunction signature", () => {
|
|
601
|
+
test("dirtiedFunc receives node as parameter", () => {
|
|
602
|
+
const root = Node.create();
|
|
603
|
+
root.setWidth(100);
|
|
604
|
+
root.setHeight(100);
|
|
605
|
+
|
|
606
|
+
let receivedNode: Node | undefined = undefined;
|
|
607
|
+
|
|
608
|
+
root.setMeasureFunc(() => ({ width: 100, height: 100 }));
|
|
609
|
+
root.calculateLayout(100, 100, Direction.LTR);
|
|
610
|
+
|
|
611
|
+
root.setDirtiedFunc((node) => {
|
|
612
|
+
receivedNode = node;
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
root.markDirty();
|
|
616
|
+
|
|
617
|
+
expect(receivedNode).toBeDefined();
|
|
618
|
+
expect(receivedNode === root).toBe(true);
|
|
619
|
+
|
|
620
|
+
root.free();
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
describe("Config", () => {
|
|
625
|
+
test("errata settings", () => {
|
|
626
|
+
const config = Config.create();
|
|
627
|
+
|
|
628
|
+
config.setErrata(Errata.Classic);
|
|
629
|
+
expect(config.getErrata()).toBe(Errata.Classic);
|
|
630
|
+
|
|
631
|
+
config.setErrata(Errata.None);
|
|
632
|
+
expect(config.getErrata()).toBe(Errata.None);
|
|
633
|
+
|
|
634
|
+
config.free();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("experimental features", () => {
|
|
638
|
+
const config = Config.create();
|
|
639
|
+
|
|
640
|
+
expect(config.isExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis)).toBe(false);
|
|
641
|
+
|
|
642
|
+
config.setExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis, true);
|
|
643
|
+
expect(config.isExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis)).toBe(true);
|
|
644
|
+
|
|
645
|
+
config.setExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis, false);
|
|
646
|
+
expect(config.isExperimentalFeatureEnabled(ExperimentalFeature.WebFlexBasis)).toBe(false);
|
|
647
|
+
|
|
648
|
+
config.free();
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
describe("Type exports", () => {
|
|
653
|
+
test("enum types can be used as types", () => {
|
|
654
|
+
// This test verifies that the type exports work correctly
|
|
655
|
+
// If the types weren't exported, this wouldn't compile
|
|
656
|
+
const align: Align = Align.Center;
|
|
657
|
+
const direction: Direction = Direction.LTR;
|
|
658
|
+
const edge: Edge = Edge.All;
|
|
659
|
+
const flexDir: FlexDirection = FlexDirection.Row;
|
|
660
|
+
const boxSizing: BoxSizing = BoxSizing.BorderBox;
|
|
661
|
+
const errata: Errata = Errata.None;
|
|
662
|
+
|
|
663
|
+
expect(align).toBe(2);
|
|
664
|
+
expect(direction).toBe(1);
|
|
665
|
+
expect(edge).toBe(8);
|
|
666
|
+
expect(flexDir).toBe(2);
|
|
667
|
+
expect(boxSizing).toBe(0);
|
|
668
|
+
expect(errata).toBe(0);
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
describe("Use-after-free protection", () => {
|
|
673
|
+
test("isFreed returns false before free", () => {
|
|
674
|
+
const node = Node.create();
|
|
675
|
+
expect(node.isFreed()).toBe(false);
|
|
676
|
+
node.free();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("isFreed returns true after free", () => {
|
|
680
|
+
const node = Node.create();
|
|
681
|
+
node.free();
|
|
682
|
+
expect(node.isFreed()).toBe(true);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("isFreed returns true after freeRecursive", () => {
|
|
686
|
+
const root = Node.create();
|
|
687
|
+
root.setWidth(100);
|
|
688
|
+
root.setHeight(100);
|
|
689
|
+
|
|
690
|
+
const child = Node.create();
|
|
691
|
+
root.insertChild(child, 0);
|
|
692
|
+
|
|
693
|
+
root.freeRecursive();
|
|
694
|
+
expect(root.isFreed()).toBe(true);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("methods return default values after free (yoga-layout compatible)", () => {
|
|
698
|
+
const node = Node.create();
|
|
699
|
+
node.setWidth(100);
|
|
700
|
+
node.free();
|
|
701
|
+
|
|
702
|
+
// After free, getters return default values instead of throwing (matches yoga-layout)
|
|
703
|
+
expect(node.getComputedWidth()).toBe(0);
|
|
704
|
+
expect(node.getWidth()).toEqual({ unit: Unit.Undefined, value: NaN });
|
|
705
|
+
expect(node.getFlexDirection()).toBe(FlexDirection.Column);
|
|
706
|
+
expect(node.isDirty()).toBe(true);
|
|
707
|
+
|
|
708
|
+
// Setters are no-ops (don't throw)
|
|
709
|
+
node.setWidth(50); // Should not throw
|
|
710
|
+
node.calculateLayout(); // Should not throw
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test("double free is safe (no-op)", () => {
|
|
714
|
+
const node = Node.create();
|
|
715
|
+
node.free();
|
|
716
|
+
// Should not throw
|
|
717
|
+
node.free();
|
|
718
|
+
node.free();
|
|
719
|
+
expect(node.isFreed()).toBe(true);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("double freeRecursive is safe (no-op)", () => {
|
|
723
|
+
const root = Node.create();
|
|
724
|
+
const child = Node.create();
|
|
725
|
+
root.insertChild(child, 0);
|
|
726
|
+
|
|
727
|
+
root.freeRecursive();
|
|
728
|
+
// Should not throw
|
|
729
|
+
root.freeRecursive();
|
|
730
|
+
expect(root.isFreed()).toBe(true);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("accessing freed node in rapid cycles does not crash", () => {
|
|
734
|
+
// This test verifies the fix for malloc corruption on Linux
|
|
735
|
+
// Note: Only the root node that called freeRecursive() knows it's freed.
|
|
736
|
+
// Child nodes' JS wrappers don't automatically know they were freed.
|
|
737
|
+
const config = Config.create();
|
|
738
|
+
|
|
739
|
+
for (let i = 0; i < 50; i++) {
|
|
740
|
+
const root = Node.create(config);
|
|
741
|
+
root.setWidth(100);
|
|
742
|
+
root.setFlexDirection(FlexDirection.Column);
|
|
743
|
+
|
|
744
|
+
const child = Node.create(config);
|
|
745
|
+
child.setAlignSelf(Align.FlexStart);
|
|
746
|
+
child.setMeasureFunc(() => ({ width: 50, height: 50 }));
|
|
747
|
+
root.insertChild(child, 0);
|
|
748
|
+
|
|
749
|
+
root.calculateLayout();
|
|
750
|
+
root.freeRecursive();
|
|
751
|
+
|
|
752
|
+
// The root node should return default values, not crash (matches yoga-layout)
|
|
753
|
+
expect(root.getComputedWidth()).toBe(0);
|
|
754
|
+
root.setWidth(50); // Should be a no-op, not crash
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
config.free();
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe("Memory management", () => {
|
|
762
|
+
test("freeRecursive cleans up callbacks without errors", () => {
|
|
763
|
+
const root = Node.create();
|
|
764
|
+
root.setWidth(200);
|
|
765
|
+
root.setHeight(200);
|
|
766
|
+
root.setFlexDirection(FlexDirection.Column);
|
|
767
|
+
|
|
768
|
+
// Create a child with measure function (nodes with measure funcs can't have children)
|
|
769
|
+
const child = Node.create();
|
|
770
|
+
child.setAlignSelf(Align.FlexStart);
|
|
771
|
+
|
|
772
|
+
let measureCalled = false;
|
|
773
|
+
child.setMeasureFunc(() => {
|
|
774
|
+
measureCalled = true;
|
|
775
|
+
return { width: 100, height: 100 };
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
root.insertChild(child, 0);
|
|
779
|
+
root.calculateLayout();
|
|
780
|
+
expect(measureCalled).toBe(true);
|
|
781
|
+
|
|
782
|
+
// freeRecursive should clean up callbacks properly
|
|
783
|
+
// This should not throw or cause memory corruption
|
|
784
|
+
root.freeRecursive();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
test("reset cleans up callbacks and allows new ones", () => {
|
|
788
|
+
const node = Node.create();
|
|
789
|
+
|
|
790
|
+
// Set measure function (don't set width/height so measure func is called)
|
|
791
|
+
let firstCallbackCalled = false;
|
|
792
|
+
node.setMeasureFunc(() => {
|
|
793
|
+
firstCallbackCalled = true;
|
|
794
|
+
return { width: 50, height: 50 };
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
node.calculateLayout();
|
|
798
|
+
expect(firstCallbackCalled).toBe(true);
|
|
799
|
+
expect(node.hasMeasureFunc()).toBe(true);
|
|
800
|
+
|
|
801
|
+
// Reset should clean up the callback
|
|
802
|
+
node.reset();
|
|
803
|
+
|
|
804
|
+
// After reset, node should not have measure function
|
|
805
|
+
expect(node.hasMeasureFunc()).toBe(false);
|
|
806
|
+
|
|
807
|
+
// Should be able to set a new measure function
|
|
808
|
+
let secondCallbackCalled = false;
|
|
809
|
+
node.setMeasureFunc(() => {
|
|
810
|
+
secondCallbackCalled = true;
|
|
811
|
+
return { width: 75, height: 75 };
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
expect(node.hasMeasureFunc()).toBe(true);
|
|
815
|
+
node.calculateLayout();
|
|
816
|
+
expect(secondCallbackCalled).toBe(true);
|
|
817
|
+
|
|
818
|
+
node.free();
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("rapid free/create cycles with measure functions", () => {
|
|
822
|
+
// This test verifies that rapid free/create cycles don't cause
|
|
823
|
+
// memory corruption (the original bug on Linux)
|
|
824
|
+
const config = Config.create();
|
|
825
|
+
|
|
826
|
+
for (let i = 0; i < 100; i++) {
|
|
827
|
+
const node = Node.create(config);
|
|
828
|
+
// Don't set width/height so measure function is called
|
|
829
|
+
|
|
830
|
+
const expectedSize = 10 + i;
|
|
831
|
+
node.setMeasureFunc(() => ({
|
|
832
|
+
width: expectedSize,
|
|
833
|
+
height: expectedSize,
|
|
834
|
+
}));
|
|
835
|
+
|
|
836
|
+
node.calculateLayout();
|
|
837
|
+
|
|
838
|
+
const width = node.getComputedWidth();
|
|
839
|
+
expect(width).toBe(expectedSize);
|
|
840
|
+
expect(Number.isNaN(width)).toBe(false);
|
|
841
|
+
|
|
842
|
+
node.free();
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
config.free();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("reset followed by free works correctly", () => {
|
|
849
|
+
const node = Node.create();
|
|
850
|
+
|
|
851
|
+
node.setMeasureFunc(() => ({ width: 50, height: 50 }));
|
|
852
|
+
node.setBaselineFunc(() => 25);
|
|
853
|
+
node.setDirtiedFunc(() => {});
|
|
854
|
+
|
|
855
|
+
node.calculateLayout();
|
|
856
|
+
|
|
857
|
+
// Reset clears callbacks
|
|
858
|
+
node.reset();
|
|
859
|
+
|
|
860
|
+
// Free should work without double-free
|
|
861
|
+
node.free();
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("multiple reset calls are safe", () => {
|
|
865
|
+
const node = Node.create();
|
|
866
|
+
|
|
867
|
+
node.setMeasureFunc(() => ({ width: 50, height: 50 }));
|
|
868
|
+
node.calculateLayout();
|
|
869
|
+
|
|
870
|
+
// Multiple resets should be safe
|
|
871
|
+
node.reset();
|
|
872
|
+
node.reset();
|
|
873
|
+
node.reset();
|
|
874
|
+
|
|
875
|
+
node.free();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
test("freeRecursive with nested children with callbacks", () => {
|
|
879
|
+
const root = Node.create();
|
|
880
|
+
root.setWidth(200);
|
|
881
|
+
root.setHeight(200);
|
|
882
|
+
root.setFlexDirection(FlexDirection.Column);
|
|
883
|
+
|
|
884
|
+
// Create children with measure functions
|
|
885
|
+
// Note: We can't add children to nodes with measure functions,
|
|
886
|
+
// so we set up the hierarchy first, then add measure func to leaf nodes
|
|
887
|
+
const child1 = Node.create();
|
|
888
|
+
child1.setAlignSelf(Align.FlexStart);
|
|
889
|
+
child1.setMeasureFunc(() => ({ width: 50, height: 50 }));
|
|
890
|
+
|
|
891
|
+
const child2 = Node.create();
|
|
892
|
+
child2.setAlignSelf(Align.FlexStart);
|
|
893
|
+
child2.setMeasureFunc(() => ({ width: 60, height: 60 }));
|
|
894
|
+
|
|
895
|
+
root.insertChild(child1, 0);
|
|
896
|
+
root.insertChild(child2, 1);
|
|
897
|
+
|
|
898
|
+
root.calculateLayout();
|
|
899
|
+
|
|
900
|
+
expect(child1.getComputedWidth()).toBe(50);
|
|
901
|
+
expect(child2.getComputedWidth()).toBe(60);
|
|
902
|
+
|
|
903
|
+
// freeRecursive should clean up all nodes and their callbacks
|
|
904
|
+
// The native context is cleaned up by Zig's freeContextRecursive
|
|
905
|
+
root.freeRecursive();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
test("interleaved node lifecycle with callbacks", () => {
|
|
909
|
+
const config = Config.create();
|
|
910
|
+
const nodes: Node[] = [];
|
|
911
|
+
|
|
912
|
+
// Create several nodes with callbacks
|
|
913
|
+
for (let i = 0; i < 20; i++) {
|
|
914
|
+
const node = Node.create(config);
|
|
915
|
+
node.setMeasureFunc(() => ({ width: 10 + i, height: 10 + i }));
|
|
916
|
+
nodes.push(node);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Free some, keep others
|
|
920
|
+
for (let i = 0; i < nodes.length; i += 2) {
|
|
921
|
+
nodes[i]!.free();
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Calculate remaining nodes
|
|
925
|
+
for (let i = 1; i < nodes.length; i += 2) {
|
|
926
|
+
nodes[i]!.calculateLayout();
|
|
927
|
+
const width = nodes[i]!.getComputedWidth();
|
|
928
|
+
expect(width).toBe(10 + i);
|
|
929
|
+
expect(Number.isNaN(width)).toBe(false);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Free remaining
|
|
933
|
+
for (let i = 1; i < nodes.length; i += 2) {
|
|
934
|
+
nodes[i]!.free();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
config.free();
|
|
938
|
+
});
|
|
939
|
+
});
|