craft-native 0.0.172 → 0.0.173

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.
@@ -0,0 +1,1651 @@
1
+ //! Host-neutral layout primitives for the native renderer.
2
+ //!
3
+ //! This is the host-neutral layout core for the native renderer. It owns the
4
+ //! pure single-line main-axis flex rule that can be shared by every host while
5
+ //! leaving intrinsic measurement (especially text) to a platform callback.
6
+
7
+ const std = @import("std");
8
+
9
+ pub const Axis = enum {
10
+ row,
11
+ column,
12
+ };
13
+
14
+ /// Main-axis free-space distribution for the host-neutral tree layout.
15
+ /// Platform bridges use the same numeric rule through the flat ABI below.
16
+ pub const JustifyContent = enum {
17
+ flex_start,
18
+ center,
19
+ flex_end,
20
+ space_between,
21
+ space_around,
22
+ space_evenly,
23
+ };
24
+
25
+ /// Cross-axis alignment for a flex tree. The default stretch behavior keeps
26
+ /// children without an explicit cross size filling the available line.
27
+ pub const AlignItems = enum {
28
+ flex_start,
29
+ center,
30
+ flex_end,
31
+ stretch,
32
+ baseline,
33
+ };
34
+
35
+ /// Cross-axis distribution between wrapped flex lines.
36
+ pub const AlignContent = enum {
37
+ flex_start,
38
+ center,
39
+ flex_end,
40
+ space_between,
41
+ space_around,
42
+ space_evenly,
43
+ stretch,
44
+ };
45
+
46
+ pub const Size = struct {
47
+ width: f32,
48
+ height: f32,
49
+ };
50
+
51
+ /// C-compatible output frame shared by the iOS and Android grid bridges.
52
+ /// Keep this extern: hosts allocate a flat array and the exported ABI writes
53
+ /// these four floats directly into it.
54
+ pub const Frame = extern struct {
55
+ x: f32,
56
+ y: f32,
57
+ width: f32,
58
+ height: f32,
59
+ };
60
+
61
+ pub const Style = struct {
62
+ direction: Axis = .row,
63
+ width: ?f32 = null,
64
+ height: ?f32 = null,
65
+ /// Host-measured natural dimensions. These are deliberately plain numbers
66
+ /// so UIKit and Android can provide font/image metrics without leaking
67
+ /// platform types into the shared layout core.
68
+ intrinsic_width: ?f32 = null,
69
+ intrinsic_height: ?f32 = null,
70
+ /// Host-measured distance from the child's top edge to its text baseline.
71
+ /// A missing value falls back to the child's bottom edge.
72
+ baseline: ?f32 = null,
73
+ aspect_ratio: ?f32 = null,
74
+ flex_basis: ?f32 = null,
75
+ flex_grow: f32 = 0,
76
+ flex_shrink: f32 = 0,
77
+ min_width: ?f32 = null,
78
+ max_width: ?f32 = null,
79
+ min_height: ?f32 = null,
80
+ max_height: ?f32 = null,
81
+ gap: f32 = 0,
82
+ justify_content: JustifyContent = .flex_start,
83
+ align_items: AlignItems = .stretch,
84
+ align_self: ?AlignItems = null,
85
+ flex_wrap: bool = false,
86
+ align_content: AlignContent = .stretch,
87
+ margin_top: f32 = 0,
88
+ margin_right: f32 = 0,
89
+ margin_bottom: f32 = 0,
90
+ margin_left: f32 = 0,
91
+ padding_top: f32 = 0,
92
+ padding_right: f32 = 0,
93
+ padding_bottom: f32 = 0,
94
+ padding_left: f32 = 0,
95
+ grid: bool = false,
96
+ grid_columns: u32 = 1,
97
+ row_gap: f32 = 0,
98
+ column_gap: f32 = 0,
99
+ };
100
+
101
+ pub const Node = struct {
102
+ style: Style = .{},
103
+ children: []const Node = &.{},
104
+ };
105
+
106
+ pub const LayoutError = error{
107
+ FrameBufferTooSmall,
108
+ };
109
+
110
+ /// C-compatible input for the host bridge's one-line flex distribution.
111
+ /// `min` and `max` use a negative value to mean "unset".
112
+ pub const FlexItem = extern struct {
113
+ basis: f32,
114
+ grow: f32,
115
+ shrink: f32,
116
+ min: f32,
117
+ max: f32,
118
+ margin_start: f32,
119
+ margin_end: f32,
120
+ };
121
+
122
+ /// C-compatible input for shared flex line formation. Cross-axis measurement
123
+ /// stays on the host; line breaks depend only on these main-axis values.
124
+ pub const FlexLineItem = extern struct {
125
+ basis: f32,
126
+ margin_start: f32,
127
+ margin_end: f32,
128
+ };
129
+
130
+ /// C-compatible input for equal-track native grids. A negative or NaN
131
+ /// dimension means "use intrinsic"; `stretch` is 1 for the host's stretch
132
+ /// alignment and `alignment` is 0=start, 1=center, or 2=end.
133
+ pub const GridItem = extern struct {
134
+ intrinsic_width: f32,
135
+ intrinsic_height: f32,
136
+ explicit_width: f32,
137
+ explicit_height: f32,
138
+ min_width: f32,
139
+ max_width: f32,
140
+ min_height: f32,
141
+ max_height: f32,
142
+ margin_left: f32,
143
+ margin_right: f32,
144
+ margin_top: f32,
145
+ margin_bottom: f32,
146
+ stretch: u32,
147
+ alignment: u32,
148
+ };
149
+
150
+ /// Resolve an equal-track grid in content coordinates for native hosts.
151
+ pub export fn craft_native_grid(
152
+ available_width: f32,
153
+ available_height: f32,
154
+ columns: u32,
155
+ column_gap: f32,
156
+ row_gap: f32,
157
+ items: [*]const GridItem,
158
+ item_count: usize,
159
+ frames: [*]Frame,
160
+ ) callconv(.c) c_int {
161
+ return craftNativeGridWithAutoRows(available_width, available_height, columns, column_gap, row_gap, 0, std.math.nan(f32), items, item_count, frames);
162
+ }
163
+
164
+ /// Resolve equal-track grid frames while distributing leftover block-axis
165
+ /// space between the generated rows. The original symbol remains available
166
+ /// for older hosts and is equivalent to `align_content = 0`.
167
+ pub export fn craft_native_grid_aligned(
168
+ available_width: f32,
169
+ available_height: f32,
170
+ columns: u32,
171
+ column_gap: f32,
172
+ row_gap: f32,
173
+ align_content: u32,
174
+ items: [*]const GridItem,
175
+ item_count: usize,
176
+ frames: [*]Frame,
177
+ ) callconv(.c) c_int {
178
+ return craftNativeGridWithAutoRows(available_width, available_height, columns, column_gap, row_gap, align_content, std.math.nan(f32), items, item_count, frames);
179
+ }
180
+
181
+ /// Resolve an equal-track grid with a host-provided `grid-auto-rows` minimum.
182
+ /// The separate symbol keeps the original ABI intact for runtimes that do not
183
+ /// ship this optional argument yet.
184
+ pub export fn craft_native_grid_auto_rows(
185
+ available_width: f32,
186
+ available_height: f32,
187
+ columns: u32,
188
+ column_gap: f32,
189
+ row_gap: f32,
190
+ auto_row_height: f32,
191
+ items: [*]const GridItem,
192
+ item_count: usize,
193
+ frames: [*]Frame,
194
+ ) callconv(.c) c_int {
195
+ return craftNativeGridWithAutoRows(available_width, available_height, columns, column_gap, row_gap, 0, auto_row_height, items, item_count, frames);
196
+ }
197
+
198
+ /// Auto-row variant of `craft_native_grid_aligned`.
199
+ pub export fn craft_native_grid_auto_rows_aligned(
200
+ available_width: f32,
201
+ available_height: f32,
202
+ columns: u32,
203
+ column_gap: f32,
204
+ row_gap: f32,
205
+ align_content: u32,
206
+ auto_row_height: f32,
207
+ items: [*]const GridItem,
208
+ item_count: usize,
209
+ frames: [*]Frame,
210
+ ) callconv(.c) c_int {
211
+ return craftNativeGridWithAutoRows(available_width, available_height, columns, column_gap, row_gap, align_content, auto_row_height, items, item_count, frames);
212
+ }
213
+
214
+ fn craftNativeGridWithAutoRows(
215
+ available_width: f32,
216
+ available_height: f32,
217
+ columns: u32,
218
+ column_gap: f32,
219
+ row_gap: f32,
220
+ align_content: u32,
221
+ auto_row_height: f32,
222
+ items: [*]const GridItem,
223
+ item_count: usize,
224
+ frames: [*]Frame,
225
+ ) c_int {
226
+ if (item_count == 0) return 0;
227
+ const column_count = @max(@as(usize, 1), @as(usize, @intCast(columns)));
228
+ const width = finiteNonNegative(available_width);
229
+ const height = finiteNonNegative(available_height);
230
+ const safe_column_gap = finiteNonNegative(column_gap);
231
+ const safe_row_gap = finiteNonNegative(row_gap);
232
+ const safe_auto_row_height = finiteNonNegative(auto_row_height);
233
+ const cell_width = @max(0, width - safe_column_gap * @as(f32, @floatFromInt(column_count - 1))) /
234
+ @as(f32, @floatFromInt(column_count));
235
+
236
+ const row_count = (item_count + column_count - 1) / column_count;
237
+ var total_rows: f32 = 0;
238
+ for (0..row_count) |row| {
239
+ total_rows += gridItemRowHeight(items, item_count, column_count, row, safe_auto_row_height);
240
+ }
241
+ const gap_total = safe_row_gap * @as(f32, @floatFromInt(if (row_count > 1) row_count - 1 else 0));
242
+ const free_rows = @max(0, height - total_rows - gap_total);
243
+ var row_leading: f32 = 0;
244
+ var row_between = safe_row_gap;
245
+ var row_extra: f32 = 0;
246
+ switch (align_content) {
247
+ 1 => row_leading = free_rows / 2,
248
+ 2 => row_leading = free_rows,
249
+ 3 => {
250
+ if (row_count > 1) row_between += free_rows / @as(f32, @floatFromInt(row_count - 1));
251
+ },
252
+ 4 => {
253
+ row_leading = free_rows / @as(f32, @floatFromInt(row_count * 2));
254
+ row_between += free_rows / @as(f32, @floatFromInt(row_count));
255
+ },
256
+ 6 => {
257
+ row_leading = free_rows / @as(f32, @floatFromInt(row_count + 1));
258
+ row_between += free_rows / @as(f32, @floatFromInt(row_count + 1));
259
+ },
260
+ 5 => row_extra = free_rows / @as(f32, @floatFromInt(row_count)),
261
+ else => {},
262
+ }
263
+ var row_y = row_leading;
264
+ for (0..row_count) |row| {
265
+ const row_height = gridItemRowHeight(items, item_count, column_count, row, safe_auto_row_height) + row_extra;
266
+ const start = row * column_count;
267
+ const end = @min(item_count, start + column_count);
268
+ for (items[start..end], start..) |item, index| {
269
+ const column = index % column_count;
270
+ const available_item_width = @max(0, cell_width - finiteNonNegative(item.margin_left) - finiteNonNegative(item.margin_right));
271
+ const requested_width = optionalDimension(item.explicit_width, item.intrinsic_width);
272
+ const item_width = clampBounds(@min(available_item_width, if (item.stretch == 1 and isUnset(item.explicit_width)) available_item_width else requested_width), item.min_width, item.max_width);
273
+ const available_item_height = @max(0, row_height - finiteNonNegative(item.margin_top) - finiteNonNegative(item.margin_bottom));
274
+ const requested_height = optionalDimension(item.explicit_height, item.intrinsic_height);
275
+ const item_height = clampBounds(@min(available_item_height, if (item.stretch == 1 and isUnset(item.explicit_height)) available_item_height else requested_height), item.min_height, item.max_height);
276
+ const horizontal_space = @max(0, available_item_width - item_width);
277
+ const vertical_space = @max(0, available_item_height - item_height);
278
+ const align_x = if (item.alignment == 1) horizontal_space / 2 else if (item.alignment == 2) horizontal_space else 0;
279
+ const align_y = if (item.alignment == 1) vertical_space / 2 else if (item.alignment == 2) vertical_space else 0;
280
+ frames[index] = .{
281
+ .x = @as(f32, @floatFromInt(column)) * (cell_width + safe_column_gap) + finiteNonNegative(item.margin_left) + align_x,
282
+ .y = row_y + finiteNonNegative(item.margin_top) + align_y,
283
+ .width = item_width,
284
+ .height = item_height,
285
+ };
286
+ }
287
+ row_y += row_height + row_between;
288
+ }
289
+ return 0;
290
+ }
291
+
292
+ fn gridItemRowHeight(items: [*]const GridItem, item_count: usize, column_count: usize, row: usize, auto_row_height: f32) f32 {
293
+ var row_height: f32 = 0;
294
+ const start = row * column_count;
295
+ const end = @min(item_count, start + column_count);
296
+ for (items[start..end]) |item| {
297
+ const intrinsic = optionalDimension(item.explicit_height, item.intrinsic_height);
298
+ row_height = @max(row_height, auto_row_height, clampBounds(intrinsic, item.min_height, item.max_height) + finiteNonNegative(item.margin_top) + finiteNonNegative(item.margin_bottom));
299
+ }
300
+ return row_height;
301
+ }
302
+
303
+ /// Distribute one flex line in content coordinates for native hosts.
304
+ /// Margins and gaps consume space; free space follows grow or shrink*basis.
305
+ /// Bounds are solved as a water-fill rather than clamped after one pass: when
306
+ /// an item reaches min/max, the remaining free space is redistributed to the
307
+ /// still-flexible items. Return 0 for success.
308
+ pub export fn craft_native_flex_row(
309
+ available: f32,
310
+ gap: f32,
311
+ items: [*]const FlexItem,
312
+ item_count: usize,
313
+ widths: [*]f32,
314
+ positions: [*]f32,
315
+ ) callconv(.c) c_int {
316
+ return craftNativeFlexRow(available, gap, 0, items, item_count, widths, positions);
317
+ }
318
+
319
+ /// Resolve one flex item's cross-axis offset. `alignment` is 0=start,
320
+ /// 1=center, 2=end, or 3=baseline. For baseline, `available` is the line's
321
+ /// baseline, `size` is the child's measured baseline, and `margin_start` is
322
+ /// the child's leading margin. Margins are part of the available cross-axis
323
+ /// space.
324
+ /// Keeping this calculation in the shared core prevents host-specific drift
325
+ /// when a child is larger than its flex line.
326
+ pub export fn craft_native_cross_axis_position(
327
+ available: f32,
328
+ size: f32,
329
+ margin_start: f32,
330
+ margin_end: f32,
331
+ alignment: u32,
332
+ ) callconv(.c) f32 {
333
+ const safe_available = finiteNonNegative(available);
334
+ const safe_size = finiteNonNegative(size);
335
+ const safe_margin_start = finiteNonNegative(margin_start);
336
+ const safe_margin_end = finiteNonNegative(margin_end);
337
+ return switch (alignment) {
338
+ 1 => safe_margin_start + (safe_available - safe_margin_start - safe_margin_end - safe_size) / 2,
339
+ 2 => safe_available - safe_margin_end - safe_size,
340
+ 3 => @max(safe_margin_start, safe_available - safe_size),
341
+ else => safe_margin_start,
342
+ };
343
+ }
344
+
345
+ /// Form wrapped flex lines. `wrap` is 0 for one line or non-zero to break
346
+ /// before an item that would exceed `available`; the return value is the line
347
+ /// count, and `line_ids` receives one zero-based line id per item.
348
+ pub export fn craft_native_flex_line_breaks(
349
+ available: f32,
350
+ gap: f32,
351
+ wrap: u32,
352
+ items: [*]const FlexLineItem,
353
+ item_count: usize,
354
+ line_ids: [*]i32,
355
+ ) callconv(.c) c_int {
356
+ if (item_count == 0) return 0;
357
+ const safe_available = finiteNonNegative(available);
358
+ const safe_gap = finiteNonNegative(gap);
359
+ var line_id: i32 = 0;
360
+ var used: f32 = 0;
361
+ for (items[0..item_count], 0..) |item, index| {
362
+ const item_size = finiteNonNegative(item.basis) + finiteNonNegative(item.margin_start) + finiteNonNegative(item.margin_end);
363
+ if (wrap != 0 and index > 0 and used + safe_gap + item_size > safe_available) {
364
+ line_id += 1;
365
+ used = item_size;
366
+ } else {
367
+ used = if (index == 0) item_size else used + safe_gap + item_size;
368
+ }
369
+ line_ids[index] = line_id;
370
+ }
371
+ return line_id + 1;
372
+ }
373
+
374
+ /// Distribute wrapped flex lines in the cross axis. `alignment` is 0=start,
375
+ /// 1=center, 2=end, 3=space-between, 4=space-around, 5=stretch, or
376
+ /// 6=space-evenly.
377
+ pub export fn craft_native_flex_line_distribution(
378
+ available: f32,
379
+ gap: f32,
380
+ alignment: u32,
381
+ line_crosses: [*]const f32,
382
+ line_count: usize,
383
+ offsets: [*]f32,
384
+ sizes: [*]f32,
385
+ ) callconv(.c) c_int {
386
+ if (line_count == 0) return 0;
387
+ const safe_available = finiteNonNegative(available);
388
+ const safe_gap = finiteNonNegative(gap);
389
+ var total: f32 = 0;
390
+ for (line_crosses[0..line_count]) |line_cross| total += finiteNonNegative(line_cross);
391
+ total += safe_gap * @as(f32, @floatFromInt(line_count - 1));
392
+ const free = @max(0, safe_available - total);
393
+ var leading: f32 = 0;
394
+ var between = safe_gap;
395
+ switch (alignment) {
396
+ 1 => leading = free / 2,
397
+ 2 => leading = free,
398
+ 3 => {
399
+ if (line_count > 1) between = safe_gap + free / @as(f32, @floatFromInt(line_count - 1));
400
+ },
401
+ 4 => {
402
+ leading = free / @as(f32, @floatFromInt(line_count * 2));
403
+ between = safe_gap + free / @as(f32, @floatFromInt(line_count));
404
+ },
405
+ 5 => {
406
+ const extra = free / @as(f32, @floatFromInt(line_count));
407
+ for (line_crosses[0..line_count], 0..) |line_cross, index| sizes[index] = finiteNonNegative(line_cross) + extra;
408
+ },
409
+ 6 => {
410
+ leading = free / @as(f32, @floatFromInt(line_count + 1));
411
+ between = safe_gap + free / @as(f32, @floatFromInt(line_count + 1));
412
+ },
413
+ else => {},
414
+ }
415
+ if (alignment != 5) {
416
+ for (line_crosses[0..line_count], 0..) |line_cross, index| sizes[index] = finiteNonNegative(line_cross);
417
+ }
418
+ var offset = leading;
419
+ for (0..line_count) |index| {
420
+ offsets[index] = offset;
421
+ offset += sizes[index];
422
+ if (index + 1 < line_count) offset += between;
423
+ }
424
+ return 0;
425
+ }
426
+
427
+ /// ABI v2 flex row distribution. `justify` is 0=start, 1=center, 2=end,
428
+ /// 3=space-between, 4=space-around, or 5=space-evenly. The original export
429
+ /// remains available for older hosts and keeps start justification.
430
+ pub export fn craft_native_flex_row_justified(
431
+ available: f32,
432
+ gap: f32,
433
+ justify: u32,
434
+ items: [*]const FlexItem,
435
+ item_count: usize,
436
+ widths: [*]f32,
437
+ positions: [*]f32,
438
+ ) callconv(.c) c_int {
439
+ return craftNativeFlexRow(available, gap, justify, items, item_count, widths, positions);
440
+ }
441
+
442
+ fn craftNativeFlexRow(
443
+ available: f32,
444
+ gap: f32,
445
+ justify: u32,
446
+ items: [*]const FlexItem,
447
+ item_count: usize,
448
+ widths: [*]f32,
449
+ positions: [*]f32,
450
+ ) c_int {
451
+ if (item_count == 0) return 0;
452
+
453
+ const safe_available = finiteNonNegative(available);
454
+ const safe_gap = finiteNonNegative(gap);
455
+ var basis_total = safe_gap * @as(f32, @floatFromInt(item_count - 1));
456
+ var grow_total: f32 = 0;
457
+ var shrink_total: f32 = 0;
458
+ for (items[0..item_count]) |item| {
459
+ const basis = finiteNonNegative(item.basis);
460
+ basis_total += basis + finiteNonNegative(item.margin_start) + finiteNonNegative(item.margin_end);
461
+ grow_total += positiveGrow(item.grow);
462
+ shrink_total += positiveShrink(item.shrink) * basis;
463
+ }
464
+
465
+ const free = safe_available - basis_total;
466
+ const mode: FlexMode = if (free > 0 and grow_total > 0)
467
+ .grow
468
+ else if (free < 0 and shrink_total > 0)
469
+ .shrink
470
+ else
471
+ .none;
472
+ const lambda = flexLambda(items[0..item_count], safe_available, safe_gap, mode);
473
+ var used = safe_gap * @as(f32, @floatFromInt(item_count - 1));
474
+ for (items[0..item_count], 0..) |item, index| {
475
+ const basis = finiteNonNegative(item.basis);
476
+ const width = clampBounds(flexRawSize(item, basis, lambda, mode), item.min, item.max);
477
+ used += width + finiteNonNegative(item.margin_start) + finiteNonNegative(item.margin_end);
478
+ widths[index] = width;
479
+ }
480
+
481
+ const remaining = @max(0, safe_available - used);
482
+ var leading_space: f32 = 0;
483
+ var between_space: f32 = 0;
484
+ switch (justify) {
485
+ 1 => leading_space = remaining / 2,
486
+ 2 => leading_space = remaining,
487
+ 3 => {
488
+ if (item_count > 1) between_space = remaining / @as(f32, @floatFromInt(item_count - 1));
489
+ },
490
+ 4 => {
491
+ leading_space = remaining / @as(f32, @floatFromInt(item_count * 2));
492
+ between_space = remaining / @as(f32, @floatFromInt(item_count));
493
+ },
494
+ 5 => {
495
+ leading_space = remaining / @as(f32, @floatFromInt(item_count + 1));
496
+ between_space = leading_space;
497
+ },
498
+ else => {},
499
+ }
500
+
501
+ var offset = leading_space;
502
+ for (items[0..item_count], 0..) |item, index| {
503
+ const leading = finiteNonNegative(item.margin_start);
504
+ const trailing = finiteNonNegative(item.margin_end);
505
+ positions[index] = offset + leading;
506
+ offset += leading + widths[index] + trailing;
507
+ if (index + 1 < item_count) offset += safe_gap + between_space;
508
+ }
509
+ return 0;
510
+ }
511
+
512
+ const FlexMode = enum { none, grow, shrink };
513
+
514
+ fn flexRawSize(item: FlexItem, basis: f32, lambda: f32, mode: FlexMode) f32 {
515
+ return switch (mode) {
516
+ .grow => basis + lambda * positiveGrow(item.grow),
517
+ .shrink => basis - lambda * positiveShrink(item.shrink) * basis,
518
+ .none => basis,
519
+ };
520
+ }
521
+
522
+ fn flexUsed(items: []const FlexItem, gap: f32, lambda: f32, mode: FlexMode) f32 {
523
+ var used = gap * @as(f32, @floatFromInt(if (items.len > 1) items.len - 1 else 0));
524
+ for (items) |item| {
525
+ const basis = finiteNonNegative(item.basis);
526
+ used += clampBounds(flexRawSize(item, basis, lambda, mode), item.min, item.max);
527
+ used += finiteNonNegative(item.margin_start) + finiteNonNegative(item.margin_end);
528
+ }
529
+ return used;
530
+ }
531
+
532
+ /// Find the scalar that fills (or, when bounds make that impossible, gets as
533
+ /// close as possible to) the available line. This is equivalent to freezing
534
+ /// bounded flex items one at a time, but does not allocate scratch buffers and
535
+ /// therefore remains safe across the C ABI.
536
+ fn flexLambda(items: []const FlexItem, available: f32, gap: f32, mode: FlexMode) f32 {
537
+ if (mode == .none) return 0;
538
+ var low: f32 = 0;
539
+ var high: f32 = if (mode == .shrink) 1 else 1;
540
+ if (mode == .grow) {
541
+ var attempts: u8 = 0;
542
+ while (flexUsed(items, gap, high, mode) < available and attempts < 31) : (attempts += 1) {
543
+ high *= 2;
544
+ }
545
+ }
546
+ for (0..32) |_| {
547
+ const mid = (low + high) / 2;
548
+ if ((mode == .grow and flexUsed(items, gap, mid, mode) < available) or
549
+ (mode == .shrink and flexUsed(items, gap, mid, mode) > available))
550
+ low = mid
551
+ else
552
+ high = mid;
553
+ }
554
+ return (low + high) / 2;
555
+ }
556
+
557
+ /// Computes a tree's absolute frames without importing any platform type.
558
+ ///
559
+ /// The root uses its explicit size when present and otherwise the viewport.
560
+ /// A child consumes `flex_basis`, then its explicit main-axis size, then zero.
561
+ /// Positive remaining space is distributed by `flex_grow`; negative remaining
562
+ /// space is removed by `flex_shrink * basis`. Both results honor the child's
563
+ /// margins and min/max bounds. A grid parent uses equal columns and max-content
564
+ /// row tracks from explicit child heights; `row_gap` and `column_gap` are
565
+ /// independent.
566
+ /// Text and other intrinsic measurements are intentionally absent: a host
567
+ /// callback can provide those bases without leaking UIKit or Android concepts.
568
+ pub fn layout(root: Node, viewport: Size, frames: []Frame) LayoutError!usize {
569
+ if (frames.len == 0) return error.FrameBufferTooSmall;
570
+ return layoutNode(root, viewport, 0, 0, 0, frames);
571
+ }
572
+
573
+ fn layoutNode(
574
+ node: Node,
575
+ available: Size,
576
+ x: f32,
577
+ y: f32,
578
+ frame_index: usize,
579
+ frames: []Frame,
580
+ ) LayoutError!usize {
581
+ if (frame_index >= frames.len) return error.FrameBufferTooSmall;
582
+
583
+ const width = if (frame_index == 0)
584
+ finiteNonNegative(node.style.width orelse available.width)
585
+ else
586
+ finiteNonNegative(available.width);
587
+ const height = if (frame_index == 0)
588
+ finiteNonNegative(node.style.height orelse available.height)
589
+ else
590
+ finiteNonNegative(available.height);
591
+ frames[frame_index] = .{ .x = x, .y = y, .width = width, .height = height };
592
+
593
+ if (node.children.len == 0) return frame_index + 1;
594
+ const content_width = @max(0, width - node.style.padding_left - node.style.padding_right);
595
+ const content_height = @max(0, height - node.style.padding_top - node.style.padding_bottom);
596
+ const content_x = x + node.style.padding_left;
597
+ const content_y = y + node.style.padding_top;
598
+ if (node.style.grid) return layoutGrid(node, content_width, content_height, content_x, content_y, frame_index, frames);
599
+
600
+ const main_size = if (node.style.direction == .row) content_width else content_height;
601
+ const gap = @max(0, node.style.gap);
602
+ const gap_count = if (node.children.len > 1) node.children.len - 1 else 0;
603
+ const gap_total = gap * @as(f32, @floatFromInt(gap_count));
604
+ if (node.style.flex_wrap) return layoutWrapped(node, content_width, content_height, content_x, content_y, frame_index, frames);
605
+ const flex = resolveFlex(node.children, node.style.direction, main_size, gap_total);
606
+ const line_baseline = baselineForChildren(node.children, node.style.direction, content_width, content_height, content_height, node.style.align_items);
607
+ var used_main: f32 = gap_total;
608
+ for (node.children) |child| {
609
+ used_main += flex.size(child, childMainBasis(child, node.style.direction)) + mainMargins(child, node.style.direction);
610
+ }
611
+ const free_main = @max(0, main_size - used_main);
612
+ var leading_space: f32 = 0;
613
+ var between_space: f32 = gap;
614
+ switch (node.style.justify_content) {
615
+ .center => leading_space = free_main / 2,
616
+ .flex_end => leading_space = free_main,
617
+ .space_between => {
618
+ if (gap_count > 0) between_space += free_main / @as(f32, @floatFromInt(gap_count));
619
+ },
620
+ .space_around => {
621
+ leading_space = free_main / @as(f32, @floatFromInt(node.children.len * 2));
622
+ between_space += free_main / @as(f32, @floatFromInt(node.children.len));
623
+ },
624
+ .space_evenly => {
625
+ leading_space = free_main / @as(f32, @floatFromInt(node.children.len + 1));
626
+ between_space += leading_space;
627
+ },
628
+ .flex_start => {},
629
+ }
630
+ var next_index = frame_index + 1;
631
+ var main_offset: f32 = leading_space;
632
+ for (node.children, 0..) |child, index| {
633
+ const basis = childMainBasis(child, node.style.direction);
634
+ const child_main = flex.size(child, basis);
635
+ const child_cross = childCrossSize(child, node.style.direction, content_width, content_height, node.style.align_items);
636
+ const child_cross_offset = crossOffset(
637
+ child,
638
+ node.style.direction,
639
+ child_cross,
640
+ content_width,
641
+ content_height,
642
+ node.style.align_items,
643
+ line_baseline,
644
+ );
645
+ const leading_margin = mainLeadingMargin(child, node.style.direction);
646
+ const child_size = if (node.style.direction == .row)
647
+ Size{ .width = child_main, .height = child_cross }
648
+ else
649
+ Size{ .width = child_cross, .height = child_main };
650
+ const child_x = if (node.style.direction == .row) content_x + main_offset + leading_margin else content_x + child_cross_offset;
651
+ const child_y = if (node.style.direction == .row) content_y + child_cross_offset else content_y + main_offset + leading_margin;
652
+ next_index = try layoutNode(child, child_size, child_x, child_y, next_index, frames);
653
+ main_offset += mainMargins(child, node.style.direction) + child_main + if (index + 1 < node.children.len) between_space else 0;
654
+ }
655
+ return next_index;
656
+ }
657
+
658
+ fn layoutWrapped(
659
+ node: Node,
660
+ width: f32,
661
+ height: f32,
662
+ x: f32,
663
+ y: f32,
664
+ frame_index: usize,
665
+ frames: []Frame,
666
+ ) LayoutError!usize {
667
+ const direction = node.style.direction;
668
+ const main_size = if (direction == .row) width else height;
669
+ const cross_size = if (direction == .row) height else width;
670
+ const gap = @max(0, node.style.gap);
671
+ var line_count: usize = 0;
672
+ var cross_total: f32 = 0;
673
+ var start: usize = 0;
674
+ while (start < node.children.len) {
675
+ const end = wrappedLineEnd(node.children, start, direction, main_size, gap);
676
+ cross_total += wrappedLineCross(node.children[start..end], direction);
677
+ if (line_count > 0) cross_total += gap;
678
+ line_count += 1;
679
+ start = end;
680
+ }
681
+ const free_cross = @max(0, cross_size - cross_total);
682
+ var line_extra: f32 = 0;
683
+ var cross_offset: f32 = 0;
684
+ var cross_between = gap;
685
+ switch (node.style.align_content) {
686
+ .center => cross_offset = free_cross / 2,
687
+ .flex_end => cross_offset = free_cross,
688
+ .space_between => {
689
+ if (line_count > 1) cross_between += free_cross / @as(f32, @floatFromInt(line_count - 1));
690
+ },
691
+ .space_around => {
692
+ cross_offset = free_cross / @as(f32, @floatFromInt(line_count * 2));
693
+ cross_between += free_cross / @as(f32, @floatFromInt(line_count));
694
+ },
695
+ .space_evenly => {
696
+ cross_offset = free_cross / @as(f32, @floatFromInt(line_count + 1));
697
+ cross_between += free_cross / @as(f32, @floatFromInt(line_count + 1));
698
+ },
699
+ .stretch => line_extra = free_cross / @as(f32, @floatFromInt(line_count)),
700
+ .flex_start => {},
701
+ }
702
+ var next_index = frame_index + 1;
703
+ start = 0;
704
+ while (start < node.children.len) {
705
+ const end = wrappedLineEnd(node.children, start, direction, main_size, gap);
706
+ const children = node.children[start..end];
707
+ const line_cross = wrappedLineCross(children, direction) + line_extra;
708
+ const flex = resolveFlex(children, direction, main_size, gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0)));
709
+ var used_main: f32 = gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0));
710
+ for (children) |child| used_main += flex.size(child, childMainBasis(child, direction)) + mainMargins(child, direction);
711
+ const free_main = @max(0, main_size - used_main);
712
+ var leading_space: f32 = 0;
713
+ var between_space: f32 = gap;
714
+ distributeJustify(node.style.justify_content, free_main, children.len, &leading_space, &between_space);
715
+ const line_baseline = baselineForChildren(children, direction, width, height, line_cross, node.style.align_items);
716
+ var main_offset = leading_space;
717
+ for (children, 0..) |child, index| {
718
+ const basis = childMainBasis(child, direction);
719
+ const child_main = flex.size(child, basis);
720
+ const child_cross = if (direction == .row)
721
+ childCrossSize(child, direction, width, line_cross, node.style.align_items)
722
+ else
723
+ childCrossSize(child, direction, line_cross, height, node.style.align_items);
724
+ const child_cross_offset = if (direction == .row)
725
+ crossOffset(child, direction, child_cross, width, line_cross, node.style.align_items, line_baseline)
726
+ else
727
+ crossOffset(child, direction, child_cross, line_cross, height, node.style.align_items, null);
728
+ const leading_margin = mainLeadingMargin(child, direction);
729
+ const child_size = if (direction == .row)
730
+ Size{ .width = child_main, .height = child_cross }
731
+ else
732
+ Size{ .width = child_cross, .height = child_main };
733
+ const child_x = if (direction == .row) x + main_offset + leading_margin else x + child_cross_offset;
734
+ const child_y = if (direction == .row) y + cross_offset + child_cross_offset else y + main_offset + leading_margin;
735
+ next_index = try layoutNode(child, child_size, child_x, child_y, next_index, frames);
736
+ main_offset += mainMargins(child, direction) + child_main;
737
+ if (index + 1 < children.len) main_offset += between_space;
738
+ }
739
+ cross_offset += line_cross + cross_between;
740
+ start = end;
741
+ }
742
+ return next_index;
743
+ }
744
+
745
+ fn wrappedLineEnd(children: []const Node, start: usize, direction: Axis, available: f32, gap: f32) usize {
746
+ var end = start;
747
+ var used: f32 = 0;
748
+ while (end < children.len) : (end += 1) {
749
+ const child = children[end];
750
+ const item = childMainBasis(child, direction) + mainMargins(child, direction);
751
+ const candidate = if (end == start) item else used + gap + item;
752
+ if (end > start and candidate > available) break;
753
+ used = candidate;
754
+ }
755
+ return if (end == start) start + 1 else end;
756
+ }
757
+
758
+ fn wrappedLineCross(children: []const Node, direction: Axis) f32 {
759
+ var cross: f32 = 0;
760
+ for (children) |child| {
761
+ const size = if (direction == .row)
762
+ child.style.height orelse child.style.intrinsic_height
763
+ else
764
+ child.style.width orelse child.style.intrinsic_width;
765
+ const margins = if (direction == .row) verticalMargins(child) else horizontalMargins(child);
766
+ cross = @max(cross, (size orelse 0) + margins);
767
+ }
768
+ return cross;
769
+ }
770
+
771
+ fn distributeJustify(justify: JustifyContent, free: f32, item_count: usize, leading: *f32, between: *f32) void {
772
+ switch (justify) {
773
+ .center => leading.* = free / 2,
774
+ .flex_end => leading.* = free,
775
+ .space_between => {
776
+ if (item_count > 1) between.* += free / @as(f32, @floatFromInt(item_count - 1));
777
+ },
778
+ .space_around => {
779
+ leading.* = free / @as(f32, @floatFromInt(item_count * 2));
780
+ between.* += free / @as(f32, @floatFromInt(item_count));
781
+ },
782
+ .space_evenly => {
783
+ leading.* = free / @as(f32, @floatFromInt(item_count + 1));
784
+ between.* += leading.*;
785
+ },
786
+ .flex_start => {},
787
+ }
788
+ }
789
+
790
+ const NodeFlexMode = enum { none, grow, shrink };
791
+
792
+ const NodeFlexResolution = struct {
793
+ mode: NodeFlexMode,
794
+ lambda: f32,
795
+ direction: Axis,
796
+
797
+ fn size(self: NodeFlexResolution, child: Node, basis: f32) f32 {
798
+ const raw = switch (self.mode) {
799
+ .grow => basis + self.lambda * positiveGrow(child.style.flex_grow),
800
+ .shrink => basis - self.lambda * positiveShrink(child.style.flex_shrink) * basis,
801
+ .none => basis,
802
+ };
803
+ return clampMain(child, self.direction, raw);
804
+ }
805
+ };
806
+
807
+ fn nodeFlexUsed(children: []const Node, direction: Axis, gap: f32, lambda: f32, mode: NodeFlexMode) f32 {
808
+ var used = gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0));
809
+ for (children) |child| {
810
+ const basis = childMainBasis(child, direction);
811
+ const raw = switch (mode) {
812
+ .grow => basis + lambda * positiveGrow(child.style.flex_grow),
813
+ .shrink => basis - lambda * positiveShrink(child.style.flex_shrink) * basis,
814
+ .none => basis,
815
+ };
816
+ used += clampMain(child, direction, raw) + mainMargins(child, direction);
817
+ }
818
+ return used;
819
+ }
820
+
821
+ fn resolveFlex(children: []const Node, direction: Axis, available: f32, gap: f32) NodeFlexResolution {
822
+ var basis_total = gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0));
823
+ var grow_total: f32 = 0;
824
+ var shrink_total: f32 = 0;
825
+ for (children) |child| {
826
+ const basis = childMainBasis(child, direction);
827
+ basis_total += basis + mainMargins(child, direction);
828
+ grow_total += positiveGrow(child.style.flex_grow);
829
+ shrink_total += positiveShrink(child.style.flex_shrink) * basis;
830
+ }
831
+ const free = available - basis_total;
832
+ const mode: NodeFlexMode = if (free > 0 and grow_total > 0)
833
+ .grow
834
+ else if (free < 0 and shrink_total > 0)
835
+ .shrink
836
+ else
837
+ .none;
838
+ if (mode == .none) return .{ .mode = .none, .lambda = 0, .direction = direction };
839
+
840
+ var low: f32 = 0;
841
+ var high: f32 = if (mode == .shrink) 1 else 1;
842
+ if (mode == .grow) {
843
+ var attempts: u8 = 0;
844
+ while (nodeFlexUsed(children, direction, gap, high, mode) < available and attempts < 31) : (attempts += 1) {
845
+ high *= 2;
846
+ }
847
+ }
848
+ for (0..32) |_| {
849
+ const mid = (low + high) / 2;
850
+ if ((mode == .grow and nodeFlexUsed(children, direction, gap, mid, mode) < available) or
851
+ (mode == .shrink and nodeFlexUsed(children, direction, gap, mid, mode) > available))
852
+ low = mid
853
+ else
854
+ high = mid;
855
+ }
856
+ return .{ .mode = mode, .lambda = (low + high) / 2, .direction = direction };
857
+ }
858
+
859
+ fn layoutGrid(
860
+ node: Node,
861
+ width: f32,
862
+ height: f32,
863
+ x: f32,
864
+ y: f32,
865
+ frame_index: usize,
866
+ frames: []Frame,
867
+ ) LayoutError!usize {
868
+ const columns = @max(@as(u32, 1), node.style.grid_columns);
869
+ const column_count: usize = @intCast(columns);
870
+ const column_gap = @max(0, node.style.column_gap);
871
+ const row_gap = @max(0, node.style.row_gap);
872
+ const available_width = @max(0, width - column_gap * @as(f32, @floatFromInt(column_count - 1)));
873
+ const cell_width = available_width / @as(f32, @floatFromInt(column_count));
874
+ var next_index = frame_index + 1;
875
+ var row_y = y;
876
+ for (node.children, 0..) |child, index| {
877
+ const row = index / column_count;
878
+ const column = index % column_count;
879
+ const row_height = gridRowHeight(node, row, column_count);
880
+ const child_available_width = @max(0, cell_width - horizontalMargins(child));
881
+ const ratio_width = if (validAspectRatio(child.style.aspect_ratio)) |ratio|
882
+ if (child.style.width == null and child.style.height != null) child.style.height.? * ratio else null
883
+ else
884
+ null;
885
+ const child_width = clampMain(child, .row, @min(child_available_width, child.style.width orelse ratio_width orelse child.style.intrinsic_width orelse child_available_width));
886
+ const available_height = @max(0, row_height - verticalMargins(child));
887
+ const alignment = effectiveAlignment(child, node.style.align_items);
888
+ const ratio_height = if (validAspectRatio(child.style.aspect_ratio)) |ratio|
889
+ if (child.style.height == null and child.style.width != null) child.style.width.? / ratio else null
890
+ else
891
+ null;
892
+ const requested_height = child.style.height orelse ratio_height orelse child.style.intrinsic_height orelse child.style.flex_basis;
893
+ const child_height = if (requested_height == null and alignment == .stretch)
894
+ available_height
895
+ else
896
+ clamp(@min(available_height, requested_height orelse 0), child.style.min_height, child.style.max_height);
897
+ const vertical_space = @max(0, available_height - child_height);
898
+ const vertical_offset = switch (alignment) {
899
+ .center => vertical_space / 2,
900
+ .flex_end => vertical_space,
901
+ else => 0,
902
+ };
903
+ const child_x = x + @as(f32, @floatFromInt(column)) * (cell_width + column_gap) + child.style.margin_left;
904
+ const child_y = row_y + child.style.margin_top + vertical_offset;
905
+ next_index = try layoutNode(child, .{ .width = child_width, .height = child_height }, child_x, child_y, next_index, frames);
906
+ if (column + 1 == column_count or index + 1 == node.children.len) {
907
+ row_y += row_height + row_gap;
908
+ }
909
+ }
910
+ _ = height;
911
+ return next_index;
912
+ }
913
+
914
+ fn gridRowHeight(node: Node, row: usize, column_count: usize) f32 {
915
+ const start = row * column_count;
916
+ const end = @min(node.children.len, start + column_count);
917
+ var height: f32 = 0;
918
+ for (node.children[start..end]) |child| {
919
+ const ratio_height = if (validAspectRatio(child.style.aspect_ratio)) |ratio|
920
+ if (child.style.height == null and child.style.width != null) child.style.width.? / ratio else null
921
+ else
922
+ null;
923
+ const child_height = child.style.height orelse ratio_height orelse child.style.intrinsic_height orelse child.style.flex_basis orelse 0;
924
+ height = @max(height, clamp(child_height, child.style.min_height, child.style.max_height) + verticalMargins(child));
925
+ }
926
+ return height;
927
+ }
928
+
929
+ fn childMainBasis(node: Node, parent_direction: Axis) f32 {
930
+ const explicit = if (parent_direction == .row) node.style.width else node.style.height;
931
+ const ratio_size = if (validAspectRatio(node.style.aspect_ratio)) |ratio|
932
+ if (parent_direction == .row and explicit == null and node.style.height != null)
933
+ node.style.height.? * ratio
934
+ else if (parent_direction == .column and explicit == null and node.style.width != null)
935
+ node.style.width.? / ratio
936
+ else
937
+ null
938
+ else
939
+ null;
940
+ const intrinsic = if (parent_direction == .row) node.style.intrinsic_width else node.style.intrinsic_height;
941
+ return clampMain(node, parent_direction, node.style.flex_basis orelse explicit orelse ratio_size orelse intrinsic orelse 0);
942
+ }
943
+
944
+ fn childCrossSize(node: Node, parent_direction: Axis, parent_width: f32, parent_height: f32, parent_alignment: AlignItems) f32 {
945
+ if (parent_direction == .row) {
946
+ const available = @max(0, parent_height - verticalMargins(node));
947
+ const ratio_size = if (validAspectRatio(node.style.aspect_ratio)) |ratio|
948
+ if (node.style.height == null and node.style.width != null) node.style.width.? / ratio else null
949
+ else
950
+ null;
951
+ return clampCross(node, parent_direction, node.style.height orelse ratio_size orelse node.style.intrinsic_height orelse if (effectiveAlignment(node, parent_alignment) == .stretch) available else 0);
952
+ }
953
+ const available = @max(0, parent_width - horizontalMargins(node));
954
+ const ratio_size = if (validAspectRatio(node.style.aspect_ratio)) |ratio|
955
+ if (node.style.width == null and node.style.height != null) node.style.height.? * ratio else null
956
+ else
957
+ null;
958
+ return clampCross(node, parent_direction, node.style.width orelse ratio_size orelse node.style.intrinsic_width orelse if (effectiveAlignment(node, parent_alignment) == .stretch) available else 0);
959
+ }
960
+
961
+ fn validAspectRatio(ratio: ?f32) ?f32 {
962
+ if (ratio) |value| {
963
+ if (std.math.isFinite(value) and value > 0) return value;
964
+ }
965
+ return null;
966
+ }
967
+
968
+ fn effectiveAlignment(node: Node, parent_alignment: AlignItems) AlignItems {
969
+ return node.style.align_self orelse parent_alignment;
970
+ }
971
+
972
+ fn crossOffset(
973
+ node: Node,
974
+ parent_direction: Axis,
975
+ child_cross: f32,
976
+ parent_width: f32,
977
+ parent_height: f32,
978
+ parent_alignment: AlignItems,
979
+ line_baseline: ?f32,
980
+ ) f32 {
981
+ const available = if (parent_direction == .row) parent_height else parent_width;
982
+ const margin_start = crossLeadingMargin(node, parent_direction);
983
+ const margin_end = if (parent_direction == .row) node.style.margin_bottom else node.style.margin_right;
984
+ const free = @max(0, available - margin_start - margin_end - child_cross);
985
+ return switch (effectiveAlignment(node, parent_alignment)) {
986
+ .center => margin_start + free / 2,
987
+ .flex_end => available - margin_end - child_cross,
988
+ .baseline => if (parent_direction == .row)
989
+ @max(margin_start, (line_baseline orelse child_cross) - (node.style.baseline orelse child_cross))
990
+ else
991
+ margin_start,
992
+ else => margin_start,
993
+ };
994
+ }
995
+
996
+ fn baselineForChildren(
997
+ children: []const Node,
998
+ direction: Axis,
999
+ parent_width: f32,
1000
+ parent_height: f32,
1001
+ line_cross: f32,
1002
+ parent_alignment: AlignItems,
1003
+ ) ?f32 {
1004
+ if (direction != .row) return null;
1005
+ var result: ?f32 = null;
1006
+ for (children) |child| {
1007
+ if (effectiveAlignment(child, parent_alignment) != .baseline) continue;
1008
+ const child_cross = childCrossSize(child, direction, parent_width, parent_height, parent_alignment);
1009
+ const measured = child.style.baseline orelse child_cross;
1010
+ const candidate = child.style.margin_top + @min(line_cross, measured);
1011
+ result = if (result) |current| @max(current, candidate) else candidate;
1012
+ }
1013
+ return result;
1014
+ }
1015
+
1016
+ fn mainLeadingMargin(node: Node, direction: Axis) f32 {
1017
+ return if (direction == .row) node.style.margin_left else node.style.margin_top;
1018
+ }
1019
+
1020
+ fn crossLeadingMargin(node: Node, direction: Axis) f32 {
1021
+ return if (direction == .row) node.style.margin_top else node.style.margin_left;
1022
+ }
1023
+
1024
+ fn mainMargins(node: Node, direction: Axis) f32 {
1025
+ return if (direction == .row)
1026
+ node.style.margin_left + node.style.margin_right
1027
+ else
1028
+ node.style.margin_top + node.style.margin_bottom;
1029
+ }
1030
+
1031
+ fn horizontalMargins(node: Node) f32 {
1032
+ return node.style.margin_left + node.style.margin_right;
1033
+ }
1034
+
1035
+ fn verticalMargins(node: Node) f32 {
1036
+ return node.style.margin_top + node.style.margin_bottom;
1037
+ }
1038
+
1039
+ fn positiveGrow(value: f32) f32 {
1040
+ return @max(0, value);
1041
+ }
1042
+
1043
+ fn positiveShrink(value: f32) f32 {
1044
+ return @max(0, value);
1045
+ }
1046
+
1047
+ fn clampMain(node: Node, direction: Axis, value: f32) f32 {
1048
+ return if (direction == .row)
1049
+ clamp(value, node.style.min_width, node.style.max_width)
1050
+ else
1051
+ clamp(value, node.style.min_height, node.style.max_height);
1052
+ }
1053
+
1054
+ fn clampCross(node: Node, direction: Axis, value: f32) f32 {
1055
+ return if (direction == .row)
1056
+ clamp(value, node.style.min_height, node.style.max_height)
1057
+ else
1058
+ clamp(value, node.style.min_width, node.style.max_width);
1059
+ }
1060
+
1061
+ fn clamp(value: f32, lower: ?f32, upper: ?f32) f32 {
1062
+ var result = finiteNonNegative(value);
1063
+ if (lower) |minimum| {
1064
+ if (std.math.isFinite(minimum) and minimum >= 0) result = @max(result, minimum);
1065
+ }
1066
+ if (upper) |maximum| {
1067
+ if (std.math.isFinite(maximum) and maximum >= 0) result = @min(result, maximum);
1068
+ }
1069
+ return result;
1070
+ }
1071
+
1072
+ fn finiteNonNegative(value: f32) f32 {
1073
+ return if (!std.math.isFinite(value) or value < 0) 0 else value;
1074
+ }
1075
+
1076
+ fn clampBounds(value: f32, lower: f32, upper: f32) f32 {
1077
+ var result = finiteNonNegative(value);
1078
+ if (std.math.isFinite(lower) and lower >= 0) result = @max(result, lower);
1079
+ if (std.math.isFinite(upper) and upper >= 0) result = @min(result, upper);
1080
+ return result;
1081
+ }
1082
+
1083
+ fn isUnset(value: f32) bool {
1084
+ return std.math.isNan(value) or value < 0;
1085
+ }
1086
+
1087
+ fn optionalDimension(explicit: f32, intrinsic: f32) f32 {
1088
+ return finiteNonNegative(if (isUnset(explicit)) intrinsic else explicit);
1089
+ }
1090
+
1091
+ test "distributes positive flex grow from a shared basis" {
1092
+ const children = [_]Node{
1093
+ .{ .style = .{ .flex_basis = 40, .flex_grow = 1 } },
1094
+ .{ .style = .{ .flex_basis = 20, .flex_grow = 2 } },
1095
+ };
1096
+ const root = Node{
1097
+ .style = .{ .direction = .row, .width = 180, .height = 48 },
1098
+ .children = &children,
1099
+ };
1100
+ var frames: [3]Frame = undefined;
1101
+
1102
+ const count = try layout(root, .{ .width = 180, .height = 48 }, &frames);
1103
+ try std.testing.expectEqual(@as(usize, 3), count);
1104
+ try std.testing.expectEqual(@as(f32, 0), frames[1].x);
1105
+ try std.testing.expectApproxEqAbs(@as(f32, 80), frames[1].width, 0.001);
1106
+ try std.testing.expectApproxEqAbs(@as(f32, 80), frames[2].x, 0.001);
1107
+ try std.testing.expectApproxEqAbs(@as(f32, 100), frames[2].width, 0.001);
1108
+ try std.testing.expectEqual(@as(f32, 48), frames[1].height);
1109
+ }
1110
+
1111
+ test "derives aspect ratio dimensions in flex and grid trees" {
1112
+ const flex_children = [_]Node{
1113
+ .{ .style = .{ .width = 40, .aspect_ratio = 2 } },
1114
+ };
1115
+ const flex_root = Node{
1116
+ .style = .{ .direction = .row, .width = 100, .height = 40, .align_items = .flex_start },
1117
+ .children = &flex_children,
1118
+ };
1119
+ var flex_frames: [2]Frame = undefined;
1120
+ _ = try layout(flex_root, .{ .width = 100, .height = 40 }, &flex_frames);
1121
+ try std.testing.expectApproxEqAbs(@as(f32, 40), flex_frames[1].width, 0.001);
1122
+ try std.testing.expectApproxEqAbs(@as(f32, 20), flex_frames[1].height, 0.001);
1123
+
1124
+ const grid_children = [_]Node{
1125
+ .{ .style = .{ .width = 40, .aspect_ratio = 2 } },
1126
+ };
1127
+ const grid_root = Node{
1128
+ .style = .{ .grid = true, .grid_columns = 1, .width = 100, .height = 60 },
1129
+ .children = &grid_children,
1130
+ };
1131
+ var grid_frames: [2]Frame = undefined;
1132
+ _ = try layout(grid_root, .{ .width = 100, .height = 60 }, &grid_frames);
1133
+ try std.testing.expectApproxEqAbs(@as(f32, 40), grid_frames[1].width, 0.001);
1134
+ try std.testing.expectApproxEqAbs(@as(f32, 20), grid_frames[1].height, 0.001);
1135
+ }
1136
+
1137
+ test "positions tree children with justify content" {
1138
+ const children = [_]Node{
1139
+ .{ .style = .{ .flex_basis = 20 } },
1140
+ .{ .style = .{ .flex_basis = 20 } },
1141
+ };
1142
+ const root = Node{
1143
+ .style = .{ .direction = .row, .width = 100, .height = 20, .justify_content = .space_evenly },
1144
+ .children = &children,
1145
+ };
1146
+ var frames: [3]Frame = undefined;
1147
+
1148
+ _ = try layout(root, .{ .width = 100, .height = 20 }, &frames);
1149
+ try std.testing.expectApproxEqAbs(@as(f32, 20), frames[1].x, 0.001);
1150
+ try std.testing.expectApproxEqAbs(@as(f32, 60), frames[2].x, 0.001);
1151
+ }
1152
+
1153
+ test "aligns tree children across the cross axis" {
1154
+ const children = [_]Node{
1155
+ .{ .style = .{ .width = 20, .height = 10, .align_self = .center } },
1156
+ .{ .style = .{ .width = 20, .height = 10, .align_self = .flex_end } },
1157
+ };
1158
+ const root = Node{
1159
+ .style = .{ .direction = .row, .width = 100, .height = 40, .align_items = .flex_start },
1160
+ .children = &children,
1161
+ };
1162
+ var frames: [3]Frame = undefined;
1163
+
1164
+ _ = try layout(root, .{ .width = 100, .height = 40 }, &frames);
1165
+ try std.testing.expectApproxEqAbs(@as(f32, 15), frames[1].y, 0.001);
1166
+ try std.testing.expectApproxEqAbs(@as(f32, 30), frames[2].y, 0.001);
1167
+ }
1168
+
1169
+ test "wraps tree children into stretched cross-axis lines" {
1170
+ const children = [_]Node{
1171
+ .{ .style = .{ .width = 60, .height = 10 } },
1172
+ .{ .style = .{ .width = 60, .height = 20 } },
1173
+ .{ .style = .{ .width = 20, .height = 30 } },
1174
+ };
1175
+ const root = Node{
1176
+ .style = .{ .direction = .row, .width = 100, .height = 80, .gap = 4, .flex_wrap = true },
1177
+ .children = &children,
1178
+ };
1179
+ var frames: [4]Frame = undefined;
1180
+
1181
+ _ = try layout(root, .{ .width = 100, .height = 80 }, &frames);
1182
+ try std.testing.expectApproxEqAbs(@as(f32, 0), frames[1].x, 0.001);
1183
+ try std.testing.expectApproxEqAbs(@as(f32, 32), frames[2].y, 0.001);
1184
+ try std.testing.expectApproxEqAbs(@as(f32, 64), frames[3].x, 0.001);
1185
+ try std.testing.expectApproxEqAbs(@as(f32, 32), frames[3].y, 0.001);
1186
+ }
1187
+
1188
+ test "distributes wrapped tree lines with align content" {
1189
+ const children = [_]Node{
1190
+ .{ .style = .{ .width = 60, .height = 10 } },
1191
+ .{ .style = .{ .width = 60, .height = 20 } },
1192
+ .{ .style = .{ .width = 20, .height = 30 } },
1193
+ };
1194
+ const root = Node{
1195
+ .style = .{ .direction = .row, .width = 100, .height = 100, .gap = 4, .flex_wrap = true, .align_content = .center },
1196
+ .children = &children,
1197
+ };
1198
+ var frames: [4]Frame = undefined;
1199
+
1200
+ _ = try layout(root, .{ .width = 100, .height = 100 }, &frames);
1201
+ try std.testing.expectApproxEqAbs(@as(f32, 28), frames[1].y, 0.001);
1202
+ try std.testing.expectApproxEqAbs(@as(f32, 42), frames[2].y, 0.001);
1203
+ }
1204
+
1205
+ test "aligns wrapped children using align self" {
1206
+ const children = [_]Node{
1207
+ .{ .style = .{ .width = 60, .height = 10 } },
1208
+ .{ .style = .{ .width = 60, .height = 20 } },
1209
+ .{ .style = .{ .width = 20, .height = 10, .align_self = .flex_end } },
1210
+ };
1211
+ const root = Node{
1212
+ .style = .{ .direction = .row, .width = 100, .height = 80, .gap = 4, .flex_wrap = true, .align_items = .flex_start, .align_content = .flex_start },
1213
+ .children = &children,
1214
+ };
1215
+ var frames: [4]Frame = undefined;
1216
+
1217
+ _ = try layout(root, .{ .width = 100, .height = 80 }, &frames);
1218
+ try std.testing.expectApproxEqAbs(@as(f32, 14), frames[2].y, 0.001);
1219
+ try std.testing.expectApproxEqAbs(@as(f32, 24), frames[3].y, 0.001);
1220
+ }
1221
+
1222
+ test "uses viewport size when the root is intrinsic" {
1223
+ var frames: [1]Frame = undefined;
1224
+ const count = try layout(.{}, .{ .width = 320, .height = 640 }, &frames);
1225
+ try std.testing.expectEqual(@as(usize, 1), count);
1226
+ try std.testing.expectEqual(@as(f32, 320), frames[0].width);
1227
+ try std.testing.expectEqual(@as(f32, 640), frames[0].height);
1228
+ }
1229
+
1230
+ test "uses host-measured intrinsic dimensions in flex, wrap, and grid trees" {
1231
+ const flex_children = [_]Node{
1232
+ .{ .style = .{ .intrinsic_width = 42, .intrinsic_height = 12 } },
1233
+ .{ .style = .{ .intrinsic_width = 18, .intrinsic_height = 8 } },
1234
+ };
1235
+ const flex_root = Node{
1236
+ .style = .{ .direction = .row, .width = 100, .height = 40, .gap = 4, .align_items = .flex_start },
1237
+ .children = &flex_children,
1238
+ };
1239
+ var flex_frames: [3]Frame = undefined;
1240
+ _ = try layout(flex_root, .{ .width = 100, .height = 40 }, &flex_frames);
1241
+ try std.testing.expectApproxEqAbs(@as(f32, 42), flex_frames[1].width, 0.001);
1242
+ try std.testing.expectApproxEqAbs(@as(f32, 12), flex_frames[1].height, 0.001);
1243
+ try std.testing.expectApproxEqAbs(@as(f32, 46), flex_frames[2].x, 0.001);
1244
+ try std.testing.expectApproxEqAbs(@as(f32, 18), flex_frames[2].width, 0.001);
1245
+
1246
+ const wrapped_children = [_]Node{
1247
+ .{ .style = .{ .intrinsic_width = 40, .intrinsic_height = 10 } },
1248
+ .{ .style = .{ .intrinsic_width = 30, .intrinsic_height = 12 } },
1249
+ };
1250
+ const wrapped_root = Node{
1251
+ .style = .{ .direction = .row, .width = 60, .height = 40, .gap = 4, .flex_wrap = true, .align_items = .flex_start, .align_content = .flex_start },
1252
+ .children = &wrapped_children,
1253
+ };
1254
+ var wrapped_frames: [3]Frame = undefined;
1255
+ _ = try layout(wrapped_root, .{ .width = 60, .height = 40 }, &wrapped_frames);
1256
+ try std.testing.expectApproxEqAbs(@as(f32, 40), wrapped_frames[1].width, 0.001);
1257
+ try std.testing.expectApproxEqAbs(@as(f32, 14), wrapped_frames[2].y, 0.001);
1258
+
1259
+ const grid_children = [_]Node{
1260
+ .{ .style = .{ .intrinsic_width = 20, .intrinsic_height = 11 } },
1261
+ .{ .style = .{ .intrinsic_width = 30, .intrinsic_height = 15 } },
1262
+ };
1263
+ const grid_root = Node{
1264
+ .style = .{ .grid = true, .grid_columns = 2, .width = 100, .height = 60, .align_items = .flex_start },
1265
+ .children = &grid_children,
1266
+ };
1267
+ var grid_frames: [3]Frame = undefined;
1268
+ _ = try layout(grid_root, .{ .width = 100, .height = 60 }, &grid_frames);
1269
+ try std.testing.expectApproxEqAbs(@as(f32, 20), grid_frames[1].width, 0.001);
1270
+ try std.testing.expectApproxEqAbs(@as(f32, 11), grid_frames[1].height, 0.001);
1271
+ try std.testing.expectApproxEqAbs(@as(f32, 30), grid_frames[2].width, 0.001);
1272
+ try std.testing.expectApproxEqAbs(@as(f32, 15), grid_frames[2].height, 0.001);
1273
+ }
1274
+
1275
+ test "aligns baseline children from host measurements" {
1276
+ const children = [_]Node{
1277
+ .{ .style = .{ .intrinsic_width = 20, .intrinsic_height = 20, .baseline = 15 } },
1278
+ .{ .style = .{ .intrinsic_width = 20, .intrinsic_height = 10, .baseline = 7 } },
1279
+ };
1280
+ const root = Node{
1281
+ .style = .{ .direction = .row, .width = 100, .height = 40, .gap = 4, .align_items = .baseline },
1282
+ .children = &children,
1283
+ };
1284
+ var frames: [3]Frame = undefined;
1285
+
1286
+ _ = try layout(root, .{ .width = 100, .height = 40 }, &frames);
1287
+ try std.testing.expectApproxEqAbs(@as(f32, 0), frames[1].y, 0.001);
1288
+ try std.testing.expectApproxEqAbs(@as(f32, 8), frames[2].y, 0.001);
1289
+ }
1290
+
1291
+ test "sanitizes non-finite tree dimensions" {
1292
+ const children = [_]Node{
1293
+ .{ .style = .{ .width = std.math.nan(f32), .height = std.math.inf(f32), .min_width = std.math.nan(f32), .max_height = std.math.inf(f32) } },
1294
+ };
1295
+ const root = Node{
1296
+ .style = .{ .width = std.math.nan(f32), .height = std.math.inf(f32) },
1297
+ .children = &children,
1298
+ };
1299
+ var frames: [2]Frame = undefined;
1300
+
1301
+ _ = try layout(root, .{ .width = 320, .height = 640 }, &frames);
1302
+ for (frames) |frame| {
1303
+ try std.testing.expect(std.math.isFinite(frame.x));
1304
+ try std.testing.expect(std.math.isFinite(frame.y));
1305
+ try std.testing.expect(std.math.isFinite(frame.width));
1306
+ try std.testing.expect(std.math.isFinite(frame.height));
1307
+ try std.testing.expect(frame.width >= 0);
1308
+ try std.testing.expect(frame.height >= 0);
1309
+ }
1310
+ }
1311
+
1312
+ test "recurses across row and column axes without host concepts" {
1313
+ const grandchildren = [_]Node{
1314
+ .{ .style = .{ .flex_basis = 10, .flex_grow = 1 } },
1315
+ .{ .style = .{ .flex_basis = 10, .flex_grow = 1 } },
1316
+ };
1317
+ const child = Node{
1318
+ .style = .{ .direction = .column, .width = 80, .height = 60 },
1319
+ .children = &grandchildren,
1320
+ };
1321
+ const children = [_]Node{child};
1322
+ const root = Node{
1323
+ .style = .{ .direction = .row, .width = 160, .height = 100 },
1324
+ .children = &children,
1325
+ };
1326
+ var frames: [4]Frame = undefined;
1327
+
1328
+ const count = try layout(root, .{ .width = 160, .height = 100 }, &frames);
1329
+ try std.testing.expectEqual(@as(usize, 4), count);
1330
+ try std.testing.expectEqual(@as(f32, 80), frames[1].width);
1331
+ try std.testing.expectEqual(@as(f32, 30), frames[2].height);
1332
+ try std.testing.expectEqual(@as(f32, 30), frames[3].height);
1333
+ try std.testing.expectEqual(@as(f32, 30), frames[3].y);
1334
+ }
1335
+
1336
+ test "applies gap and shrink without crossing minimum bounds" {
1337
+ const children = [_]Node{
1338
+ .{ .style = .{ .flex_basis = 80, .flex_shrink = 1, .min_width = 60 } },
1339
+ .{ .style = .{ .flex_basis = 80, .flex_shrink = 1 } },
1340
+ };
1341
+ const root = Node{
1342
+ .style = .{ .direction = .row, .width = 130, .height = 20, .gap = 10 },
1343
+ .children = &children,
1344
+ };
1345
+ var frames: [3]Frame = undefined;
1346
+
1347
+ _ = try layout(root, .{ .width = 130, .height = 20 }, &frames);
1348
+ try std.testing.expectApproxEqAbs(@as(f32, 60), frames[1].width, 0.001);
1349
+ try std.testing.expectApproxEqAbs(@as(f32, 60), frames[2].width, 0.001);
1350
+ try std.testing.expectApproxEqAbs(@as(f32, 70), frames[2].x, 0.001);
1351
+ }
1352
+
1353
+ test "clamps positive flex growth to a maximum" {
1354
+ const children = [_]Node{
1355
+ .{ .style = .{ .flex_basis = 20, .flex_grow = 1, .max_width = 40 } },
1356
+ .{ .style = .{ .flex_basis = 20, .flex_grow = 1 } },
1357
+ };
1358
+ const root = Node{
1359
+ .style = .{ .direction = .row, .width = 140, .height = 20 },
1360
+ .children = &children,
1361
+ };
1362
+ var frames: [3]Frame = undefined;
1363
+
1364
+ _ = try layout(root, .{ .width = 140, .height = 20 }, &frames);
1365
+ try std.testing.expectApproxEqAbs(@as(f32, 40), frames[1].width, 0.001);
1366
+ try std.testing.expectApproxEqAbs(@as(f32, 100), frames[2].width, 0.001);
1367
+ }
1368
+
1369
+ test "redistributes shrink space after a minimum bound freezes" {
1370
+ const children = [_]Node{
1371
+ .{ .style = .{ .flex_basis = 80, .flex_shrink = 1, .min_width = 70 } },
1372
+ .{ .style = .{ .flex_basis = 80, .flex_shrink = 1 } },
1373
+ };
1374
+ const root = Node{
1375
+ .style = .{ .direction = .row, .width = 100, .height = 20 },
1376
+ .children = &children,
1377
+ };
1378
+ var frames: [3]Frame = undefined;
1379
+
1380
+ _ = try layout(root, .{ .width = 100, .height = 20 }, &frames);
1381
+ try std.testing.expectApproxEqAbs(@as(f32, 70), frames[1].width, 0.001);
1382
+ try std.testing.expectApproxEqAbs(@as(f32, 30), frames[2].width, 0.001);
1383
+ }
1384
+
1385
+ test "places equal grid tracks and max-content rows" {
1386
+ const children = [_]Node{
1387
+ .{ .style = .{ .height = 20 } },
1388
+ .{ .style = .{ .height = 30 } },
1389
+ .{ .style = .{ .height = 10 } },
1390
+ };
1391
+ const root = Node{
1392
+ .style = .{ .grid = true, .grid_columns = 2, .width = 100, .height = 100, .column_gap = 10, .row_gap = 5 },
1393
+ .children = &children,
1394
+ };
1395
+ var frames: [4]Frame = undefined;
1396
+
1397
+ _ = try layout(root, .{ .width = 100, .height = 100 }, &frames);
1398
+ try std.testing.expectEqual(@as(f32, 45), frames[1].width);
1399
+ try std.testing.expectEqual(@as(f32, 0), frames[1].x);
1400
+ try std.testing.expectEqual(@as(f32, 55), frames[2].x);
1401
+ try std.testing.expectEqual(@as(f32, 20), frames[1].height);
1402
+ try std.testing.expectEqual(@as(f32, 30), frames[2].height);
1403
+ try std.testing.expectEqual(@as(f32, 35), frames[3].y);
1404
+ }
1405
+
1406
+ test "aligns shorter grid children inside intrinsic rows" {
1407
+ const children = [_]Node{
1408
+ .{ .style = .{ .height = 30 } },
1409
+ .{ .style = .{ .height = 10 } },
1410
+ };
1411
+ const root = Node{
1412
+ .style = .{ .grid = true, .grid_columns = 2, .width = 100, .height = 40, .align_items = .center },
1413
+ .children = &children,
1414
+ };
1415
+ var frames: [3]Frame = undefined;
1416
+
1417
+ _ = try layout(root, .{ .width = 100, .height = 40 }, &frames);
1418
+ try std.testing.expectApproxEqAbs(@as(f32, 10), frames[2].y, 0.001);
1419
+ }
1420
+
1421
+ test "accounts for per-edge margins in flex and grid frames" {
1422
+ const flex_children = [_]Node{
1423
+ .{ .style = .{ .flex_basis = 20, .margin_left = 5, .margin_right = 7, .margin_top = 3, .margin_bottom = 4 } },
1424
+ .{ .style = .{ .flex_basis = 20 } },
1425
+ };
1426
+ const flex_root = Node{
1427
+ .style = .{ .direction = .row, .width = 100, .height = 40 },
1428
+ .children = &flex_children,
1429
+ };
1430
+ var flex_frames: [3]Frame = undefined;
1431
+ _ = try layout(flex_root, .{ .width = 100, .height = 40 }, &flex_frames);
1432
+ try std.testing.expectEqual(@as(f32, 5), flex_frames[1].x);
1433
+ try std.testing.expectEqual(@as(f32, 3), flex_frames[1].y);
1434
+ try std.testing.expectEqual(@as(f32, 33), flex_frames[1].height);
1435
+ try std.testing.expectEqual(@as(f32, 32), flex_frames[2].x);
1436
+
1437
+ const grid_children = [_]Node{
1438
+ .{ .style = .{ .width = 60, .height = 10, .margin_left = 2, .margin_right = 3, .margin_top = 4, .margin_bottom = 5 } },
1439
+ };
1440
+ const grid_root = Node{
1441
+ .style = .{ .grid = true, .grid_columns = 2, .width = 100, .height = 40, .column_gap = 10 },
1442
+ .children = &grid_children,
1443
+ };
1444
+ var grid_frames: [2]Frame = undefined;
1445
+ _ = try layout(grid_root, .{ .width = 100, .height = 40 }, &grid_frames);
1446
+ try std.testing.expectEqual(@as(f32, 2), grid_frames[1].x);
1447
+ try std.testing.expectEqual(@as(f32, 4), grid_frames[1].y);
1448
+ try std.testing.expectEqual(@as(f32, 40), grid_frames[1].width);
1449
+ try std.testing.expectEqual(@as(f32, 10), grid_frames[1].height);
1450
+ }
1451
+
1452
+ test "insets flex and grid children by parent padding" {
1453
+ const children = [_]Node{
1454
+ .{ .style = .{ .flex_basis = 20 } },
1455
+ .{ .style = .{ .flex_basis = 20 } },
1456
+ };
1457
+ const flex_root = Node{
1458
+ .style = .{ .direction = .row, .width = 100, .height = 40, .padding_left = 8, .padding_top = 4, .padding_right = 12, .padding_bottom = 6 },
1459
+ .children = &children,
1460
+ };
1461
+ var flex_frames: [3]Frame = undefined;
1462
+ _ = try layout(flex_root, .{ .width = 100, .height = 40 }, &flex_frames);
1463
+ try std.testing.expectEqual(@as(f32, 8), flex_frames[1].x);
1464
+ try std.testing.expectEqual(@as(f32, 4), flex_frames[1].y);
1465
+ try std.testing.expectEqual(@as(f32, 30), flex_frames[1].height);
1466
+
1467
+ const grid_children = [_]Node{.{ .style = .{ .height = 10 } }};
1468
+ const grid_root = Node{
1469
+ .style = .{ .grid = true, .grid_columns = 1, .width = 80, .height = 40, .padding_left = 5, .padding_top = 3, .padding_right = 7, .padding_bottom = 4 },
1470
+ .children = &grid_children,
1471
+ };
1472
+ var grid_frames: [2]Frame = undefined;
1473
+ _ = try layout(grid_root, .{ .width = 80, .height = 40 }, &grid_frames);
1474
+ try std.testing.expectEqual(@as(f32, 5), grid_frames[1].x);
1475
+ try std.testing.expectEqual(@as(f32, 3), grid_frames[1].y);
1476
+ try std.testing.expectEqual(@as(f32, 68), grid_frames[1].width);
1477
+ }
1478
+
1479
+ test "exports the shared flex row contract with margins and bounds" {
1480
+ const items = [_]FlexItem{
1481
+ .{ .basis = 40, .grow = 1, .shrink = 1, .min = 30, .max = 70, .margin_start = 5, .margin_end = 7 },
1482
+ .{ .basis = 20, .grow = 2, .shrink = 1, .min = -1, .max = -1, .margin_start = 3, .margin_end = 4 },
1483
+ };
1484
+ var widths: [2]f32 = undefined;
1485
+ var positions: [2]f32 = undefined;
1486
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_flex_row(180, 10, &items, items.len, &widths, &positions));
1487
+ try std.testing.expectApproxEqAbs(@as(f32, 70), widths[0], 0.001);
1488
+ try std.testing.expectApproxEqAbs(@as(f32, 81), widths[1], 0.001);
1489
+ try std.testing.expectApproxEqAbs(@as(f32, 5), positions[0], 0.001);
1490
+ try std.testing.expectApproxEqAbs(@as(f32, 95), positions[1], 0.001);
1491
+ }
1492
+
1493
+ test "exports justified flex positions" {
1494
+ const items = [_]FlexItem{
1495
+ .{ .basis = 20, .grow = 0, .shrink = 0, .min = -1, .max = -1, .margin_start = 0, .margin_end = 0 },
1496
+ .{ .basis = 20, .grow = 0, .shrink = 0, .min = -1, .max = -1, .margin_start = 0, .margin_end = 0 },
1497
+ };
1498
+ var widths: [2]f32 = undefined;
1499
+ var positions: [2]f32 = undefined;
1500
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_flex_row_justified(100, 10, 1, &items, items.len, &widths, &positions));
1501
+ try std.testing.expectApproxEqAbs(@as(f32, 20), widths[0], 0.001);
1502
+ try std.testing.expectApproxEqAbs(@as(f32, 25), positions[0], 0.001);
1503
+ try std.testing.expectApproxEqAbs(@as(f32, 55), positions[1], 0.001);
1504
+ }
1505
+
1506
+ test "shared flex ABI redistributes after a bound freezes" {
1507
+ const items = [_]FlexItem{
1508
+ .{ .basis = 20, .grow = 1, .shrink = 1, .min = -1, .max = 40, .margin_start = 0, .margin_end = 0 },
1509
+ .{ .basis = 20, .grow = 1, .shrink = 1, .min = -1, .max = -1, .margin_start = 0, .margin_end = 0 },
1510
+ };
1511
+ var widths: [2]f32 = undefined;
1512
+ var positions: [2]f32 = undefined;
1513
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_flex_row_justified(140, 0, 0, &items, items.len, &widths, &positions));
1514
+ try std.testing.expectApproxEqAbs(@as(f32, 40), widths[0], 0.001);
1515
+ try std.testing.expectApproxEqAbs(@as(f32, 100), widths[1], 0.001);
1516
+ }
1517
+
1518
+ test "exports shared cross-axis alignment positions" {
1519
+ try std.testing.expectApproxEqAbs(@as(f32, 7), craft_native_cross_axis_position(100, 30, 7, 9, 0), 0.001);
1520
+ try std.testing.expectApproxEqAbs(@as(f32, 34), craft_native_cross_axis_position(100, 30, 7, 9, 1), 0.001);
1521
+ try std.testing.expectApproxEqAbs(@as(f32, 61), craft_native_cross_axis_position(100, 30, 7, 9, 2), 0.001);
1522
+ try std.testing.expectApproxEqAbs(@as(f32, 11), craft_native_cross_axis_position(24, 13, 4, 0, 3), 0.001);
1523
+ try std.testing.expectApproxEqAbs(@as(f32, 4), craft_native_cross_axis_position(10, 13, 4, 0, 3), 0.001);
1524
+ try std.testing.expect(std.math.isFinite(craft_native_cross_axis_position(std.math.inf(f32), std.math.nan(f32), -1, std.math.inf(f32), 1)));
1525
+ }
1526
+
1527
+ test "exports wrapped flex line breaks" {
1528
+ const items = [_]FlexLineItem{
1529
+ .{ .basis = 40, .margin_start = 2, .margin_end = 2 },
1530
+ .{ .basis = 50, .margin_start = 0, .margin_end = 0 },
1531
+ .{ .basis = 20, .margin_start = 1, .margin_end = 1 },
1532
+ };
1533
+ var line_ids: [3]i32 = undefined;
1534
+ try std.testing.expectEqual(@as(c_int, 2), craft_native_flex_line_breaks(100, 5, 1, &items, items.len, &line_ids));
1535
+ try std.testing.expectEqual(@as(i32, 0), line_ids[0]);
1536
+ try std.testing.expectEqual(@as(i32, 0), line_ids[1]);
1537
+ try std.testing.expectEqual(@as(i32, 1), line_ids[2]);
1538
+ try std.testing.expectEqual(@as(c_int, 1), craft_native_flex_line_breaks(100, 5, 0, &items, items.len, &line_ids));
1539
+ try std.testing.expectEqual(@as(i32, 0), line_ids[2]);
1540
+ }
1541
+
1542
+ test "exports wrapped flex line distribution" {
1543
+ const line_crosses = [_]f32{ 20, 30 };
1544
+ var offsets: [2]f32 = undefined;
1545
+ var sizes: [2]f32 = undefined;
1546
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_flex_line_distribution(100, 5, 3, &line_crosses, line_crosses.len, &offsets, &sizes));
1547
+ try std.testing.expectApproxEqAbs(@as(f32, 0), offsets[0], 0.001);
1548
+ try std.testing.expectApproxEqAbs(@as(f32, 70), offsets[1], 0.001);
1549
+ try std.testing.expectApproxEqAbs(@as(f32, 20), sizes[0], 0.001);
1550
+ try std.testing.expectApproxEqAbs(@as(f32, 30), sizes[1], 0.001);
1551
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_flex_line_distribution(100, 5, 5, &line_crosses, line_crosses.len, &offsets, &sizes));
1552
+ try std.testing.expectApproxEqAbs(@as(f32, 42.5), sizes[0], 0.001);
1553
+ try std.testing.expectApproxEqAbs(@as(f32, 52.5), sizes[1], 0.001);
1554
+ }
1555
+
1556
+ test "exports evenly distributed wrapped flex lines" {
1557
+ const line_crosses = [_]f32{ 10, 10 };
1558
+ var offsets: [2]f32 = undefined;
1559
+ var sizes: [2]f32 = undefined;
1560
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_flex_line_distribution(70, 0, 6, &line_crosses, line_crosses.len, &offsets, &sizes));
1561
+ try std.testing.expectApproxEqAbs(@as(f32, 16.666), offsets[0], 0.01);
1562
+ try std.testing.expectApproxEqAbs(@as(f32, 43.333), offsets[1], 0.01);
1563
+ try std.testing.expectApproxEqAbs(@as(f32, 10), sizes[0], 0.001);
1564
+ try std.testing.expectApproxEqAbs(@as(f32, 10), sizes[1], 0.001);
1565
+ }
1566
+
1567
+ test "sanitizes non-finite flex inputs" {
1568
+ const items = [_]FlexItem{
1569
+ .{ .basis = std.math.inf(f32), .grow = 0, .shrink = 0, .min = -1, .max = -1, .margin_start = 0, .margin_end = 0 },
1570
+ .{ .basis = 20, .grow = 0, .shrink = 0, .min = -1, .max = -1, .margin_start = 0, .margin_end = 0 },
1571
+ };
1572
+ var widths: [2]f32 = undefined;
1573
+ var positions: [2]f32 = undefined;
1574
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_flex_row_justified(std.math.inf(f32), std.math.inf(f32), 3, &items, items.len, &widths, &positions));
1575
+ try std.testing.expectEqual(@as(f32, 0), widths[0]);
1576
+ try std.testing.expectEqual(@as(f32, 20), widths[1]);
1577
+ try std.testing.expect(std.math.isFinite(positions[0]));
1578
+ try std.testing.expect(std.math.isFinite(positions[1]));
1579
+ }
1580
+
1581
+ test "exports equal-track grid frames with intrinsic rows and alignment" {
1582
+ const items = [_]GridItem{
1583
+ .{ .intrinsic_width = 20, .intrinsic_height = 12, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 2, .margin_right = 3, .margin_top = 1, .margin_bottom = 2, .stretch = 0, .alignment = 1 },
1584
+ .{ .intrinsic_width = 10, .intrinsic_height = 20, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 1, .alignment = 0 },
1585
+ .{ .intrinsic_width = 30, .intrinsic_height = 10, .explicit_width = 40, .explicit_height = 10, .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 2 },
1586
+ };
1587
+ var frames: [3]Frame = undefined;
1588
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_grid(100, 80, 2, 10, 5, &items, items.len, &frames));
1589
+ try std.testing.expectApproxEqAbs(@as(f32, 12), frames[0].x, 0.001);
1590
+ try std.testing.expectApproxEqAbs(@as(f32, 20), frames[0].width, 0.001);
1591
+ try std.testing.expectApproxEqAbs(@as(f32, 55), frames[1].x, 0.001);
1592
+ try std.testing.expectApproxEqAbs(@as(f32, 45), frames[1].width, 0.001);
1593
+ try std.testing.expectApproxEqAbs(@as(f32, 25), frames[2].y, 0.001);
1594
+ try std.testing.expectApproxEqAbs(@as(f32, 40), frames[2].width, 0.001);
1595
+ }
1596
+
1597
+ test "distributes leftover grid rows with align content" {
1598
+ const items = [_]GridItem{
1599
+ .{ .intrinsic_width = 20, .intrinsic_height = 10, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 0 },
1600
+ .{ .intrinsic_width = 20, .intrinsic_height = 10, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 0 },
1601
+ };
1602
+ var frames: [2]Frame = undefined;
1603
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_grid_aligned(100, 100, 1, 0, 4, 1, &items, items.len, &frames));
1604
+ try std.testing.expectApproxEqAbs(@as(f32, 38), frames[0].y, 0.001);
1605
+ try std.testing.expectApproxEqAbs(@as(f32, 52), frames[1].y, 0.001);
1606
+ }
1607
+
1608
+ test "distributes grid rows evenly with align content" {
1609
+ const items = [_]GridItem{
1610
+ .{ .intrinsic_width = 20, .intrinsic_height = 10, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 0 },
1611
+ .{ .intrinsic_width = 20, .intrinsic_height = 10, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 0 },
1612
+ };
1613
+ var frames: [2]Frame = undefined;
1614
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_grid_aligned(100, 70, 1, 0, 0, 6, &items, items.len, &frames));
1615
+ try std.testing.expectApproxEqAbs(@as(f32, 16.666), frames[0].y, 0.01);
1616
+ try std.testing.expectApproxEqAbs(@as(f32, 43.333), frames[1].y, 0.01);
1617
+ }
1618
+
1619
+ test "distributes auto rows with grid align content" {
1620
+ const items = [_]GridItem{
1621
+ .{ .intrinsic_width = 20, .intrinsic_height = 4, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 0 },
1622
+ .{ .intrinsic_width = 20, .intrinsic_height = 4, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 0 },
1623
+ };
1624
+ var frames: [2]Frame = undefined;
1625
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_grid_auto_rows_aligned(100, 100, 1, 0, 4, 1, 16, &items, items.len, &frames));
1626
+ try std.testing.expectApproxEqAbs(@as(f32, 32), frames[0].y, 0.001);
1627
+ try std.testing.expectApproxEqAbs(@as(f32, 52), frames[1].y, 0.001);
1628
+ try std.testing.expectApproxEqAbs(@as(f32, 4), frames[0].height, 0.001);
1629
+ }
1630
+
1631
+ test "sanitizes non-finite grid bounds" {
1632
+ const items = [_]GridItem{
1633
+ .{ .intrinsic_width = 12, .intrinsic_height = 8, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.inf(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 0, .alignment = 0 },
1634
+ };
1635
+ var frames: [1]Frame = undefined;
1636
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_grid(100, 40, 1, 0, 0, &items, items.len, &frames));
1637
+ try std.testing.expect(std.math.isFinite(frames[0].width));
1638
+ try std.testing.expect(std.math.isFinite(frames[0].height));
1639
+ }
1640
+
1641
+ test "applies a shared minimum to every grid auto row" {
1642
+ const items = [_]GridItem{
1643
+ .{ .intrinsic_width = 12, .intrinsic_height = 8, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 1, .alignment = 0 },
1644
+ .{ .intrinsic_width = 12, .intrinsic_height = 10, .explicit_width = std.math.nan(f32), .explicit_height = std.math.nan(f32), .min_width = std.math.nan(f32), .max_width = std.math.nan(f32), .min_height = std.math.nan(f32), .max_height = std.math.nan(f32), .margin_left = 0, .margin_right = 0, .margin_top = 0, .margin_bottom = 0, .stretch = 1, .alignment = 0 },
1645
+ };
1646
+ var frames: [2]Frame = undefined;
1647
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_grid_auto_rows(100, 100, 1, 0, 5, 24, &items, items.len, &frames));
1648
+ try std.testing.expectApproxEqAbs(@as(f32, 24), frames[0].height, 0.001);
1649
+ try std.testing.expectApproxEqAbs(@as(f32, 29), frames[1].y, 0.001);
1650
+ try std.testing.expectApproxEqAbs(@as(f32, 24), frames[1].height, 0.001);
1651
+ }