craft-native 0.0.187 → 0.0.189

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.
@@ -60,6 +60,8 @@ pub const Frame = extern struct {
60
60
 
61
61
  pub const Style = struct {
62
62
  direction: Axis = .row,
63
+ reverse: bool = false,
64
+ wrap_reverse: bool = false,
63
65
  width: ?f32 = null,
64
66
  height: ?f32 = null,
65
67
  /// Host-measured natural dimensions. These are deliberately plain numbers
@@ -196,6 +198,51 @@ pub const LayoutNodeInput = extern struct {
196
198
  child_count: u32,
197
199
  };
198
200
 
201
+ /// Versioned tree input used when a host needs reverse main-axis placement.
202
+ /// The original LayoutNodeInput remains unchanged so older runtimes can keep
203
+ /// consuming the v1 export without interpreting a different stride.
204
+ pub const LayoutStyleInputV2 = extern struct {
205
+ direction: u32,
206
+ width: f32,
207
+ height: f32,
208
+ intrinsic_width: f32,
209
+ intrinsic_height: f32,
210
+ baseline: f32,
211
+ aspect_ratio: f32,
212
+ flex_basis: f32,
213
+ flex_grow: f32,
214
+ flex_shrink: f32,
215
+ min_width: f32,
216
+ max_width: f32,
217
+ min_height: f32,
218
+ max_height: f32,
219
+ gap: f32,
220
+ justify_content: u32,
221
+ align_items: u32,
222
+ align_self: u32,
223
+ flex_wrap: u32,
224
+ align_content: u32,
225
+ margin_top: f32,
226
+ margin_right: f32,
227
+ margin_bottom: f32,
228
+ margin_left: f32,
229
+ padding_top: f32,
230
+ padding_right: f32,
231
+ padding_bottom: f32,
232
+ padding_left: f32,
233
+ grid: u32,
234
+ grid_columns: u32,
235
+ row_gap: f32,
236
+ column_gap: f32,
237
+ reverse: u32,
238
+ };
239
+
240
+ pub const LayoutNodeInputV2 = extern struct {
241
+ style: LayoutStyleInputV2,
242
+ first_child: u32,
243
+ child_count: u32,
244
+ };
245
+
199
246
  const max_layout_nodes: usize = 65_536;
200
247
 
201
248
  /// Resolve an entire native view tree using the same layout rules used by the
@@ -239,6 +286,81 @@ pub export fn craft_native_layout_tree(
239
286
  return if (result == node_count) 0 else -1;
240
287
  }
241
288
 
289
+ /// Versioned tree layout entry point with reverse main-axis support. Keep the
290
+ /// v1 symbol above for runtimes built before the reverse flag existed.
291
+ pub export fn craft_native_layout_tree_v2(
292
+ viewport_width: f32,
293
+ viewport_height: f32,
294
+ nodes: [*]const LayoutNodeInputV2,
295
+ node_count: usize,
296
+ frames: [*]Frame,
297
+ frame_count: usize,
298
+ ) callconv(.c) c_int {
299
+ if (node_count == 0 or node_count > max_layout_nodes or frame_count < node_count) return -1;
300
+
301
+ const input = nodes[0..node_count];
302
+ const visited = std.heap.page_allocator.alloc(bool, node_count) catch return -1;
303
+ defer std.heap.page_allocator.free(visited);
304
+ @memset(visited, false);
305
+ if (!validateLayoutTreeV2(input, 0, visited)) return -1;
306
+
307
+ var tree = std.heap.page_allocator.alloc(Node, node_count) catch return -1;
308
+ defer std.heap.page_allocator.free(tree);
309
+ for (input, 0..) |item, index| {
310
+ const first = @as(usize, @intCast(item.first_child));
311
+ const count = @as(usize, @intCast(item.child_count));
312
+ tree[index] = .{
313
+ .style = layoutStyleV2(item.style),
314
+ .children = tree[first .. first + count],
315
+ };
316
+ }
317
+
318
+ const result = layout(
319
+ tree[0],
320
+ .{ .width = finiteNonNegative(viewport_width), .height = finiteNonNegative(viewport_height) },
321
+ frames[0..frame_count],
322
+ ) catch return -1;
323
+ return if (result == node_count) 0 else -1;
324
+ }
325
+
326
+ /// v3 tree layout entry point. It reuses the v2 payload shape but gives the
327
+ /// trailing reverse field a second bit for wrap-reverse cross-axis placement;
328
+ /// v2 keeps treating any non-zero value as main-axis reverse.
329
+ pub export fn craft_native_layout_tree_v3(
330
+ viewport_width: f32,
331
+ viewport_height: f32,
332
+ nodes: [*]const LayoutNodeInputV2,
333
+ node_count: usize,
334
+ frames: [*]Frame,
335
+ frame_count: usize,
336
+ ) callconv(.c) c_int {
337
+ if (node_count == 0 or node_count > max_layout_nodes or frame_count < node_count) return -1;
338
+
339
+ const input = nodes[0..node_count];
340
+ const visited = std.heap.page_allocator.alloc(bool, node_count) catch return -1;
341
+ defer std.heap.page_allocator.free(visited);
342
+ @memset(visited, false);
343
+ if (!validateLayoutTreeV2(input, 0, visited)) return -1;
344
+
345
+ var tree = std.heap.page_allocator.alloc(Node, node_count) catch return -1;
346
+ defer std.heap.page_allocator.free(tree);
347
+ for (input, 0..) |item, index| {
348
+ const first = @as(usize, @intCast(item.first_child));
349
+ const count = @as(usize, @intCast(item.child_count));
350
+ tree[index] = .{
351
+ .style = layoutStyleV3(item.style),
352
+ .children = tree[first .. first + count],
353
+ };
354
+ }
355
+
356
+ const result = layout(
357
+ tree[0],
358
+ .{ .width = finiteNonNegative(viewport_width), .height = finiteNonNegative(viewport_height) },
359
+ frames[0..frame_count],
360
+ ) catch return -1;
361
+ return if (result == node_count) 0 else -1;
362
+ }
363
+
242
364
  fn validateLayoutTree(input: []const LayoutNodeInput, index: usize, visited: []bool) bool {
243
365
  if (index >= input.len or visited[index]) return false;
244
366
  visited[index] = true;
@@ -252,9 +374,61 @@ fn validateLayoutTree(input: []const LayoutNodeInput, index: usize, visited: []b
252
374
  return true;
253
375
  }
254
376
 
377
+ fn validateLayoutTreeV2(input: []const LayoutNodeInputV2, index: usize, visited: []bool) bool {
378
+ if (index >= input.len or visited[index]) return false;
379
+ visited[index] = true;
380
+ const item = input[index];
381
+ const first = @as(usize, @intCast(item.first_child));
382
+ const count = @as(usize, @intCast(item.child_count));
383
+ if (first > input.len or count > input.len - first or (count > 0 and first <= index)) return false;
384
+ for (first..first + count) |child| {
385
+ if (!validateLayoutTreeV2(input, child, visited)) return false;
386
+ }
387
+ return true;
388
+ }
389
+
255
390
  fn layoutStyle(input: LayoutStyleInput) Style {
256
391
  return .{
257
392
  .direction = if (input.direction == 1) .column else .row,
393
+ .reverse = false,
394
+ .width = optionalFinite(input.width),
395
+ .height = optionalFinite(input.height),
396
+ .intrinsic_width = optionalFinite(input.intrinsic_width),
397
+ .intrinsic_height = optionalFinite(input.intrinsic_height),
398
+ .baseline = optionalFinite(input.baseline),
399
+ .aspect_ratio = optionalFinite(input.aspect_ratio),
400
+ .flex_basis = optionalFinite(input.flex_basis),
401
+ .flex_grow = finiteNonNegative(input.flex_grow),
402
+ .flex_shrink = finiteNonNegative(input.flex_shrink),
403
+ .min_width = optionalFinite(input.min_width),
404
+ .max_width = optionalFinite(input.max_width),
405
+ .min_height = optionalFinite(input.min_height),
406
+ .max_height = optionalFinite(input.max_height),
407
+ .gap = finiteNonNegative(input.gap),
408
+ .justify_content = @fromBackingInt(@intCast(@min(input.justify_content, 5))),
409
+ .align_items = @fromBackingInt(@intCast(@min(input.align_items, 4))),
410
+ .align_self = if (input.align_self == 0) null else @fromBackingInt(@intCast(@min(input.align_self - 1, 4))),
411
+ .flex_wrap = input.flex_wrap != 0,
412
+ .align_content = @fromBackingInt(@intCast(@min(input.align_content, 6))),
413
+ .margin_top = finiteNonNegative(input.margin_top),
414
+ .margin_right = finiteNonNegative(input.margin_right),
415
+ .margin_bottom = finiteNonNegative(input.margin_bottom),
416
+ .margin_left = finiteNonNegative(input.margin_left),
417
+ .padding_top = finiteNonNegative(input.padding_top),
418
+ .padding_right = finiteNonNegative(input.padding_right),
419
+ .padding_bottom = finiteNonNegative(input.padding_bottom),
420
+ .padding_left = finiteNonNegative(input.padding_left),
421
+ .grid = input.grid != 0,
422
+ .grid_columns = @max(@as(u32, 1), input.grid_columns),
423
+ .row_gap = finiteNonNegative(input.row_gap),
424
+ .column_gap = finiteNonNegative(input.column_gap),
425
+ };
426
+ }
427
+
428
+ fn layoutStyleV2(input: LayoutStyleInputV2) Style {
429
+ return .{
430
+ .direction = if (input.direction == 1) .column else .row,
431
+ .reverse = input.reverse != 0,
258
432
  .width = optionalFinite(input.width),
259
433
  .height = optionalFinite(input.height),
260
434
  .intrinsic_width = optionalFinite(input.intrinsic_width),
@@ -289,6 +463,13 @@ fn layoutStyle(input: LayoutStyleInput) Style {
289
463
  };
290
464
  }
291
465
 
466
+ fn layoutStyleV3(input: LayoutStyleInputV2) Style {
467
+ var style = layoutStyleV2(input);
468
+ style.reverse = (input.reverse & 1) != 0;
469
+ style.wrap_reverse = (input.reverse & 2) != 0;
470
+ return style;
471
+ }
472
+
292
473
  fn optionalFinite(value: f32) ?f32 {
293
474
  return if (std.math.isFinite(value)) value else null;
294
475
  }
@@ -792,13 +973,17 @@ fn layoutNode(
792
973
  node.style.align_items,
793
974
  line_baseline,
794
975
  );
795
- const leading_margin = mainLeadingMargin(child, node.style.direction);
976
+ const leading_margin = mainFlowStartMargin(child, node.style.direction, node.style.reverse);
796
977
  const child_size = if (node.style.direction == .row)
797
978
  Size{ .width = child_main, .height = child_cross }
798
979
  else
799
980
  Size{ .width = child_cross, .height = child_main };
800
- const child_x = if (node.style.direction == .row) content_x + main_offset + leading_margin else content_x + child_cross_offset;
801
- const child_y = if (node.style.direction == .row) content_y + child_cross_offset else content_y + main_offset + leading_margin;
981
+ const main_position = if (node.style.reverse)
982
+ main_size - main_offset - leading_margin - child_main
983
+ else
984
+ main_offset + leading_margin;
985
+ const child_x = if (node.style.direction == .row) content_x + main_position else content_x + child_cross_offset;
986
+ const child_y = if (node.style.direction == .row) content_y + child_cross_offset else content_y + main_position;
802
987
  next_index = try layoutNode(child, child_size, child_x, child_y, next_index, frames);
803
988
  main_offset += mainMargins(child, node.style.direction) + child_main + if (index + 1 < node.children.len) between_space else 0;
804
989
  }
@@ -876,13 +1061,21 @@ fn layoutWrapped(
876
1061
  crossOffset(child, direction, child_cross, width, line_cross, node.style.align_items, line_baseline)
877
1062
  else
878
1063
  crossOffset(child, direction, child_cross, line_cross, height, node.style.align_items, null);
879
- const leading_margin = mainLeadingMargin(child, direction);
1064
+ const leading_margin = mainFlowStartMargin(child, direction, node.style.reverse);
880
1065
  const child_size = if (direction == .row)
881
1066
  Size{ .width = child_main, .height = child_cross }
882
1067
  else
883
1068
  Size{ .width = child_cross, .height = child_main };
884
- const child_x = if (direction == .row) x + main_offset + leading_margin else x + child_cross_offset;
885
- const child_y = if (direction == .row) y + cross_offset + child_cross_offset else y + main_offset + leading_margin;
1069
+ const main_position = if (node.style.reverse)
1070
+ main_size - main_offset - leading_margin - child_main
1071
+ else
1072
+ main_offset + leading_margin;
1073
+ const cross_position = if (node.style.wrap_reverse)
1074
+ cross_size - cross_offset - child_cross - child_cross_offset
1075
+ else
1076
+ cross_offset + child_cross_offset;
1077
+ const child_x = if (direction == .row) x + main_position else x + cross_position;
1078
+ const child_y = if (direction == .row) y + cross_position else y + main_position;
886
1079
  next_index = try layoutNode(child, child_size, child_x, child_y, next_index, frames);
887
1080
  main_offset += mainMargins(child, direction) + child_main;
888
1081
  if (index + 1 < children.len) main_offset += between_space;
@@ -1168,6 +1361,14 @@ fn mainLeadingMargin(node: Node, direction: Axis) f32 {
1168
1361
  return if (direction == .row) node.style.margin_left else node.style.margin_top;
1169
1362
  }
1170
1363
 
1364
+ fn mainTrailingMargin(node: Node, direction: Axis) f32 {
1365
+ return if (direction == .row) node.style.margin_right else node.style.margin_bottom;
1366
+ }
1367
+
1368
+ fn mainFlowStartMargin(node: Node, direction: Axis, reverse: bool) f32 {
1369
+ return if (reverse) mainTrailingMargin(node, direction) else mainLeadingMargin(node, direction);
1370
+ }
1371
+
1171
1372
  fn crossLeadingMargin(node: Node, direction: Axis) f32 {
1172
1373
  return if (direction == .row) node.style.margin_top else node.style.margin_left;
1173
1374
  }
@@ -1284,6 +1485,45 @@ fn emptyLayoutStyleInput() LayoutStyleInput {
1284
1485
  };
1285
1486
  }
1286
1487
 
1488
+ fn emptyLayoutStyleInputV2() LayoutStyleInputV2 {
1489
+ const unset = std.math.nan(f32);
1490
+ return .{
1491
+ .direction = 0,
1492
+ .width = unset,
1493
+ .height = unset,
1494
+ .intrinsic_width = unset,
1495
+ .intrinsic_height = unset,
1496
+ .baseline = unset,
1497
+ .aspect_ratio = unset,
1498
+ .flex_basis = unset,
1499
+ .flex_grow = 0,
1500
+ .flex_shrink = 0,
1501
+ .min_width = unset,
1502
+ .max_width = unset,
1503
+ .min_height = unset,
1504
+ .max_height = unset,
1505
+ .gap = 0,
1506
+ .justify_content = 0,
1507
+ .align_items = 3,
1508
+ .align_self = 0,
1509
+ .flex_wrap = 0,
1510
+ .align_content = 6,
1511
+ .margin_top = 0,
1512
+ .margin_right = 0,
1513
+ .margin_bottom = 0,
1514
+ .margin_left = 0,
1515
+ .padding_top = 0,
1516
+ .padding_right = 0,
1517
+ .padding_bottom = 0,
1518
+ .padding_left = 0,
1519
+ .grid = 0,
1520
+ .grid_columns = 1,
1521
+ .row_gap = 0,
1522
+ .column_gap = 0,
1523
+ .reverse = 0,
1524
+ };
1525
+ }
1526
+
1287
1527
  fn clampBounds(value: f32, lower: f32, upper: f32) f32 {
1288
1528
  var result = finiteNonNegative(value);
1289
1529
  if (std.math.isFinite(lower) and lower >= 0) result = @max(result, lower);
@@ -1373,6 +1613,59 @@ test "lays out nested flat trees with absolute descendant frames" {
1373
1613
  try std.testing.expectEqual(@as(f32, 15), frames[3].height);
1374
1614
  }
1375
1615
 
1616
+ test "v2 tree layout mirrors reverse main-axis children" {
1617
+ var root_style = emptyLayoutStyleInputV2();
1618
+ root_style.width = 100;
1619
+ root_style.height = 40;
1620
+ root_style.gap = 5;
1621
+ root_style.reverse = 1;
1622
+
1623
+ var first_style = emptyLayoutStyleInputV2();
1624
+ first_style.intrinsic_width = 20;
1625
+ first_style.intrinsic_height = 10;
1626
+ var second_style = emptyLayoutStyleInputV2();
1627
+ second_style.intrinsic_width = 10;
1628
+ second_style.intrinsic_height = 12;
1629
+
1630
+ const nodes = [_]LayoutNodeInputV2{
1631
+ .{ .style = root_style, .first_child = 1, .child_count = 2 },
1632
+ .{ .style = first_style, .first_child = 0, .child_count = 0 },
1633
+ .{ .style = second_style, .first_child = 0, .child_count = 0 },
1634
+ };
1635
+ var frames: [3]Frame = undefined;
1636
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree_v2(100, 40, &nodes, nodes.len, &frames, frames.len));
1637
+ try std.testing.expectEqual(@as(f32, 80), frames[1].x);
1638
+ try std.testing.expectEqual(@as(f32, 65), frames[2].x);
1639
+ try std.testing.expectEqual(@as(f32, 20), frames[1].width);
1640
+ try std.testing.expectEqual(@as(f32, 10), frames[2].width);
1641
+ }
1642
+
1643
+ test "v3 tree layout mirrors wrapped cross-axis lines" {
1644
+ var root_style = emptyLayoutStyleInputV2();
1645
+ root_style.width = 60;
1646
+ root_style.height = 50;
1647
+ root_style.flex_wrap = 1;
1648
+ root_style.align_content = 0;
1649
+ root_style.reverse = 2;
1650
+
1651
+ var first_style = emptyLayoutStyleInputV2();
1652
+ first_style.intrinsic_width = 40;
1653
+ first_style.intrinsic_height = 10;
1654
+ var second_style = emptyLayoutStyleInputV2();
1655
+ second_style.intrinsic_width = 40;
1656
+ second_style.intrinsic_height = 10;
1657
+
1658
+ const nodes = [_]LayoutNodeInputV2{
1659
+ .{ .style = root_style, .first_child = 1, .child_count = 2 },
1660
+ .{ .style = first_style, .first_child = 0, .child_count = 0 },
1661
+ .{ .style = second_style, .first_child = 0, .child_count = 0 },
1662
+ };
1663
+ var frames: [3]Frame = undefined;
1664
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree_v3(60, 50, &nodes, nodes.len, &frames, frames.len));
1665
+ try std.testing.expectEqual(@as(f32, 40), frames[1].y);
1666
+ try std.testing.expectEqual(@as(f32, 30), frames[2].y);
1667
+ }
1668
+
1376
1669
  test "rejects malformed host-neutral layout trees without writing frames" {
1377
1670
  var style = emptyLayoutStyleInput();
1378
1671
  style.width = 20;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.187",
3
+ "version": "0.0.189",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
@@ -44,6 +44,11 @@
44
44
  "import": "./dist/mobile.js",
45
45
  "default": "./dist/mobile.js"
46
46
  },
47
+ "./browser": {
48
+ "types": "./dist/browser.d.ts",
49
+ "import": "./dist/browser.js",
50
+ "default": "./dist/browser.js"
51
+ },
47
52
  "./ios": {
48
53
  "browser": null,
49
54
  "types": "./dist/ios/src/index.d.ts",
@@ -65,6 +70,9 @@
65
70
  "mobile": [
66
71
  "./dist/mobile.d.ts"
67
72
  ],
73
+ "browser": [
74
+ "./dist/browser.d.ts"
75
+ ],
68
76
  "ios": [
69
77
  "./dist/ios/src/index.d.ts"
70
78
  ],