craft-native 0.0.246 → 0.0.248
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/android/src/index.d.ts +11 -0
- package/dist/android/src/index.js +33 -21
- package/dist/android/templates/AndroidManifest.xml.template +1 -0
- package/dist/android/templates/CraftBridge.kt.template +136 -1
- package/dist/android/templates/CraftHeartRate.kt.template +938 -0
- package/dist/android/templates/CraftHeartRateCore.kt.template +485 -0
- package/dist/android/templates/CraftNative.kt.template +17 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +115 -40
- package/dist/android/templates/CraftNativeMapGoogle.kt.template +92 -5
- package/dist/android/templates/CraftNativeMapLibre.kt.template +90 -3
- package/dist/android/templates/CraftNativeMutation.kt.template +32 -0
- package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -2
- package/dist/android/templates/CraftNativeServices.kt.template +59 -1
- package/dist/android/templates/CraftNativeShell.kt.template +54 -14
- package/dist/android/templates/MainActivityNative.kt.template +369 -69
- package/dist/android/templates/build.gradle.kts.project.template +1 -1
- package/dist/api/mobile.d.ts +39 -3
- package/dist/browser.js +9 -9
- package/dist/cli.js +90 -673
- package/dist/components/index.d.ts +50 -9
- package/dist/index.cjs +394 -438
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +446 -493
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +54 -53
- package/dist/ios/templates/CraftApp.swift +37 -2
- package/dist/ios/templates/CraftHealth.swift +217 -0
- package/dist/ios/templates/CraftHeartRate.swift +6 -3
- package/dist/ios/templates/CraftHybrid.swift +18 -19
- package/dist/ios/templates/CraftLiveActivities.swift +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +87 -8
- package/dist/ios/templates/CraftNativeFiles.swift +15 -3
- package/dist/ios/templates/CraftNativeFlatList.swift +18 -1
- package/dist/ios/templates/CraftNativeMutation.swift +36 -0
- package/dist/ios/templates/CraftNativeScreen.swift +813 -65
- package/dist/ios/templates/CraftNativeServices.swift +4 -2
- package/dist/ios/templates/CraftNativeUI.swift +10 -2
- package/dist/ios/templates/testing/DriverTests.swift.template +1 -0
- package/dist/ios/templates/testing/HostApp.swift +9 -2
- package/dist/macos/templates/CraftNativeMutation.swift +36 -0
- package/dist/macos/templates/native_layout.zig +226 -2
- package/dist/mobile-testing.js +21 -3
- package/dist/mobile.js +101 -63
- package/dist/testing/media-cache/media-cache.kt +4 -2
- package/package.json +1 -1
|
@@ -313,8 +313,10 @@ final class CraftNativeSounds {
|
|
|
313
313
|
func play(id: String, url: URL, options: [String: Any], answer: @escaping (Result<Any, CraftNativeActionError>) -> Void) {
|
|
314
314
|
stop(id: id, fadeOut: 0)
|
|
315
315
|
let loop = options["loop"] as? Bool ?? false
|
|
316
|
-
let
|
|
317
|
-
let
|
|
316
|
+
let volumeValue = craftNativeNumber(options["volume"]).map { $0.doubleValue }.flatMap { $0.isFinite ? $0 : nil } ?? 1
|
|
317
|
+
let fadeInValue = craftNativeNumber(options["fadeIn"]).map { $0.doubleValue }.flatMap { $0.isFinite ? $0 : nil } ?? 0
|
|
318
|
+
let volume = Float(min(max(volumeValue, 0), 1))
|
|
319
|
+
let fadeIn = max(0, fadeInValue) / 1000
|
|
318
320
|
Self.claimSession(respectSilentSwitch: options["respectSilentSwitch"] as? Bool ?? true)
|
|
319
321
|
let item = AVPlayerItem(url: url)
|
|
320
322
|
let player = AVQueuePlayer()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import AuthenticationServices
|
|
2
|
+
import CoreFoundation
|
|
2
3
|
import SafariServices
|
|
3
4
|
import UIKit
|
|
4
5
|
import WebKit
|
|
@@ -54,7 +55,12 @@ enum CraftPresenter {
|
|
|
54
55
|
/// view's points, as a rect. Nil when it is not one.
|
|
55
56
|
static func rect(_ value: Any?) -> CGRect? {
|
|
56
57
|
guard let value = value as? [String: Any] else { return nil }
|
|
57
|
-
func number(_ key: String) -> CGFloat? {
|
|
58
|
+
func number(_ key: String) -> CGFloat? {
|
|
59
|
+
guard let number = value[key] as? NSNumber,
|
|
60
|
+
CFGetTypeID(number) != CFBooleanGetTypeID() else { return nil }
|
|
61
|
+
let value = CGFloat(truncating: number)
|
|
62
|
+
return value.isFinite ? value : nil
|
|
63
|
+
}
|
|
58
64
|
guard let x = number("x"), let y = number("y") else { return nil }
|
|
59
65
|
return CGRect(x: x, y: y, width: max(0, number("width") ?? 0), height: max(0, number("height") ?? 0))
|
|
60
66
|
}
|
|
@@ -464,7 +470,9 @@ extension CraftSignInWithApple: ASAuthorizationControllerDelegate, ASAuthorizati
|
|
|
464
470
|
/// show the same glyph the system's own controls use.
|
|
465
471
|
enum CraftSymbols {
|
|
466
472
|
static func image(named name: String, options: [String: Any]) -> String? {
|
|
467
|
-
let pointSize = (options["pointSize"]
|
|
473
|
+
let pointSize = craftNativeNumber(options["pointSize"])
|
|
474
|
+
.map { CGFloat(truncating: $0) }
|
|
475
|
+
.flatMap { $0.isFinite ? $0 : nil } ?? 17
|
|
468
476
|
let weight: UIImage.SymbolWeight
|
|
469
477
|
switch options["weight"] as? String {
|
|
470
478
|
case "ultraLight": weight = .ultraLight
|
|
@@ -276,6 +276,7 @@ final class DriverTests: XCTestCase {
|
|
|
276
276
|
let host = XCUIApplication(bundleIdentifier: "{{BUNDLE_PREFIX}}.host")
|
|
277
277
|
host.launchArguments = ["--craft-universal-link", url.absoluteString]
|
|
278
278
|
host.launch()
|
|
279
|
+
host.buttons["craft-driver-open-universal-link"].tap()
|
|
279
280
|
let opened = app.wait(for: .runningForeground, timeout: 75)
|
|
280
281
|
if !opened {
|
|
281
282
|
post("/save?name=universal-link-host.txt", Data(host.debugDescription.utf8))
|
|
@@ -7,8 +7,15 @@ struct HostApp: App {
|
|
|
7
7
|
|
|
8
8
|
var body: some Scene {
|
|
9
9
|
WindowGroup {
|
|
10
|
-
|
|
11
|
-
.
|
|
10
|
+
VStack {
|
|
11
|
+
Text(status).accessibilityIdentifier("craft-driver-status")
|
|
12
|
+
// XCTest must finish acquiring the host's launch assertion
|
|
13
|
+
// before a handoff can move another application foreground.
|
|
14
|
+
Button("Open Universal Link") {
|
|
15
|
+
Task { await openUniversalLinkIfRequested() }
|
|
16
|
+
}
|
|
17
|
+
.accessibilityIdentifier("craft-driver-open-universal-link")
|
|
18
|
+
}
|
|
12
19
|
}
|
|
13
20
|
}
|
|
14
21
|
|
|
@@ -108,6 +108,13 @@ final class CraftNativeMutationDocument {
|
|
|
108
108
|
case "moveChild":
|
|
109
109
|
try moveChild(operation, nodes: &candidateNodes)
|
|
110
110
|
if let id = operation["parentId"] as? String { appendUnique(id, to: &affectedNodeIds) }
|
|
111
|
+
case "reparentChild":
|
|
112
|
+
let previousParent = try reparentChild(operation, nodes: &candidateNodes)
|
|
113
|
+
if let id = operation["parentId"] as? String { appendUnique(id, to: &affectedNodeIds) }
|
|
114
|
+
appendUnique(previousParent, to: &affectedNodeIds)
|
|
115
|
+
// A child crossing parents needs the committed tree to be
|
|
116
|
+
// reconciled so the host can move its existing native view.
|
|
117
|
+
requiresFullRender = true
|
|
111
118
|
case "removeNode":
|
|
112
119
|
if let id = operation["id"] as? String, let parent = candidateNodes[id]?.parent {
|
|
113
120
|
appendUnique(parent, to: &affectedNodeIds)
|
|
@@ -251,6 +258,35 @@ final class CraftNativeMutationDocument {
|
|
|
251
258
|
nodes[parentId] = parent
|
|
252
259
|
}
|
|
253
260
|
|
|
261
|
+
private func reparentChild(_ operation: [String: Any], nodes: inout [String: Node]) throws -> String {
|
|
262
|
+
let parentId = try requiredId(operation, field: "parentId", name: "reparentChild")
|
|
263
|
+
let childId = try requiredId(operation, field: "childId", name: "reparentChild")
|
|
264
|
+
guard var parent = nodes[parentId] else {
|
|
265
|
+
throw CraftNativeMutationFailure("UNKNOWN_PARENT", "node \(parentId) does not exist")
|
|
266
|
+
}
|
|
267
|
+
guard var child = nodes[childId], let previousParentId = child.parent,
|
|
268
|
+
var previousParent = nodes[previousParentId] else {
|
|
269
|
+
throw CraftNativeMutationFailure("NOT_ATTACHED", "node \(childId) is not attached to a parent")
|
|
270
|
+
}
|
|
271
|
+
guard acceptsChildren(parent.type) else {
|
|
272
|
+
throw CraftNativeMutationFailure("INVALID_PARENT", "node \(parentId) cannot contain child nodes")
|
|
273
|
+
}
|
|
274
|
+
guard parentId != childId, !descendants(of: childId, nodes: nodes).contains(parentId) else {
|
|
275
|
+
throw CraftNativeMutationFailure("CYCLE", "reparenting node \(childId) would create a cycle")
|
|
276
|
+
}
|
|
277
|
+
guard parentId != previousParentId else {
|
|
278
|
+
throw CraftNativeMutationFailure("SAME_PARENT", "node \(childId) is already a child of \(parentId)")
|
|
279
|
+
}
|
|
280
|
+
let index = try insertionIndex(operation, count: parent.children.count)
|
|
281
|
+
previousParent.children.removeAll { $0 == childId }
|
|
282
|
+
parent.children.insert(childId, at: index)
|
|
283
|
+
child.parent = parentId
|
|
284
|
+
nodes[previousParentId] = previousParent
|
|
285
|
+
nodes[parentId] = parent
|
|
286
|
+
nodes[childId] = child
|
|
287
|
+
return previousParentId
|
|
288
|
+
}
|
|
289
|
+
|
|
254
290
|
private func removeNode(
|
|
255
291
|
_ operation: [String: Any],
|
|
256
292
|
nodes: inout [String: Node],
|
|
@@ -105,6 +105,8 @@ pub const Style = struct {
|
|
|
105
105
|
/// ABI and remains host-neutral because hosts resolve percentages before
|
|
106
106
|
/// serializing the style.
|
|
107
107
|
grid_auto_rows: ?f32 = null,
|
|
108
|
+
/// Explicit equal row-track count. Null means derive rows from children.
|
|
109
|
+
grid_rows: ?u32 = null,
|
|
108
110
|
};
|
|
109
111
|
|
|
110
112
|
pub const Node = struct {
|
|
@@ -297,6 +299,51 @@ pub const LayoutNodeInputV4 = extern struct {
|
|
|
297
299
|
child_count: u32,
|
|
298
300
|
};
|
|
299
301
|
|
|
302
|
+
/// v5 appends the resolved explicit grid-row count to the v4 payload.
|
|
303
|
+
pub const LayoutStyleInputV5 = extern struct {
|
|
304
|
+
direction: u32,
|
|
305
|
+
width: f32,
|
|
306
|
+
height: f32,
|
|
307
|
+
intrinsic_width: f32,
|
|
308
|
+
intrinsic_height: f32,
|
|
309
|
+
baseline: f32,
|
|
310
|
+
aspect_ratio: f32,
|
|
311
|
+
flex_basis: f32,
|
|
312
|
+
flex_grow: f32,
|
|
313
|
+
flex_shrink: f32,
|
|
314
|
+
min_width: f32,
|
|
315
|
+
max_width: f32,
|
|
316
|
+
min_height: f32,
|
|
317
|
+
max_height: f32,
|
|
318
|
+
gap: f32,
|
|
319
|
+
justify_content: u32,
|
|
320
|
+
align_items: u32,
|
|
321
|
+
align_self: u32,
|
|
322
|
+
flex_wrap: u32,
|
|
323
|
+
align_content: u32,
|
|
324
|
+
margin_top: f32,
|
|
325
|
+
margin_right: f32,
|
|
326
|
+
margin_bottom: f32,
|
|
327
|
+
margin_left: f32,
|
|
328
|
+
padding_top: f32,
|
|
329
|
+
padding_right: f32,
|
|
330
|
+
padding_bottom: f32,
|
|
331
|
+
padding_left: f32,
|
|
332
|
+
grid: u32,
|
|
333
|
+
grid_columns: u32,
|
|
334
|
+
row_gap: f32,
|
|
335
|
+
column_gap: f32,
|
|
336
|
+
reverse: u32,
|
|
337
|
+
grid_auto_rows: f32,
|
|
338
|
+
grid_rows: u32,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
pub const LayoutNodeInputV5 = extern struct {
|
|
342
|
+
style: LayoutStyleInputV5,
|
|
343
|
+
first_child: u32,
|
|
344
|
+
child_count: u32,
|
|
345
|
+
};
|
|
346
|
+
|
|
300
347
|
const max_layout_nodes: usize = 65_536;
|
|
301
348
|
|
|
302
349
|
/// Resolve an entire native view tree using the same layout rules used by the
|
|
@@ -452,6 +499,43 @@ pub export fn craft_native_layout_tree_v4(
|
|
|
452
499
|
return if (result == node_count) 0 else -1;
|
|
453
500
|
}
|
|
454
501
|
|
|
502
|
+
/// v5 tree layout entry point. It preserves v4's auto-row and reverse fields,
|
|
503
|
+
/// adding an explicit equal-track row count for gridTemplateRows.
|
|
504
|
+
pub export fn craft_native_layout_tree_v5(
|
|
505
|
+
viewport_width: f32,
|
|
506
|
+
viewport_height: f32,
|
|
507
|
+
nodes: [*]const LayoutNodeInputV5,
|
|
508
|
+
node_count: usize,
|
|
509
|
+
frames: [*]Frame,
|
|
510
|
+
frame_count: usize,
|
|
511
|
+
) callconv(.c) c_int {
|
|
512
|
+
if (node_count == 0 or node_count > max_layout_nodes or frame_count < node_count) return -1;
|
|
513
|
+
|
|
514
|
+
const input = nodes[0..node_count];
|
|
515
|
+
const visited = std.heap.page_allocator.alloc(bool, node_count) catch return -1;
|
|
516
|
+
defer std.heap.page_allocator.free(visited);
|
|
517
|
+
@memset(visited, false);
|
|
518
|
+
if (!validateLayoutTreeV5(input, 0, visited)) return -1;
|
|
519
|
+
|
|
520
|
+
var tree = std.heap.page_allocator.alloc(Node, node_count) catch return -1;
|
|
521
|
+
defer std.heap.page_allocator.free(tree);
|
|
522
|
+
for (input, 0..) |item, index| {
|
|
523
|
+
const first = @as(usize, @intCast(item.first_child));
|
|
524
|
+
const count = @as(usize, @intCast(item.child_count));
|
|
525
|
+
tree[index] = .{
|
|
526
|
+
.style = layoutStyleV5(item.style),
|
|
527
|
+
.children = tree[first .. first + count],
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const result = layout(
|
|
532
|
+
tree[0],
|
|
533
|
+
.{ .width = finiteNonNegative(viewport_width), .height = finiteNonNegative(viewport_height) },
|
|
534
|
+
frames[0..frame_count],
|
|
535
|
+
) catch return -1;
|
|
536
|
+
return if (result == node_count) 0 else -1;
|
|
537
|
+
}
|
|
538
|
+
|
|
455
539
|
fn validateLayoutTree(input: []const LayoutNodeInput, index: usize, visited: []bool) bool {
|
|
456
540
|
if (index >= input.len or visited[index]) return false;
|
|
457
541
|
visited[index] = true;
|
|
@@ -491,6 +575,19 @@ fn validateLayoutTreeV4(input: []const LayoutNodeInputV4, index: usize, visited:
|
|
|
491
575
|
return true;
|
|
492
576
|
}
|
|
493
577
|
|
|
578
|
+
fn validateLayoutTreeV5(input: []const LayoutNodeInputV5, index: usize, visited: []bool) bool {
|
|
579
|
+
if (index >= input.len or visited[index]) return false;
|
|
580
|
+
visited[index] = true;
|
|
581
|
+
const item = input[index];
|
|
582
|
+
const first = @as(usize, @intCast(item.first_child));
|
|
583
|
+
const count = @as(usize, @intCast(item.child_count));
|
|
584
|
+
if (first > input.len or count > input.len - first or (count > 0 and first <= index)) return false;
|
|
585
|
+
for (first..first + count) |child| {
|
|
586
|
+
if (!validateLayoutTreeV5(input, child, visited)) return false;
|
|
587
|
+
}
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
494
591
|
fn layoutStyle(input: LayoutStyleInput) Style {
|
|
495
592
|
return .{
|
|
496
593
|
.direction = if (input.direction == 1) .column else .row,
|
|
@@ -614,6 +711,47 @@ fn layoutStyleV4(input: LayoutStyleInputV4) Style {
|
|
|
614
711
|
};
|
|
615
712
|
}
|
|
616
713
|
|
|
714
|
+
fn layoutStyleV5(input: LayoutStyleInputV5) Style {
|
|
715
|
+
return .{
|
|
716
|
+
.direction = if (input.direction == 1) .column else .row,
|
|
717
|
+
.reverse = (input.reverse & 1) != 0,
|
|
718
|
+
.wrap_reverse = (input.reverse & 2) != 0,
|
|
719
|
+
.width = optionalFinite(input.width),
|
|
720
|
+
.height = optionalFinite(input.height),
|
|
721
|
+
.intrinsic_width = optionalFinite(input.intrinsic_width),
|
|
722
|
+
.intrinsic_height = optionalFinite(input.intrinsic_height),
|
|
723
|
+
.baseline = optionalFinite(input.baseline),
|
|
724
|
+
.aspect_ratio = optionalFinite(input.aspect_ratio),
|
|
725
|
+
.flex_basis = optionalFinite(input.flex_basis),
|
|
726
|
+
.flex_grow = finiteNonNegative(input.flex_grow),
|
|
727
|
+
.flex_shrink = finiteNonNegative(input.flex_shrink),
|
|
728
|
+
.min_width = optionalFinite(input.min_width),
|
|
729
|
+
.max_width = optionalFinite(input.max_width),
|
|
730
|
+
.min_height = optionalFinite(input.min_height),
|
|
731
|
+
.max_height = optionalFinite(input.max_height),
|
|
732
|
+
.gap = finiteNonNegative(input.gap),
|
|
733
|
+
.justify_content = @fromBackingInt(@intCast(@min(input.justify_content, 5))),
|
|
734
|
+
.align_items = @fromBackingInt(@intCast(@min(input.align_items, 4))),
|
|
735
|
+
.align_self = if (input.align_self == 0) null else @fromBackingInt(@intCast(@min(input.align_self - 1, 4))),
|
|
736
|
+
.flex_wrap = input.flex_wrap != 0,
|
|
737
|
+
.align_content = @fromBackingInt(@intCast(@min(input.align_content, 6))),
|
|
738
|
+
.margin_top = finiteNonNegative(input.margin_top),
|
|
739
|
+
.margin_right = finiteNonNegative(input.margin_right),
|
|
740
|
+
.margin_bottom = finiteNonNegative(input.margin_bottom),
|
|
741
|
+
.margin_left = finiteNonNegative(input.margin_left),
|
|
742
|
+
.padding_top = finiteNonNegative(input.padding_top),
|
|
743
|
+
.padding_right = finiteNonNegative(input.padding_right),
|
|
744
|
+
.padding_bottom = finiteNonNegative(input.padding_bottom),
|
|
745
|
+
.padding_left = finiteNonNegative(input.padding_left),
|
|
746
|
+
.grid = input.grid != 0,
|
|
747
|
+
.grid_columns = @max(@as(u32, 1), input.grid_columns),
|
|
748
|
+
.row_gap = finiteNonNegative(input.row_gap),
|
|
749
|
+
.column_gap = finiteNonNegative(input.column_gap),
|
|
750
|
+
.grid_auto_rows = optionalFinite(input.grid_auto_rows),
|
|
751
|
+
.grid_rows = if (input.grid_rows == 0) null else input.grid_rows,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
617
755
|
fn optionalFinite(value: f32) ?f32 {
|
|
618
756
|
return if (std.math.isFinite(value)) value else null;
|
|
619
757
|
}
|
|
@@ -1359,12 +1497,26 @@ fn layoutGrid(
|
|
|
1359
1497
|
const row_gap = axisGap(node.style.row_gap, node.style.gap);
|
|
1360
1498
|
const available_width = @max(0, width - column_gap * @as(f32, @floatFromInt(column_count - 1)));
|
|
1361
1499
|
const cell_width = available_width / @as(f32, @floatFromInt(column_count));
|
|
1500
|
+
const natural_rows = if (node.children.len == 0) 0 else (node.children.len + column_count - 1) / column_count;
|
|
1501
|
+
const row_count = @max(natural_rows, @as(usize, @intCast(node.style.grid_rows orelse 0)));
|
|
1502
|
+
const row_heights = std.heap.page_allocator.alloc(f32, row_count) catch return error.FrameBufferTooSmall;
|
|
1503
|
+
defer std.heap.page_allocator.free(row_heights);
|
|
1504
|
+
for (0..row_count) |row| {
|
|
1505
|
+
var track = gridRowHeight(node, row, column_count);
|
|
1506
|
+
if (node.style.grid_rows != null) {
|
|
1507
|
+
const gap_count = if (row_count > 1) row_count - 1 else 0;
|
|
1508
|
+
const gaps = row_gap * @as(f32, @floatFromInt(gap_count));
|
|
1509
|
+
const equal = @max(0, (height - gaps) / @as(f32, @floatFromInt(@max(row_count, 1))));
|
|
1510
|
+
track = @max(track, equal);
|
|
1511
|
+
}
|
|
1512
|
+
row_heights[row] = track;
|
|
1513
|
+
}
|
|
1362
1514
|
var next_index = frame_index + 1;
|
|
1363
1515
|
var row_y = y;
|
|
1364
1516
|
for (node.children, 0..) |child, index| {
|
|
1365
1517
|
const row = index / column_count;
|
|
1366
1518
|
const column = index % column_count;
|
|
1367
|
-
const row_height =
|
|
1519
|
+
const row_height = row_heights[row];
|
|
1368
1520
|
const child_available_width = @max(0, cell_width - horizontalMargins(child));
|
|
1369
1521
|
const ratio_width = if (validAspectRatio(child.style.aspect_ratio)) |ratio|
|
|
1370
1522
|
if (child.style.width == null and child.style.height != null) child.style.height.? * ratio else null
|
|
@@ -1395,7 +1547,6 @@ fn layoutGrid(
|
|
|
1395
1547
|
row_y += row_height + row_gap;
|
|
1396
1548
|
}
|
|
1397
1549
|
}
|
|
1398
|
-
_ = height;
|
|
1399
1550
|
return next_index;
|
|
1400
1551
|
}
|
|
1401
1552
|
|
|
@@ -1708,6 +1859,47 @@ fn emptyLayoutStyleInputV4() LayoutStyleInputV4 {
|
|
|
1708
1859
|
};
|
|
1709
1860
|
}
|
|
1710
1861
|
|
|
1862
|
+
fn emptyLayoutStyleInputV5() LayoutStyleInputV5 {
|
|
1863
|
+
const unset = std.math.nan(f32);
|
|
1864
|
+
return .{
|
|
1865
|
+
.direction = 0,
|
|
1866
|
+
.width = unset,
|
|
1867
|
+
.height = unset,
|
|
1868
|
+
.intrinsic_width = unset,
|
|
1869
|
+
.intrinsic_height = unset,
|
|
1870
|
+
.baseline = unset,
|
|
1871
|
+
.aspect_ratio = unset,
|
|
1872
|
+
.flex_basis = unset,
|
|
1873
|
+
.flex_grow = 0,
|
|
1874
|
+
.flex_shrink = 0,
|
|
1875
|
+
.min_width = unset,
|
|
1876
|
+
.max_width = unset,
|
|
1877
|
+
.min_height = unset,
|
|
1878
|
+
.max_height = unset,
|
|
1879
|
+
.gap = 0,
|
|
1880
|
+
.justify_content = 0,
|
|
1881
|
+
.align_items = 3,
|
|
1882
|
+
.align_self = 0,
|
|
1883
|
+
.flex_wrap = 0,
|
|
1884
|
+
.align_content = 5,
|
|
1885
|
+
.margin_top = 0,
|
|
1886
|
+
.margin_right = 0,
|
|
1887
|
+
.margin_bottom = 0,
|
|
1888
|
+
.margin_left = 0,
|
|
1889
|
+
.padding_top = 0,
|
|
1890
|
+
.padding_right = 0,
|
|
1891
|
+
.padding_bottom = 0,
|
|
1892
|
+
.padding_left = 0,
|
|
1893
|
+
.grid = 0,
|
|
1894
|
+
.grid_columns = 1,
|
|
1895
|
+
.row_gap = 0,
|
|
1896
|
+
.column_gap = 0,
|
|
1897
|
+
.reverse = 0,
|
|
1898
|
+
.grid_auto_rows = unset,
|
|
1899
|
+
.grid_rows = 0,
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1711
1903
|
fn clampBounds(value: f32, lower: f32, upper: f32) f32 {
|
|
1712
1904
|
var result = finiteNonNegative(value);
|
|
1713
1905
|
if (std.math.isFinite(lower) and lower >= 0) result = @max(result, lower);
|
|
@@ -1818,6 +2010,38 @@ test "v4 tree layout carries auto rows through nested grids" {
|
|
|
1818
2010
|
try std.testing.expectEqual(@as(f32, 18), frames[4].y);
|
|
1819
2011
|
}
|
|
1820
2012
|
|
|
2013
|
+
test "v5 tree layout carries explicit equal grid rows through the shared ABI" {
|
|
2014
|
+
var root_style = emptyLayoutStyleInputV5();
|
|
2015
|
+
root_style.width = 100;
|
|
2016
|
+
root_style.height = 100;
|
|
2017
|
+
root_style.grid = 1;
|
|
2018
|
+
root_style.grid_columns = 2;
|
|
2019
|
+
root_style.row_gap = 4;
|
|
2020
|
+
root_style.grid_rows = 2;
|
|
2021
|
+
|
|
2022
|
+
var first_style = emptyLayoutStyleInputV5();
|
|
2023
|
+
first_style.intrinsic_width = 10;
|
|
2024
|
+
first_style.intrinsic_height = 6;
|
|
2025
|
+
var second_style = emptyLayoutStyleInputV5();
|
|
2026
|
+
second_style.intrinsic_width = 10;
|
|
2027
|
+
second_style.intrinsic_height = 7;
|
|
2028
|
+
var third_style = emptyLayoutStyleInputV5();
|
|
2029
|
+
third_style.intrinsic_width = 10;
|
|
2030
|
+
third_style.intrinsic_height = 5;
|
|
2031
|
+
|
|
2032
|
+
const nodes = [_]LayoutNodeInputV5{
|
|
2033
|
+
.{ .style = root_style, .first_child = 1, .child_count = 3 },
|
|
2034
|
+
.{ .style = first_style, .first_child = 0, .child_count = 0 },
|
|
2035
|
+
.{ .style = second_style, .first_child = 0, .child_count = 0 },
|
|
2036
|
+
.{ .style = third_style, .first_child = 0, .child_count = 0 },
|
|
2037
|
+
};
|
|
2038
|
+
var frames: [4]Frame = undefined;
|
|
2039
|
+
try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree_v5(100, 100, &nodes, nodes.len, &frames, frames.len));
|
|
2040
|
+
try std.testing.expectEqual(@as(f32, 0), frames[1].y);
|
|
2041
|
+
try std.testing.expectEqual(@as(f32, 0), frames[2].y);
|
|
2042
|
+
try std.testing.expectEqual(@as(f32, 52), frames[3].y);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
1821
2045
|
test "lays out nested flat trees with absolute descendant frames" {
|
|
1822
2046
|
var root_style = emptyLayoutStyleInput();
|
|
1823
2047
|
root_style.width = 200;
|
package/dist/mobile-testing.js
CHANGED
|
@@ -54,6 +54,7 @@ async function verifyMobileMediaCache(options) {
|
|
|
54
54
|
const scratch = await mkdtemp(join(tmpdir(), "craft-media-probe-"));
|
|
55
55
|
const bytes = new Uint8Array(await Bun.file(join(fixtures, "demo.mp4")).arrayBuffer());
|
|
56
56
|
const counts = new Map;
|
|
57
|
+
const resumed = new Set;
|
|
57
58
|
const timers = new Set;
|
|
58
59
|
const server = Bun.listen({
|
|
59
60
|
hostname: "0.0.0.0",
|
|
@@ -72,6 +73,20 @@ async function verifyMobileMediaCache(options) {
|
|
|
72
73
|
const path = socket.data.request.split(" ")[1];
|
|
73
74
|
const n = (counts.get(path) || 0) + 1;
|
|
74
75
|
counts.set(path, n);
|
|
76
|
+
const from = Number(/^range:\s*bytes=(\d+)-\s*$/im.exec(socket.data.request)?.[1] ?? Number.NaN);
|
|
77
|
+
if (path === "/android/restart.mp4" && n > 1 && from > 0 && from < bytes.length) {
|
|
78
|
+
resumed.add(path);
|
|
79
|
+
socket.write(`HTTP/1.1 206 Partial Content\r
|
|
80
|
+
Content-Type: video/mp4\r
|
|
81
|
+
Content-Range: bytes ${from}-${bytes.length - 1}/${bytes.length}\r
|
|
82
|
+
Content-Length: ${bytes.length - from}\r
|
|
83
|
+
Connection: close\r
|
|
84
|
+
\r
|
|
85
|
+
`);
|
|
86
|
+
socket.write(bytes.subarray(from));
|
|
87
|
+
socket.end();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
75
90
|
const empty = path.endsWith("/empty.mp4");
|
|
76
91
|
const partial = path.endsWith("/partial.mp4");
|
|
77
92
|
const cut = (path.endsWith("/broken.mp4") || path.endsWith("/restart.mp4")) && n === 1;
|
|
@@ -207,6 +222,7 @@ include(":app")`);
|
|
|
207
222
|
`);
|
|
208
223
|
await Bun.write(join(android, "gradle.properties"), `org.gradle.jvmargs=-Xmx768m
|
|
209
224
|
org.gradle.workers.max=2
|
|
225
|
+
android.useAndroidX=true
|
|
210
226
|
`);
|
|
211
227
|
await Bun.write(join(app, "build.gradle.kts"), `plugins { id("com.android.application");
|
|
212
228
|
id("org.jetbrains.kotlin.android") }
|
|
@@ -264,11 +280,13 @@ ${await Bun.file(join(fixtures, "media-cache.kt")).text()}`);
|
|
|
264
280
|
if (counts.get("/android/restart.mp4") !== 1)
|
|
265
281
|
throw new Error("Android process did not start interrupted download");
|
|
266
282
|
await probe("recover");
|
|
283
|
+
if (!resumed.has("/android/restart.mp4"))
|
|
284
|
+
throw new Error("Android did not resume the interrupted download from its part file");
|
|
267
285
|
if (counts.get("/android/slow.mp4") !== 1)
|
|
268
286
|
throw new Error("Android duplicate fetch: keep did not coalesce");
|
|
269
287
|
if (counts.get("/android/account/slow.mp4") !== 2)
|
|
270
288
|
throw new Error("Android account switch did not start a fresh transfer");
|
|
271
|
-
print("PASS: Android emulator interruption, restart, retry, prune and cached video decode");
|
|
289
|
+
print("PASS: Android emulator interruption, resumed restart, retry, prune and cached video decode");
|
|
272
290
|
}
|
|
273
291
|
}
|
|
274
292
|
} finally {
|
|
@@ -459,7 +477,7 @@ ${errors || output}`);
|
|
|
459
477
|
return { restart, installUpdate, installedBuild };
|
|
460
478
|
}
|
|
461
479
|
export {
|
|
462
|
-
|
|
480
|
+
createMobileTestDevice,
|
|
463
481
|
recordIosSimulatorVideo,
|
|
464
|
-
|
|
482
|
+
verifyMobileMediaCache
|
|
465
483
|
};
|