craft-native 0.0.212 → 0.0.213

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.
@@ -353,6 +353,12 @@ object CraftNative {
353
353
  nodeData: FloatArray,
354
354
  treeMeta: FloatArray,
355
355
  ): FloatArray?
356
+ private external fun nativeLayoutTreeV4(
357
+ width: Float,
358
+ height: Float,
359
+ nodeData: FloatArray,
360
+ treeMeta: FloatArray,
361
+ ): FloatArray?
356
362
  private external fun nativeClipboardRead(activity: Activity): String?
357
363
  private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
358
364
  private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
@@ -635,6 +641,17 @@ object CraftNative {
635
641
  }
636
642
  }
637
643
 
644
+ /** Resolve a reverse-aware tree with shared grid-auto-rows support. */
645
+ fun layoutTreeV4(width: Float, height: Float, nodeData: FloatArray, treeMeta: FloatArray): FloatArray? {
646
+ if (!isAvailable || nodeData.isEmpty() || treeMeta.isEmpty()) return null
647
+ return try {
648
+ nativeLayoutTreeV4(width, height, nodeData, treeMeta)
649
+ ?.takeIf { it.size >= treeMeta.size / 2 * 4 && it.all { value -> value.isFinite() } }
650
+ } catch (e: UnsatisfiedLinkError) {
651
+ null
652
+ }
653
+ }
654
+
638
655
  /**
639
656
  * The clipboard's text, or null to use the Kotlin implementation.
640
657
  *
package/dist/cli.js CHANGED
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3424
3424
  `);
3425
3425
  }
3426
3426
  // package.json
3427
- var version = "0.0.212";
3427
+ var version = "0.0.213";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
309
309
  `);
310
310
  }
311
311
  // package.json
312
- var version = "0.0.212";
312
+ var version = "0.0.213";
313
313
 
314
314
  // src/sdk-version.ts
315
315
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
57
57
  `);
58
58
  }
59
59
  // package.json
60
- var version = "0.0.212";
60
+ var version = "0.0.213";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -87,6 +87,13 @@ final class DriverTests: XCTestCase {
87
87
  XCTAssertTrue(ok, what)
88
88
  }
89
89
 
90
+ /// testIDs are exact identifiers. Resolving a unique element with the
91
+ /// subscript scans the whole accessibility tree for ambiguous matches;
92
+ /// firstMatch can stop at the identified node, including a scrolled row.
93
+ func identified(_ id: String) -> XCUIElement {
94
+ app.descendants(matching: .any).matching(NSPredicate(format: "identifier == %@", id)).firstMatch
95
+ }
96
+
90
97
  func element(containing text: String, in target: XCUIApplication? = nil) -> XCUIElement? {
91
98
  let root = target ?? app
92
99
  let predicate = NSPredicate(format: "label CONTAINS[c] %@ OR value CONTAINS[c] %@ OR identifier == %@", text, text, text)
@@ -197,7 +204,7 @@ final class DriverTests: XCTestCase {
197
204
  switch command {
198
205
  case "ifid":
199
206
  let rest = arg.split(separator: " ", maxSplits: 1).map(String.init)
200
- if rest.count == 2, app.descendants(matching: .any)[rest[0]].waitForExistence(timeout: 3) { note("ifid \(rest[0]): shown"); run(rest[1]) }
207
+ if rest.count == 2, identified(rest[0]).waitForExistence(timeout: 3) { note("ifid \(rest[0]): shown"); run(rest[1]) }
201
208
  else { note("ifid \(rest.first ?? ""): not shown") }
202
209
  case "launch": app.launch()
203
210
  case "activate": app.activate()
@@ -261,7 +268,7 @@ final class DriverTests: XCTestCase {
261
268
  if let target = poll(10, { exact(arg) }) { press(target); note("tapexact \(arg): ok") }
262
269
  else { XCTFail("Element not found"); note("FAIL tapexact \(arg): NOT FOUND") }
263
270
  case "tapid":
264
- let target = app.descendants(matching: .any)[arg]
271
+ let target = identified(arg)
265
272
  if target.waitForExistence(timeout: 10) { press(target); note("tapid \(arg): ok") }
266
273
  else { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") }
267
274
  case "tapxy":
@@ -281,7 +288,7 @@ final class DriverTests: XCTestCase {
281
288
  case "return": app.typeText(XCUIKeyboardKey.return.rawValue); note("return")
282
289
  case "typeid":
283
290
  let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
284
- let target = app.descendants(matching: .any)[idParts[0]]
291
+ let target = identified(idParts[0])
285
292
  if target.waitForExistence(timeout: 10) {
286
293
  target.tap()
287
294
  Thread.sleep(forTimeInterval: 0.8)
@@ -306,15 +313,15 @@ final class DriverTests: XCTestCase {
306
313
  check(poll(seconds, { exact(text) }) != nil, "expectexact \(text)")
307
314
  case "expectid":
308
315
  let (id, seconds) = split(arg)
309
- check(app.descendants(matching: .any)[id].waitForExistence(timeout: seconds), "expectid \(id)")
316
+ check(identified(id).waitForExistence(timeout: seconds), "expectid \(id)")
310
317
  case "labelid":
311
318
  let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
312
- let target = app.descendants(matching: .any)[idParts[0]]
319
+ let target = identified(idParts[0])
313
320
  let want = idParts.count > 1 ? idParts[1] : ""
314
321
  let seen = target.waitForExistence(timeout: 5) ? target.label : "(missing)"
315
322
  check(seen == want, "labelid \(idParts[0]) \(want) (saw \(seen))")
316
323
  case "absentid":
317
- check(!app.descendants(matching: .any)[arg].exists, "absentid \(arg)")
324
+ check(!identified(arg).exists, "absentid \(arg)")
318
325
  case "absent":
319
326
  check(element(containing: arg) == nil, "absent \(arg)")
320
327
  case "tabbar":
@@ -98,6 +98,10 @@ pub const Style = struct {
98
98
  grid_columns: u32 = 1,
99
99
  row_gap: f32 = 0,
100
100
  column_gap: f32 = 0,
101
+ /// Minimum height for generated grid rows. This is optional in the tree
102
+ /// ABI and remains host-neutral because hosts resolve percentages before
103
+ /// serializing the style.
104
+ grid_auto_rows: ?f32 = null,
101
105
  };
102
106
 
103
107
  pub const Node = struct {
@@ -243,6 +247,53 @@ pub const LayoutNodeInputV2 = extern struct {
243
247
  child_count: u32,
244
248
  };
245
249
 
250
+ /// Versioned tree input that carries the resolved `grid-auto-rows` minimum.
251
+ /// Appending the field keeps the v1/v2/v3 layouts ABI-compatible for older
252
+ /// hosts while allowing a native tree to use the same auto-row rule on every
253
+ /// platform.
254
+ pub const LayoutStyleInputV4 = extern struct {
255
+ direction: u32,
256
+ width: f32,
257
+ height: f32,
258
+ intrinsic_width: f32,
259
+ intrinsic_height: f32,
260
+ baseline: f32,
261
+ aspect_ratio: f32,
262
+ flex_basis: f32,
263
+ flex_grow: f32,
264
+ flex_shrink: f32,
265
+ min_width: f32,
266
+ max_width: f32,
267
+ min_height: f32,
268
+ max_height: f32,
269
+ gap: f32,
270
+ justify_content: u32,
271
+ align_items: u32,
272
+ align_self: u32,
273
+ flex_wrap: u32,
274
+ align_content: u32,
275
+ margin_top: f32,
276
+ margin_right: f32,
277
+ margin_bottom: f32,
278
+ margin_left: f32,
279
+ padding_top: f32,
280
+ padding_right: f32,
281
+ padding_bottom: f32,
282
+ padding_left: f32,
283
+ grid: u32,
284
+ grid_columns: u32,
285
+ row_gap: f32,
286
+ column_gap: f32,
287
+ reverse: u32,
288
+ grid_auto_rows: f32,
289
+ };
290
+
291
+ pub const LayoutNodeInputV4 = extern struct {
292
+ style: LayoutStyleInputV4,
293
+ first_child: u32,
294
+ child_count: u32,
295
+ };
296
+
246
297
  const max_layout_nodes: usize = 65_536;
247
298
 
248
299
  /// Resolve an entire native view tree using the same layout rules used by the
@@ -361,6 +412,43 @@ pub export fn craft_native_layout_tree_v3(
361
412
  return if (result == node_count) 0 else -1;
362
413
  }
363
414
 
415
+ /// v4 tree layout entry point. It preserves v3's reverse/wrap-reverse bits
416
+ /// and adds one resolved `grid-auto-rows` minimum per node.
417
+ pub export fn craft_native_layout_tree_v4(
418
+ viewport_width: f32,
419
+ viewport_height: f32,
420
+ nodes: [*]const LayoutNodeInputV4,
421
+ node_count: usize,
422
+ frames: [*]Frame,
423
+ frame_count: usize,
424
+ ) callconv(.c) c_int {
425
+ if (node_count == 0 or node_count > max_layout_nodes or frame_count < node_count) return -1;
426
+
427
+ const input = nodes[0..node_count];
428
+ const visited = std.heap.page_allocator.alloc(bool, node_count) catch return -1;
429
+ defer std.heap.page_allocator.free(visited);
430
+ @memset(visited, false);
431
+ if (!validateLayoutTreeV4(input, 0, visited)) return -1;
432
+
433
+ var tree = std.heap.page_allocator.alloc(Node, node_count) catch return -1;
434
+ defer std.heap.page_allocator.free(tree);
435
+ for (input, 0..) |item, index| {
436
+ const first = @as(usize, @intCast(item.first_child));
437
+ const count = @as(usize, @intCast(item.child_count));
438
+ tree[index] = .{
439
+ .style = layoutStyleV4(item.style),
440
+ .children = tree[first .. first + count],
441
+ };
442
+ }
443
+
444
+ const result = layout(
445
+ tree[0],
446
+ .{ .width = finiteNonNegative(viewport_width), .height = finiteNonNegative(viewport_height) },
447
+ frames[0..frame_count],
448
+ ) catch return -1;
449
+ return if (result == node_count) 0 else -1;
450
+ }
451
+
364
452
  fn validateLayoutTree(input: []const LayoutNodeInput, index: usize, visited: []bool) bool {
365
453
  if (index >= input.len or visited[index]) return false;
366
454
  visited[index] = true;
@@ -387,6 +475,19 @@ fn validateLayoutTreeV2(input: []const LayoutNodeInputV2, index: usize, visited:
387
475
  return true;
388
476
  }
389
477
 
478
+ fn validateLayoutTreeV4(input: []const LayoutNodeInputV4, index: usize, visited: []bool) bool {
479
+ if (index >= input.len or visited[index]) return false;
480
+ visited[index] = true;
481
+ const item = input[index];
482
+ const first = @as(usize, @intCast(item.first_child));
483
+ const count = @as(usize, @intCast(item.child_count));
484
+ if (first > input.len or count > input.len - first or (count > 0 and first <= index)) return false;
485
+ for (first..first + count) |child| {
486
+ if (!validateLayoutTreeV4(input, child, visited)) return false;
487
+ }
488
+ return true;
489
+ }
490
+
390
491
  fn layoutStyle(input: LayoutStyleInput) Style {
391
492
  return .{
392
493
  .direction = if (input.direction == 1) .column else .row,
@@ -470,6 +571,46 @@ fn layoutStyleV3(input: LayoutStyleInputV2) Style {
470
571
  return style;
471
572
  }
472
573
 
574
+ fn layoutStyleV4(input: LayoutStyleInputV4) Style {
575
+ return .{
576
+ .direction = if (input.direction == 1) .column else .row,
577
+ .reverse = (input.reverse & 1) != 0,
578
+ .wrap_reverse = (input.reverse & 2) != 0,
579
+ .width = optionalFinite(input.width),
580
+ .height = optionalFinite(input.height),
581
+ .intrinsic_width = optionalFinite(input.intrinsic_width),
582
+ .intrinsic_height = optionalFinite(input.intrinsic_height),
583
+ .baseline = optionalFinite(input.baseline),
584
+ .aspect_ratio = optionalFinite(input.aspect_ratio),
585
+ .flex_basis = optionalFinite(input.flex_basis),
586
+ .flex_grow = finiteNonNegative(input.flex_grow),
587
+ .flex_shrink = finiteNonNegative(input.flex_shrink),
588
+ .min_width = optionalFinite(input.min_width),
589
+ .max_width = optionalFinite(input.max_width),
590
+ .min_height = optionalFinite(input.min_height),
591
+ .max_height = optionalFinite(input.max_height),
592
+ .gap = finiteNonNegative(input.gap),
593
+ .justify_content = @fromBackingInt(@intCast(@min(input.justify_content, 5))),
594
+ .align_items = @fromBackingInt(@intCast(@min(input.align_items, 4))),
595
+ .align_self = if (input.align_self == 0) null else @fromBackingInt(@intCast(@min(input.align_self - 1, 4))),
596
+ .flex_wrap = input.flex_wrap != 0,
597
+ .align_content = @fromBackingInt(@intCast(@min(input.align_content, 6))),
598
+ .margin_top = finiteNonNegative(input.margin_top),
599
+ .margin_right = finiteNonNegative(input.margin_right),
600
+ .margin_bottom = finiteNonNegative(input.margin_bottom),
601
+ .margin_left = finiteNonNegative(input.margin_left),
602
+ .padding_top = finiteNonNegative(input.padding_top),
603
+ .padding_right = finiteNonNegative(input.padding_right),
604
+ .padding_bottom = finiteNonNegative(input.padding_bottom),
605
+ .padding_left = finiteNonNegative(input.padding_left),
606
+ .grid = input.grid != 0,
607
+ .grid_columns = @max(@as(u32, 1), input.grid_columns),
608
+ .row_gap = finiteNonNegative(input.row_gap),
609
+ .column_gap = finiteNonNegative(input.column_gap),
610
+ .grid_auto_rows = optionalFinite(input.grid_auto_rows),
611
+ };
612
+ }
613
+
473
614
  fn optionalFinite(value: f32) ?f32 {
474
615
  return if (std.math.isFinite(value)) value else null;
475
616
  }
@@ -1267,7 +1408,7 @@ fn gridRowHeight(node: Node, row: usize, column_count: usize) f32 {
1267
1408
  const child_height = child.style.height orelse ratio_height orelse child.style.intrinsic_height orelse child.style.flex_basis orelse 0;
1268
1409
  height = @max(height, clamp(child_height, child.style.min_height, child.style.max_height) + verticalMargins(child));
1269
1410
  }
1270
- return height;
1411
+ return @max(height, finiteNonNegative(node.style.grid_auto_rows orelse 0));
1271
1412
  }
1272
1413
 
1273
1414
  fn childMainBasis(node: Node, parent_direction: Axis) f32 {
@@ -1524,6 +1665,46 @@ fn emptyLayoutStyleInputV2() LayoutStyleInputV2 {
1524
1665
  };
1525
1666
  }
1526
1667
 
1668
+ fn emptyLayoutStyleInputV4() LayoutStyleInputV4 {
1669
+ const unset = std.math.nan(f32);
1670
+ return .{
1671
+ .direction = 0,
1672
+ .width = unset,
1673
+ .height = unset,
1674
+ .intrinsic_width = unset,
1675
+ .intrinsic_height = unset,
1676
+ .baseline = unset,
1677
+ .aspect_ratio = unset,
1678
+ .flex_basis = unset,
1679
+ .flex_grow = 0,
1680
+ .flex_shrink = 0,
1681
+ .min_width = unset,
1682
+ .max_width = unset,
1683
+ .min_height = unset,
1684
+ .max_height = unset,
1685
+ .gap = 0,
1686
+ .justify_content = 0,
1687
+ .align_items = 3,
1688
+ .align_self = 0,
1689
+ .flex_wrap = 0,
1690
+ .align_content = 6,
1691
+ .margin_top = 0,
1692
+ .margin_right = 0,
1693
+ .margin_bottom = 0,
1694
+ .margin_left = 0,
1695
+ .padding_top = 0,
1696
+ .padding_right = 0,
1697
+ .padding_bottom = 0,
1698
+ .padding_left = 0,
1699
+ .grid = 0,
1700
+ .grid_columns = 1,
1701
+ .row_gap = 0,
1702
+ .column_gap = 0,
1703
+ .reverse = 0,
1704
+ .grid_auto_rows = unset,
1705
+ };
1706
+ }
1707
+
1527
1708
  fn clampBounds(value: f32, lower: f32, upper: f32) f32 {
1528
1709
  var result = finiteNonNegative(value);
1529
1710
  if (std.math.isFinite(lower) and lower >= 0) result = @max(result, lower);
@@ -1566,6 +1747,37 @@ test "exports the host-neutral tree layout contract" {
1566
1747
  try std.testing.expectEqual(@as(f32, 10), frames[1].height);
1567
1748
  }
1568
1749
 
1750
+ test "v4 tree layout applies a resolved grid auto-row minimum" {
1751
+ var root_style = emptyLayoutStyleInputV4();
1752
+ root_style.width = 100;
1753
+ root_style.height = 100;
1754
+ root_style.grid = 1;
1755
+ root_style.grid_columns = 2;
1756
+ root_style.grid_auto_rows = 20;
1757
+
1758
+ var first_style = emptyLayoutStyleInputV4();
1759
+ first_style.intrinsic_width = 10;
1760
+ first_style.intrinsic_height = 5;
1761
+ var second_style = emptyLayoutStyleInputV4();
1762
+ second_style.intrinsic_width = 10;
1763
+ second_style.intrinsic_height = 7;
1764
+ var third_style = emptyLayoutStyleInputV4();
1765
+ third_style.intrinsic_width = 10;
1766
+ third_style.intrinsic_height = 4;
1767
+
1768
+ const nodes = [_]LayoutNodeInputV4{
1769
+ .{ .style = root_style, .first_child = 1, .child_count = 3 },
1770
+ .{ .style = first_style, .first_child = 0, .child_count = 0 },
1771
+ .{ .style = second_style, .first_child = 0, .child_count = 0 },
1772
+ .{ .style = third_style, .first_child = 0, .child_count = 0 },
1773
+ };
1774
+ var frames: [4]Frame = undefined;
1775
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree_v4(100, 100, &nodes, nodes.len, &frames, frames.len));
1776
+ try std.testing.expectEqual(@as(f32, 0), frames[1].y);
1777
+ try std.testing.expectEqual(@as(f32, 0), frames[2].y);
1778
+ try std.testing.expectEqual(@as(f32, 20), frames[3].y);
1779
+ }
1780
+
1569
1781
  test "lays out nested flat trees with absolute descendant frames" {
1570
1782
  var root_style = emptyLayoutStyleInput();
1571
1783
  root_style.width = 200;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.212",
3
+ "version": "0.0.213",
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>",