craft-native 0.0.171 → 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.
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +6 -0
- package/dist/ios/templates/CraftHybrid.swift +33 -18
- package/dist/ios/templates/CraftNativeScreen.swift +56 -6
- package/dist/macos/src/index.d.ts +16 -0
- package/dist/macos/src/index.js +75 -0
- package/dist/macos/templates/CraftMacApplication.swift +342 -0
- package/dist/macos/templates/CraftMacRenderer.swift +481 -0
- package/dist/macos/templates/CraftMacRuntime.swift +266 -0
- package/dist/macos/templates/CraftNativeMutation.swift +390 -0
- package/dist/macos/templates/native_layout.zig +1651 -0
- package/package.json +9 -1
|
@@ -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
|
+
}
|