craft-native 0.0.213 → 0.0.214
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/templates/CraftNativeShell.kt.template +55 -1
- package/dist/android/templates/MainActivityNative.kt.template +25 -20
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftNativeFlatList.swift +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +285 -51
- package/dist/macos/templates/native_layout.zig +37 -0
- package/package.json +1 -1
|
@@ -395,6 +395,7 @@ internal object CraftNativePrelude {
|
|
|
395
395
|
if (typeof globalThis.window === 'undefined') globalThis.window = globalThis;
|
|
396
396
|
(function() {
|
|
397
397
|
var forward = null;
|
|
398
|
+
var listeners = [];
|
|
398
399
|
var queued = [];
|
|
399
400
|
var sync = { storage: {}, secure: {}, snapshots: {} };
|
|
400
401
|
var preload = $STORES;
|
|
@@ -445,7 +446,11 @@ internal object CraftNativePrelude {
|
|
|
445
446
|
}
|
|
446
447
|
function send(value) { if (typeof value !== 'string') value = JSON.stringify(value); $post }
|
|
447
448
|
function emit(message) {
|
|
448
|
-
|
|
449
|
+
var raw = typeof message === 'string' ? message : JSON.stringify(message);
|
|
450
|
+
listeners.slice().forEach(function(callback) {
|
|
451
|
+
try { callback(raw); } catch (error) {}
|
|
452
|
+
});
|
|
453
|
+
if (forward) forward(raw);
|
|
449
454
|
else queued.push(message);
|
|
450
455
|
}
|
|
451
456
|
function deliver(raw) {
|
|
@@ -515,6 +520,14 @@ internal object CraftNativePrelude {
|
|
|
515
520
|
var pending = queued;
|
|
516
521
|
queued = [];
|
|
517
522
|
pending.forEach(emit);
|
|
523
|
+
},
|
|
524
|
+
addMessageListener: function(callback) {
|
|
525
|
+
if (typeof callback !== 'function') return function() {};
|
|
526
|
+
listeners.push(callback);
|
|
527
|
+
return function() {
|
|
528
|
+
var index = listeners.indexOf(callback);
|
|
529
|
+
if (index >= 0) listeners.splice(index, 1);
|
|
530
|
+
};
|
|
518
531
|
}
|
|
519
532
|
};
|
|
520
533
|
$receiveSetup
|
|
@@ -563,6 +576,47 @@ internal object CraftNativePrelude {
|
|
|
563
576
|
return value === undefined ? null : value;
|
|
564
577
|
}
|
|
565
578
|
});
|
|
579
|
+
// Older native bundles call the page-shaped `craft.dialog` API
|
|
580
|
+
// directly. Keep that API on the capability bridge so JavaScript
|
|
581
|
+
// Core/JavaScriptSandbox screens never fall back to a DOM sheet.
|
|
582
|
+
if (Array.isArray(__stxNativeBridge.capabilities) && __stxNativeBridge.capabilities.indexOf('dialog') >= 0 && !craft.dialog) {
|
|
583
|
+
var nextDialogRequest = 0;
|
|
584
|
+
function dialogRequest(method, args) {
|
|
585
|
+
return new Promise(function(resolve, reject) {
|
|
586
|
+
var id = 'craft-dialog-' + (++nextDialogRequest);
|
|
587
|
+
var remove = __stxNativeBridge.addMessageListener(function(raw) {
|
|
588
|
+
var message;
|
|
589
|
+
try { message = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch (error) { return; }
|
|
590
|
+
var payload = message && message.payload;
|
|
591
|
+
if (!payload || payload.requestId !== id) return;
|
|
592
|
+
remove();
|
|
593
|
+
if (message.type === 'API_ERROR') {
|
|
594
|
+
var failure = new Error(payload.message || 'Native dialog failed');
|
|
595
|
+
failure.code = payload.code || 'NATIVE_CALL_FAILED';
|
|
596
|
+
reject(failure);
|
|
597
|
+
} else if (message.type === 'API_RESPONSE') {
|
|
598
|
+
resolve(payload.data === undefined ? null : payload.data);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
__stxNativeBridge.postMessage(JSON.stringify({
|
|
602
|
+
type: 'API_REQUEST',
|
|
603
|
+
id: id,
|
|
604
|
+
payload: {
|
|
605
|
+
version: __stxNativeBridge.capabilityProtocolVersion || 1,
|
|
606
|
+
module: 'Dialog',
|
|
607
|
+
method: method,
|
|
608
|
+
args: args,
|
|
609
|
+
untimed: true,
|
|
610
|
+
},
|
|
611
|
+
}));
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
craft.dialog = {
|
|
615
|
+
alert: function(options) { return dialogRequest('alert', [options || {}]).then(function() {}); },
|
|
616
|
+
confirm: function(options) { return dialogRequest('confirm', [options || {}]).then(Boolean); },
|
|
617
|
+
actionSheet: function(options) { return dialogRequest('actionSheet', [options || {}]).then(function(id) { return typeof id === 'string' ? id : null; }); },
|
|
618
|
+
};
|
|
619
|
+
}
|
|
566
620
|
})();
|
|
567
621
|
""".trimIndent()
|
|
568
622
|
}
|
|
@@ -1327,9 +1327,9 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1327
1327
|
return result
|
|
1328
1328
|
}
|
|
1329
1329
|
|
|
1330
|
-
private fun defaultFlexShrink(view: View): Double = when {
|
|
1330
|
+
private fun defaultFlexShrink(view: View, horizontal: Boolean = orientation == HORIZONTAL): Double = when {
|
|
1331
1331
|
view is ScrollView || view is HorizontalScrollView -> 1.0
|
|
1332
|
-
view is TextView && view !is Button && view !is EditText &&
|
|
1332
|
+
view is TextView && view !is Button && view !is EditText && horizontal -> 1.0
|
|
1333
1333
|
else -> 0.0
|
|
1334
1334
|
}
|
|
1335
1335
|
|
|
@@ -1372,6 +1372,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1372
1372
|
val margins: IntArray,
|
|
1373
1373
|
val grid: Boolean,
|
|
1374
1374
|
val gridColumns: Int,
|
|
1375
|
+
val gridAutoRows: CraftNativeLength?,
|
|
1375
1376
|
val reverse: Boolean,
|
|
1376
1377
|
val wrapReverse: Boolean,
|
|
1377
1378
|
val children: List<SharedTreeNode>,
|
|
@@ -1383,14 +1384,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1383
1384
|
val frames: FloatArray,
|
|
1384
1385
|
)
|
|
1385
1386
|
|
|
1386
|
-
/**
|
|
1387
|
-
* Flatten a flex or equal-track grid subtree for the shared ABI. The local
|
|
1388
|
-
* layout stays authoritative for grid-auto-rows,
|
|
1389
|
-
* absolute/modal children, and scroll hosts; those cases cannot be
|
|
1390
|
-
* represented without changing their native measurement contract.
|
|
1391
|
-
*/
|
|
1387
|
+
/** Flatten a flex or equal-track grid subtree for the shared ABI. */
|
|
1392
1388
|
private fun sharedTreeChildren(width: Int, height: Int): List<Entry>? {
|
|
1393
|
-
if (gridAutoRows != null) return null
|
|
1394
1389
|
if (grid) {
|
|
1395
1390
|
val layout = gridLayout(width, height)
|
|
1396
1391
|
if (layout.absolute.isNotEmpty()) return null
|
|
@@ -1428,6 +1423,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1428
1423
|
margins = intArrayOf(lp?.leftMargin ?: 0, lp?.rightMargin ?: 0, lp?.topMargin ?: 0, lp?.bottomMargin ?: 0),
|
|
1429
1424
|
grid = nested?.grid ?: false,
|
|
1430
1425
|
gridColumns = nested?.gridColumns ?: 1,
|
|
1426
|
+
gridAutoRows = nested?.gridAutoRows,
|
|
1431
1427
|
reverse = nested?.reverse ?: false,
|
|
1432
1428
|
wrapReverse = nested?.wrapReverse ?: false,
|
|
1433
1429
|
children = children.map { child -> sharedTreeNode(child) ?: return null },
|
|
@@ -1435,7 +1431,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1435
1431
|
}
|
|
1436
1432
|
|
|
1437
1433
|
private fun sharedTreeFrames(entries: List<Entry>, width: Int, height: Int): SharedTreeResult? {
|
|
1438
|
-
if (entries.isEmpty() || width < 0 || height < 0
|
|
1434
|
+
if (entries.isEmpty() || width < 0 || height < 0) return null
|
|
1439
1435
|
val children = entries.map { sharedTreeNode(it) ?: return null }
|
|
1440
1436
|
val root = SharedTreeNode(
|
|
1441
1437
|
view = null,
|
|
@@ -1455,6 +1451,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1455
1451
|
margins = intArrayOf(0, 0, 0, 0),
|
|
1456
1452
|
grid = grid,
|
|
1457
1453
|
gridColumns = gridColumns,
|
|
1454
|
+
gridAutoRows = gridAutoRows,
|
|
1458
1455
|
reverse = reverse,
|
|
1459
1456
|
wrapReverse = wrapReverse,
|
|
1460
1457
|
children = children,
|
|
@@ -1470,14 +1467,19 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1470
1467
|
append(root, -1)
|
|
1471
1468
|
val reverseAware = nodes.any { it.reverse || it.wrapReverse }
|
|
1472
1469
|
val wrapReverseAware = nodes.any { it.wrapReverse }
|
|
1473
|
-
val
|
|
1470
|
+
val autoRowsAware = nodes.any { it.gridAutoRows != null }
|
|
1471
|
+
val fields = when {
|
|
1472
|
+
autoRowsAware -> 34
|
|
1473
|
+
reverseAware -> 33
|
|
1474
|
+
else -> 32
|
|
1475
|
+
}
|
|
1474
1476
|
val nodeData = FloatArray(nodes.size * fields) { Float.NaN }
|
|
1475
1477
|
val treeMeta = FloatArray(nodes.size * 2)
|
|
1476
1478
|
fun enumBits(value: Int): Float = Float.fromBits(value)
|
|
1477
1479
|
fun put(base: Int, index: Int, value: Float) { nodeData[base + index] = value }
|
|
1478
1480
|
fun putEnum(base: Int, index: Int, value: Int) { put(base, index, enumBits(value)) }
|
|
1479
1481
|
var cursor = 0
|
|
1480
|
-
fun write(node: SharedTreeNode, parentWidth: Int, parentHeight: Int, rootNode: Boolean) {
|
|
1482
|
+
fun write(node: SharedTreeNode, parentWidth: Int, parentHeight: Int, rootNode: Boolean, parentDirection: Int) {
|
|
1481
1483
|
val index = cursor++
|
|
1482
1484
|
val base = index * fields
|
|
1483
1485
|
val measuredWidth = if (rootNode) width else node.style.width?.resolve(parentWidth) ?: node.intrinsicWidth.roundToInt()
|
|
@@ -1494,7 +1496,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1494
1496
|
put(base, 6, node.style.aspectRatio?.toFloat() ?: Float.NaN)
|
|
1495
1497
|
put(base, 7, sharedLength(node.style.flexBasis, mainSize))
|
|
1496
1498
|
put(base, 8, if (rootNode) 0f else node.style.flexGrow.toFloat().coerceAtLeast(0f))
|
|
1497
|
-
put(base, 9, if (rootNode) 0f else (node.style.flexShrink ?: 0.0).toFloat().coerceAtLeast(0f))
|
|
1499
|
+
put(base, 9, if (rootNode) 0f else (node.style.flexShrink ?: node.view?.let { defaultFlexShrink(it, parentDirection == 0) } ?: 0.0).toFloat().coerceAtLeast(0f))
|
|
1498
1500
|
put(base, 10, sharedLength(node.style.minWidth, contentWidth))
|
|
1499
1501
|
put(base, 11, sharedLength(node.style.maxWidth, contentWidth))
|
|
1500
1502
|
put(base, 12, sharedLength(node.style.minHeight, contentHeight))
|
|
@@ -1517,14 +1519,17 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1517
1519
|
put(base, 29, node.gridColumns.toFloat())
|
|
1518
1520
|
put(base, 30, node.rowGap)
|
|
1519
1521
|
put(base, 31, node.columnGap)
|
|
1520
|
-
if (
|
|
1522
|
+
if (fields >= 33) putEnum(base, 32, (if (node.reverse) 1 else 0) or (if (node.wrapReverse) 2 else 0))
|
|
1523
|
+
if (autoRowsAware) put(base, 33, sharedLength(node.gridAutoRows, contentHeight))
|
|
1521
1524
|
val firstChild = cursor
|
|
1522
|
-
node.children.forEach { child -> write(child, contentWidth, contentHeight, false) }
|
|
1525
|
+
node.children.forEach { child -> write(child, contentWidth, contentHeight, false, node.direction) }
|
|
1523
1526
|
treeMeta[index * 2] = firstChild.toFloat()
|
|
1524
1527
|
treeMeta[index * 2 + 1] = (cursor - firstChild).toFloat()
|
|
1525
1528
|
}
|
|
1526
|
-
write(root, width, height, true)
|
|
1527
|
-
val frames = (if (
|
|
1529
|
+
write(root, width, height, true, root.direction)
|
|
1530
|
+
val frames = (if (autoRowsAware) {
|
|
1531
|
+
CraftNative.layoutTreeV4(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1532
|
+
} else if (wrapReverseAware) {
|
|
1528
1533
|
CraftNative.layoutTreeV3(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1529
1534
|
} else if (reverseAware) {
|
|
1530
1535
|
CraftNative.layoutTreeV2(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
@@ -1537,9 +1542,9 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1537
1542
|
/**
|
|
1538
1543
|
* Use the host-neutral tree ABI when platform measurements are complete:
|
|
1539
1544
|
* definite direct-child containers, including wrapped flows, equal-track
|
|
1540
|
-
* grids,
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1545
|
+
* grids, independent row and column gaps, and grid-auto-rows. Absolute
|
|
1546
|
+
* children, missing runtimes, and malformed results deliberately fall
|
|
1547
|
+
* through to the established Kotlin layout below.
|
|
1543
1548
|
*/
|
|
1544
1549
|
private fun sharedDirectFrames(entries: List<Entry>, width: Int, height: Int): FloatArray? {
|
|
1545
1550
|
if (entries.isEmpty() || width < 0 || height < 0 || grid || reverse || wrapReverse || gridAutoRows != null) return null
|
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.
|
|
3427
|
+
var version = "0.0.214";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -308,7 +308,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
308
308
|
|
|
309
309
|
func setContentContainerStyle(_ raw: [String: Any]?) {
|
|
310
310
|
func nonNegative(_ value: Any?) -> CGFloat? {
|
|
311
|
-
guard let number = value as? NSNumber else { return nil }
|
|
311
|
+
guard !(value is Bool), let number = value as? NSNumber else { return nil }
|
|
312
312
|
let points = CGFloat(truncating: number)
|
|
313
313
|
guard points.isFinite, points >= 0 else { return nil }
|
|
314
314
|
return points
|
|
@@ -16,7 +16,9 @@ private let craftNativeCapabilityTimeoutMilliseconds = 30_000
|
|
|
16
16
|
/// which has its own: the keyboard-avoiding view and SVG text read theirs
|
|
17
17
|
/// with it, and without it the template did not compile.
|
|
18
18
|
private func number(_ value: Any?) -> CGFloat? {
|
|
19
|
-
|
|
19
|
+
// JavaScript booleans bridge to NSNumber on Apple platforms; they are
|
|
20
|
+
// invalid numeric styles and must stay unset like Android's Number cast.
|
|
21
|
+
guard !(value is Bool), let number = value as? NSNumber else { return nil }
|
|
20
22
|
let value = CGFloat(truncating: number)
|
|
21
23
|
return value.isFinite ? value : nil
|
|
22
24
|
}
|
|
@@ -30,12 +32,12 @@ private func craftNativeFontWeight(_ value: Any?) -> String? {
|
|
|
30
32
|
|
|
31
33
|
private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
32
34
|
if let value = value as? String, !value.isEmpty { return value }
|
|
33
|
-
if let value = value as? NSNumber, value.doubleValue.isFinite { return String(value.doubleValue) }
|
|
35
|
+
if !(value is Bool), let value = value as? NSNumber, value.doubleValue.isFinite { return String(value.doubleValue) }
|
|
34
36
|
return nil
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
private func craftNativeGridColumnCount(_ value: Any?) -> Int {
|
|
38
|
-
if let number = value as? NSNumber { return max(1, number.intValue) }
|
|
40
|
+
if !(value is Bool), let number = value as? NSNumber { return max(1, number.intValue) }
|
|
39
41
|
guard let text = value as? String else { return 1 }
|
|
40
42
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
41
43
|
if trimmed.hasPrefix("repeat(") {
|
|
@@ -190,6 +192,49 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
190
192
|
var childCount: UInt32
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
private struct StyleInputV4 {
|
|
196
|
+
var direction: UInt32 = 0
|
|
197
|
+
var width: Float = .nan
|
|
198
|
+
var height: Float = .nan
|
|
199
|
+
var intrinsicWidth: Float = .nan
|
|
200
|
+
var intrinsicHeight: Float = .nan
|
|
201
|
+
var baseline: Float = .nan
|
|
202
|
+
var aspectRatio: Float = .nan
|
|
203
|
+
var flexBasis: Float = .nan
|
|
204
|
+
var flexGrow: Float = 0
|
|
205
|
+
var flexShrink: Float = 0
|
|
206
|
+
var minWidth: Float = .nan
|
|
207
|
+
var maxWidth: Float = .nan
|
|
208
|
+
var minHeight: Float = .nan
|
|
209
|
+
var maxHeight: Float = .nan
|
|
210
|
+
var gap: Float = 0
|
|
211
|
+
var justifyContent: UInt32 = 0
|
|
212
|
+
var alignItems: UInt32 = 3
|
|
213
|
+
var alignSelf: UInt32 = 0
|
|
214
|
+
var flexWrap: UInt32 = 0
|
|
215
|
+
var alignContent: UInt32 = 6
|
|
216
|
+
var marginTop: Float = 0
|
|
217
|
+
var marginRight: Float = 0
|
|
218
|
+
var marginBottom: Float = 0
|
|
219
|
+
var marginLeft: Float = 0
|
|
220
|
+
var paddingTop: Float = 0
|
|
221
|
+
var paddingRight: Float = 0
|
|
222
|
+
var paddingBottom: Float = 0
|
|
223
|
+
var paddingLeft: Float = 0
|
|
224
|
+
var grid: UInt32 = 0
|
|
225
|
+
var gridColumns: UInt32 = 1
|
|
226
|
+
var rowGap: Float = 0
|
|
227
|
+
var columnGap: Float = 0
|
|
228
|
+
var reverse: UInt32 = 0
|
|
229
|
+
var gridAutoRows: Float = .nan
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private struct NodeInputV4 {
|
|
233
|
+
var style: StyleInputV4
|
|
234
|
+
var firstChild: UInt32
|
|
235
|
+
var childCount: UInt32
|
|
236
|
+
}
|
|
237
|
+
|
|
193
238
|
fileprivate struct Frame {
|
|
194
239
|
var x: Float
|
|
195
240
|
var y: Float
|
|
@@ -213,6 +258,7 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
213
258
|
let padding: UIEdgeInsets
|
|
214
259
|
let grid: Bool
|
|
215
260
|
let gridColumns: Int
|
|
261
|
+
let gridAutoRows: CraftNativeLength?
|
|
216
262
|
let reverse: Bool
|
|
217
263
|
let wrapReverse: Bool
|
|
218
264
|
let children: [TreeNode]
|
|
@@ -247,6 +293,11 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
247
293
|
return unsafeBitCast(symbol, to: FunctionV2.self)
|
|
248
294
|
}()
|
|
249
295
|
|
|
296
|
+
private static let functionV4: FunctionV2? = {
|
|
297
|
+
guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_layout_tree_v4") else { return nil }
|
|
298
|
+
return unsafeBitCast(symbol, to: FunctionV2.self)
|
|
299
|
+
}()
|
|
300
|
+
|
|
250
301
|
private static func finite(_ value: CGFloat?) -> Float {
|
|
251
302
|
guard let value, value.isFinite else { return .nan }
|
|
252
303
|
return Float(value)
|
|
@@ -359,6 +410,9 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
359
410
|
|
|
360
411
|
fileprivate static func treeFrames(root: TreeNode, width: CGFloat, height: CGFloat) -> TreeResult? {
|
|
361
412
|
guard let function, width.isFinite, height.isFinite, width >= 0, height >= 0, !root.children.isEmpty else { return nil }
|
|
413
|
+
if containsGridAutoRows(root) {
|
|
414
|
+
return treeFramesV4(root: root, width: width, height: height, function: functionV4)
|
|
415
|
+
}
|
|
362
416
|
if containsWrapReverse(root) {
|
|
363
417
|
return treeFramesV2(root: root, width: width, height: height, function: functionV3)
|
|
364
418
|
}
|
|
@@ -375,7 +429,7 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
375
429
|
}
|
|
376
430
|
append(root, parent: -1)
|
|
377
431
|
|
|
378
|
-
func put(_ input: inout StyleInput, _ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool) {
|
|
432
|
+
func put(_ input: inout StyleInput, _ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool, parentDirection: NSLayoutConstraint.Axis) {
|
|
379
433
|
let contentWidth = max(0, (rootNode ? width : node.intrinsic.width) - node.padding.left - node.padding.right)
|
|
380
434
|
let contentHeight = max(0, (rootNode ? height : node.intrinsic.height) - node.padding.top - node.padding.bottom)
|
|
381
435
|
let mainSize = node.direction == .horizontal ? contentWidth : contentHeight
|
|
@@ -388,7 +442,7 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
388
442
|
input.aspectRatio = finite(node.style.aspectRatio)
|
|
389
443
|
input.flexBasis = length(node.style.flexBasis, base: mainSize)
|
|
390
444
|
input.flexGrow = rootNode ? 0 : Float(max(0, node.style.flexGrow))
|
|
391
|
-
let defaultShrink: CGFloat = node.view is UIScrollView || (node.view is UILabel &&
|
|
445
|
+
let defaultShrink: CGFloat = node.view is UIScrollView || (node.view is UILabel && parentDirection == .horizontal) ? 1 : 0
|
|
392
446
|
input.flexShrink = rootNode ? 0 : Float(max(0, node.style.flexShrink ?? defaultShrink))
|
|
393
447
|
input.minWidth = length(node.style.minWidth, base: contentWidth)
|
|
394
448
|
input.maxWidth = length(node.style.maxWidth, base: contentWidth)
|
|
@@ -417,18 +471,18 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
417
471
|
var styles = [StyleInput](repeating: StyleInput(), count: nodes.count)
|
|
418
472
|
var metadata = [Float](repeating: 0, count: nodes.count * 2)
|
|
419
473
|
var cursor = 0
|
|
420
|
-
func write(_ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool) {
|
|
474
|
+
func write(_ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool, parentDirection: NSLayoutConstraint.Axis) {
|
|
421
475
|
let index = cursor
|
|
422
476
|
cursor += 1
|
|
423
|
-
put(&styles[index], node, parentWidth: parentWidth, parentHeight: parentHeight, rootNode: rootNode)
|
|
477
|
+
put(&styles[index], node, parentWidth: parentWidth, parentHeight: parentHeight, rootNode: rootNode, parentDirection: parentDirection)
|
|
424
478
|
let contentWidth = max(0, (rootNode ? width : node.intrinsic.width) - node.padding.left - node.padding.right)
|
|
425
479
|
let contentHeight = max(0, (rootNode ? height : node.intrinsic.height) - node.padding.top - node.padding.bottom)
|
|
426
480
|
let firstChild = cursor
|
|
427
|
-
node.children.forEach { write($0, parentWidth: contentWidth, parentHeight: contentHeight, rootNode: false) }
|
|
481
|
+
node.children.forEach { write($0, parentWidth: contentWidth, parentHeight: contentHeight, rootNode: false, parentDirection: node.direction) }
|
|
428
482
|
metadata[index * 2] = Float(firstChild)
|
|
429
483
|
metadata[index * 2 + 1] = Float(cursor - firstChild)
|
|
430
484
|
}
|
|
431
|
-
write(root, parentWidth: width, parentHeight: height, rootNode: true)
|
|
485
|
+
write(root, parentWidth: width, parentHeight: height, rootNode: true, parentDirection: root.direction)
|
|
432
486
|
let inputs = zip(styles, stride(from: 0, to: metadata.count, by: 2)).map { style, offset in
|
|
433
487
|
NodeInput(style: style, firstChild: UInt32(metadata[offset]), childCount: UInt32(metadata[offset + 1]))
|
|
434
488
|
}
|
|
@@ -460,6 +514,10 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
460
514
|
node.wrapReverse || node.children.contains(where: containsWrapReverse)
|
|
461
515
|
}
|
|
462
516
|
|
|
517
|
+
private static func containsGridAutoRows(_ node: TreeNode) -> Bool {
|
|
518
|
+
node.gridAutoRows != nil || node.children.contains(where: containsGridAutoRows)
|
|
519
|
+
}
|
|
520
|
+
|
|
463
521
|
private static func treeFramesV2(root: TreeNode, width: CGFloat, height: CGFloat, function: FunctionV2?) -> TreeResult? {
|
|
464
522
|
guard let function, width.isFinite, height.isFinite, width >= 0, height >= 0, !root.children.isEmpty else { return nil }
|
|
465
523
|
var nodes: [TreeNode] = []
|
|
@@ -472,7 +530,7 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
472
530
|
}
|
|
473
531
|
append(root, parent: -1)
|
|
474
532
|
|
|
475
|
-
func put(_ input: inout StyleInputV2, _ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool) {
|
|
533
|
+
func put(_ input: inout StyleInputV2, _ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool, parentDirection: NSLayoutConstraint.Axis) {
|
|
476
534
|
let contentWidth = max(0, (rootNode ? width : node.intrinsic.width) - node.padding.left - node.padding.right)
|
|
477
535
|
let contentHeight = max(0, (rootNode ? height : node.intrinsic.height) - node.padding.top - node.padding.bottom)
|
|
478
536
|
let mainSize = node.direction == .horizontal ? contentWidth : contentHeight
|
|
@@ -485,7 +543,7 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
485
543
|
input.aspectRatio = finite(node.style.aspectRatio)
|
|
486
544
|
input.flexBasis = length(node.style.flexBasis, base: mainSize)
|
|
487
545
|
input.flexGrow = rootNode ? 0 : Float(max(0, node.style.flexGrow))
|
|
488
|
-
let defaultShrink: CGFloat = node.view is UIScrollView || (node.view is UILabel &&
|
|
546
|
+
let defaultShrink: CGFloat = node.view is UIScrollView || (node.view is UILabel && parentDirection == .horizontal) ? 1 : 0
|
|
489
547
|
input.flexShrink = rootNode ? 0 : Float(max(0, node.style.flexShrink ?? defaultShrink))
|
|
490
548
|
input.minWidth = length(node.style.minWidth, base: contentWidth)
|
|
491
549
|
input.maxWidth = length(node.style.maxWidth, base: contentWidth)
|
|
@@ -515,18 +573,18 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
515
573
|
var styles = [StyleInputV2](repeating: StyleInputV2(), count: nodes.count)
|
|
516
574
|
var metadata = [Float](repeating: 0, count: nodes.count * 2)
|
|
517
575
|
var cursor = 0
|
|
518
|
-
func write(_ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool) {
|
|
576
|
+
func write(_ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool, parentDirection: NSLayoutConstraint.Axis) {
|
|
519
577
|
let index = cursor
|
|
520
578
|
cursor += 1
|
|
521
|
-
put(&styles[index], node, parentWidth: parentWidth, parentHeight: parentHeight, rootNode: rootNode)
|
|
579
|
+
put(&styles[index], node, parentWidth: parentWidth, parentHeight: parentHeight, rootNode: rootNode, parentDirection: parentDirection)
|
|
522
580
|
let contentWidth = max(0, (rootNode ? width : node.intrinsic.width) - node.padding.left - node.padding.right)
|
|
523
581
|
let contentHeight = max(0, (rootNode ? height : node.intrinsic.height) - node.padding.top - node.padding.bottom)
|
|
524
582
|
let firstChild = cursor
|
|
525
|
-
node.children.forEach { write($0, parentWidth: contentWidth, parentHeight: contentHeight, rootNode: false) }
|
|
583
|
+
node.children.forEach { write($0, parentWidth: contentWidth, parentHeight: contentHeight, rootNode: false, parentDirection: node.direction) }
|
|
526
584
|
metadata[index * 2] = Float(firstChild)
|
|
527
585
|
metadata[index * 2 + 1] = Float(cursor - firstChild)
|
|
528
586
|
}
|
|
529
|
-
write(root, parentWidth: width, parentHeight: height, rootNode: true)
|
|
587
|
+
write(root, parentWidth: width, parentHeight: height, rootNode: true, parentDirection: root.direction)
|
|
530
588
|
let inputs = zip(styles, stride(from: 0, to: metadata.count, by: 2)).map { style, offset in
|
|
531
589
|
NodeInputV2(style: style, firstChild: UInt32(metadata[offset]), childCount: UInt32(metadata[offset + 1]))
|
|
532
590
|
}
|
|
@@ -549,6 +607,100 @@ private enum CraftNativeTreeLayoutBridge {
|
|
|
549
607
|
}) else { return nil }
|
|
550
608
|
return TreeResult(nodes: nodes, parents: parents, frames: output)
|
|
551
609
|
}
|
|
610
|
+
|
|
611
|
+
/// v4 is the reverse-aware tree payload with one resolved grid-auto-rows
|
|
612
|
+
/// value appended. Keeping this as a separate struct preserves the v1–v3
|
|
613
|
+
/// strides for runtimes that predate the shared auto-row contract.
|
|
614
|
+
private static func treeFramesV4(root: TreeNode, width: CGFloat, height: CGFloat, function: FunctionV2?) -> TreeResult? {
|
|
615
|
+
guard let function, width.isFinite, height.isFinite, width >= 0, height >= 0, !root.children.isEmpty else { return nil }
|
|
616
|
+
var nodes: [TreeNode] = []
|
|
617
|
+
var parents: [Int] = []
|
|
618
|
+
func append(_ node: TreeNode, parent: Int) {
|
|
619
|
+
let index = nodes.count
|
|
620
|
+
nodes.append(node)
|
|
621
|
+
parents.append(parent)
|
|
622
|
+
node.children.forEach { append($0, parent: index) }
|
|
623
|
+
}
|
|
624
|
+
append(root, parent: -1)
|
|
625
|
+
|
|
626
|
+
func put(_ input: inout StyleInputV4, _ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool, parentDirection: NSLayoutConstraint.Axis) {
|
|
627
|
+
let contentWidth = max(0, (rootNode ? width : node.intrinsic.width) - node.padding.left - node.padding.right)
|
|
628
|
+
let contentHeight = max(0, (rootNode ? height : node.intrinsic.height) - node.padding.top - node.padding.bottom)
|
|
629
|
+
let mainSize = node.direction == .horizontal ? contentWidth : contentHeight
|
|
630
|
+
input.direction = node.direction == .horizontal ? 0 : 1
|
|
631
|
+
input.width = rootNode ? Float(width) : length(node.style.width, base: parentWidth)
|
|
632
|
+
input.height = rootNode ? Float(height) : length(node.style.height, base: parentHeight)
|
|
633
|
+
input.intrinsicWidth = rootNode ? .nan : Float(max(0, node.intrinsic.width))
|
|
634
|
+
input.intrinsicHeight = rootNode ? .nan : Float(max(0, node.intrinsic.height))
|
|
635
|
+
input.baseline = rootNode ? .nan : Float(max(0, node.baseline))
|
|
636
|
+
input.aspectRatio = finite(node.style.aspectRatio)
|
|
637
|
+
input.flexBasis = length(node.style.flexBasis, base: mainSize)
|
|
638
|
+
input.flexGrow = rootNode ? 0 : Float(max(0, node.style.flexGrow))
|
|
639
|
+
let defaultShrink: CGFloat = node.view is UIScrollView || (node.view is UILabel && parentDirection == .horizontal) ? 1 : 0
|
|
640
|
+
input.flexShrink = rootNode ? 0 : Float(max(0, node.style.flexShrink ?? defaultShrink))
|
|
641
|
+
input.minWidth = length(node.style.minWidth, base: contentWidth)
|
|
642
|
+
input.maxWidth = length(node.style.maxWidth, base: contentWidth)
|
|
643
|
+
input.minHeight = length(node.style.minHeight, base: contentHeight)
|
|
644
|
+
input.maxHeight = length(node.style.maxHeight, base: contentHeight)
|
|
645
|
+
input.gap = Float(max(0, node.gap))
|
|
646
|
+
input.justifyContent = justify(node.justifyContent)
|
|
647
|
+
input.alignItems = alignment(node.alignItems)
|
|
648
|
+
input.alignSelf = rootNode ? 0 : alignSelf(node.style.alignSelf)
|
|
649
|
+
input.flexWrap = node.wrap ? 1 : 0
|
|
650
|
+
input.alignContent = craftNativeAlignContentCode(node.alignContent)
|
|
651
|
+
input.marginTop = Float(max(0, node.style.marginTop))
|
|
652
|
+
input.marginRight = Float(max(0, node.style.marginRight))
|
|
653
|
+
input.marginBottom = Float(max(0, node.style.marginBottom))
|
|
654
|
+
input.marginLeft = Float(max(0, node.style.marginLeft))
|
|
655
|
+
input.paddingTop = Float(max(0, node.padding.top))
|
|
656
|
+
input.paddingRight = Float(max(0, node.padding.right))
|
|
657
|
+
input.paddingBottom = Float(max(0, node.padding.bottom))
|
|
658
|
+
input.paddingLeft = Float(max(0, node.padding.left))
|
|
659
|
+
input.grid = node.grid ? 1 : 0
|
|
660
|
+
input.gridColumns = UInt32(max(1, node.gridColumns))
|
|
661
|
+
input.rowGap = Float(max(0, node.rowGap))
|
|
662
|
+
input.columnGap = Float(max(0, node.columnGap))
|
|
663
|
+
input.reverse = (node.reverse ? 1 : 0) | (node.wrapReverse ? 2 : 0)
|
|
664
|
+
input.gridAutoRows = node.gridAutoRows.map { length($0, base: contentHeight) } ?? .nan
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
var styles = [StyleInputV4](repeating: StyleInputV4(), count: nodes.count)
|
|
668
|
+
var metadata = [Float](repeating: 0, count: nodes.count * 2)
|
|
669
|
+
var cursor = 0
|
|
670
|
+
func write(_ node: TreeNode, parentWidth: CGFloat, parentHeight: CGFloat, rootNode: Bool, parentDirection: NSLayoutConstraint.Axis) {
|
|
671
|
+
let index = cursor
|
|
672
|
+
cursor += 1
|
|
673
|
+
put(&styles[index], node, parentWidth: parentWidth, parentHeight: parentHeight, rootNode: rootNode, parentDirection: parentDirection)
|
|
674
|
+
let contentWidth = max(0, (rootNode ? width : node.intrinsic.width) - node.padding.left - node.padding.right)
|
|
675
|
+
let contentHeight = max(0, (rootNode ? height : node.intrinsic.height) - node.padding.top - node.padding.bottom)
|
|
676
|
+
let firstChild = cursor
|
|
677
|
+
node.children.forEach { write($0, parentWidth: contentWidth, parentHeight: contentHeight, rootNode: false, parentDirection: node.direction) }
|
|
678
|
+
metadata[index * 2] = Float(firstChild)
|
|
679
|
+
metadata[index * 2 + 1] = Float(cursor - firstChild)
|
|
680
|
+
}
|
|
681
|
+
write(root, parentWidth: width, parentHeight: height, rootNode: true, parentDirection: root.direction)
|
|
682
|
+
let inputs = zip(styles, stride(from: 0, to: metadata.count, by: 2)).map { style, offset in
|
|
683
|
+
NodeInputV4(style: style, firstChild: UInt32(metadata[offset]), childCount: UInt32(metadata[offset + 1]))
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
var output = [Frame](repeating: Frame(x: 0, y: 0, width: 0, height: 0), count: nodes.count)
|
|
687
|
+
let frameCount = output.count
|
|
688
|
+
let status = inputs.withUnsafeBufferPointer { inputBuffer in
|
|
689
|
+
metadata.withUnsafeBufferPointer { metadataBuffer in
|
|
690
|
+
output.withUnsafeMutableBufferPointer { outputBuffer in
|
|
691
|
+
function(
|
|
692
|
+
Float(width), Float(height), UnsafeRawPointer(inputBuffer.baseAddress!), inputs.count,
|
|
693
|
+
UnsafeMutableRawPointer(outputBuffer.baseAddress!), frameCount
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
guard status == 0, output.allSatisfy({ frame in
|
|
699
|
+
frame.x.isFinite && frame.y.isFinite && frame.width.isFinite && frame.height.isFinite &&
|
|
700
|
+
frame.width >= 0 && frame.height >= 0
|
|
701
|
+
}) else { return nil }
|
|
702
|
+
return TreeResult(nodes: nodes, parents: parents, frames: output)
|
|
703
|
+
}
|
|
552
704
|
}
|
|
553
705
|
|
|
554
706
|
private enum CraftNativeFlexLineBridge {
|
|
@@ -1047,7 +1199,7 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1047
1199
|
}
|
|
1048
1200
|
} else if type == "mappolyline" || type == "polyline" {
|
|
1049
1201
|
guard points.count > 1 else { continue }
|
|
1050
|
-
let authoredWidth = (childProps["strokeWidth"]
|
|
1202
|
+
let authoredWidth = Self.numeric(childProps["strokeWidth"]) ?? 3
|
|
1051
1203
|
let width = CGFloat(authoredWidth)
|
|
1052
1204
|
guard authoredWidth.isFinite, width.isFinite, width > 0 else { continue }
|
|
1053
1205
|
nextPolylines[id] = (points, Self.color(childProps["strokeColor"] as? String), width)
|
|
@@ -1182,8 +1334,8 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1182
1334
|
}
|
|
1183
1335
|
|
|
1184
1336
|
private static func coordinates(_ value: Any?) -> [CLLocationCoordinate2D] {
|
|
1185
|
-
let numbers = (value as? [Any])?.compactMap
|
|
1186
|
-
?? (value as? [NSNumber])?.
|
|
1337
|
+
let numbers = (value as? [Any])?.compactMap(Self.numeric)
|
|
1338
|
+
?? (value as? [NSNumber])?.compactMap(Self.numeric)
|
|
1187
1339
|
?? []
|
|
1188
1340
|
return stride(from: 0, to: numbers.count - 1, by: 2).compactMap {
|
|
1189
1341
|
let latitude = numbers[$0]
|
|
@@ -1224,17 +1376,22 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1224
1376
|
guard let value = value as? [String: Any],
|
|
1225
1377
|
let center = value["center"] as? [String: Any],
|
|
1226
1378
|
let span = value["span"] as? [String: Any],
|
|
1227
|
-
let latitude = (center["latitude"]
|
|
1228
|
-
let longitude = (center["longitude"]
|
|
1229
|
-
let latitudeDelta = (span["latitudeDelta"]
|
|
1230
|
-
let longitudeDelta = (span["longitudeDelta"]
|
|
1231
|
-
latitude.isFinite, longitude.isFinite, latitudeDelta.isFinite, longitudeDelta.isFinite,
|
|
1379
|
+
let latitude = numeric(center["latitude"]),
|
|
1380
|
+
let longitude = numeric(center["longitude"]),
|
|
1381
|
+
let latitudeDelta = numeric(span["latitudeDelta"]) ?? numeric(span["latitude_delta"]),
|
|
1382
|
+
let longitudeDelta = numeric(span["longitudeDelta"]) ?? numeric(span["longitude_delta"]),
|
|
1232
1383
|
(-90...90).contains(latitude), (-180...180).contains(longitude),
|
|
1233
1384
|
latitudeDelta > 0, latitudeDelta <= 180, longitudeDelta > 0, longitudeDelta <= 360,
|
|
1234
1385
|
latitude - latitudeDelta / 2 >= -90, latitude + latitudeDelta / 2 <= 90 else { return nil }
|
|
1235
1386
|
return MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta))
|
|
1236
1387
|
}
|
|
1237
1388
|
|
|
1389
|
+
private static func numeric(_ value: Any?) -> Double? {
|
|
1390
|
+
guard !(value is Bool), let number = value as? NSNumber else { return nil }
|
|
1391
|
+
let value = number.doubleValue
|
|
1392
|
+
return value.isFinite ? value : nil
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1238
1395
|
private func nonEmptyHandler(_ handler: String?) -> String? {
|
|
1239
1396
|
guard let handler, !handler.isEmpty else { return nil }
|
|
1240
1397
|
return handler
|
|
@@ -1299,7 +1456,9 @@ private enum CraftNativeLength: Equatable {
|
|
|
1299
1456
|
case percent(CGFloat)
|
|
1300
1457
|
|
|
1301
1458
|
init?(_ value: Any?) {
|
|
1302
|
-
|
|
1459
|
+
// JavaScript booleans can bridge to NSNumber on Apple platforms;
|
|
1460
|
+
// they are invalid CSS lengths and must stay unset like Android.
|
|
1461
|
+
if !(value is Bool), let number = value as? NSNumber {
|
|
1303
1462
|
let points = CGFloat(truncating: number)
|
|
1304
1463
|
guard points.isFinite else { return nil }
|
|
1305
1464
|
self = .points(points)
|
|
@@ -1357,7 +1516,7 @@ private struct CraftNativeLayoutStyle {
|
|
|
1357
1516
|
|
|
1358
1517
|
init(_ raw: [String: Any]) {
|
|
1359
1518
|
func number(_ value: Any?) -> CGFloat? {
|
|
1360
|
-
guard let number = value as? NSNumber else { return nil }
|
|
1519
|
+
guard !(value is Bool), let number = value as? NSNumber else { return nil }
|
|
1361
1520
|
let value = CGFloat(truncating: number)
|
|
1362
1521
|
return value.isFinite ? value : nil
|
|
1363
1522
|
}
|
|
@@ -1977,11 +2136,10 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1977
2136
|
}
|
|
1978
2137
|
|
|
1979
2138
|
/// Builds the eligible portion of this container for the host-neutral
|
|
1980
|
-
/// tree ABI. The local algorithm remains authoritative for
|
|
1981
|
-
///
|
|
1982
|
-
///
|
|
2139
|
+
/// tree ABI. The local algorithm remains authoritative for absolute
|
|
2140
|
+
/// children and any subtree whose measurement cannot be represented by
|
|
2141
|
+
/// the shared contract.
|
|
1983
2142
|
fileprivate func sharedTreeNode(parentWidth: CGFloat, parentHeight: CGFloat) -> CraftNativeTreeLayoutBridge.TreeNode? {
|
|
1984
|
-
guard gridAutoRows == nil else { return nil }
|
|
1985
2143
|
let children = visibleChildren
|
|
1986
2144
|
let flow = children.filter { $0.craftLayoutStyle.position != "absolute" }
|
|
1987
2145
|
guard flow.count == children.count else { return nil }
|
|
@@ -2025,6 +2183,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2025
2183
|
padding: nested?.effectivePadding ?? .zero,
|
|
2026
2184
|
grid: nested?.grid ?? false,
|
|
2027
2185
|
gridColumns: nested?.gridColumns ?? 1,
|
|
2186
|
+
gridAutoRows: nested?.gridAutoRows,
|
|
2028
2187
|
reverse: nested?.reverse ?? false,
|
|
2029
2188
|
wrapReverse: nested?.wrapReverse ?? false,
|
|
2030
2189
|
children: nestedNode?.children ?? []
|
|
@@ -2047,6 +2206,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2047
2206
|
padding: effectivePadding,
|
|
2048
2207
|
grid: grid,
|
|
2049
2208
|
gridColumns: gridColumns,
|
|
2209
|
+
gridAutoRows: gridAutoRows,
|
|
2050
2210
|
reverse: reverse,
|
|
2051
2211
|
wrapReverse: wrapReverse,
|
|
2052
2212
|
children: nodes.compactMap { $0 }
|
|
@@ -2054,7 +2214,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2054
2214
|
}
|
|
2055
2215
|
|
|
2056
2216
|
private func sharedTreeFrames(_ children: [UIView], width: CGFloat?, height: CGFloat?) -> CraftNativeTreeLayoutBridge.TreeResult? {
|
|
2057
|
-
guard let width, let height,
|
|
2217
|
+
guard let width, let height,
|
|
2058
2218
|
width.isFinite, height.isFinite, width >= 0, height >= 0 else { return nil }
|
|
2059
2219
|
let nodes = children.map { child -> CraftNativeTreeLayoutBridge.TreeNode? in
|
|
2060
2220
|
let style = child.craftLayoutStyle
|
|
@@ -2078,6 +2238,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2078
2238
|
padding: nested?.effectivePadding ?? .zero,
|
|
2079
2239
|
grid: nested?.grid ?? false,
|
|
2080
2240
|
gridColumns: nested?.gridColumns ?? 1,
|
|
2241
|
+
gridAutoRows: nested?.gridAutoRows,
|
|
2081
2242
|
reverse: nested?.reverse ?? false,
|
|
2082
2243
|
wrapReverse: nested?.wrapReverse ?? false,
|
|
2083
2244
|
children: nestedNode?.children ?? []
|
|
@@ -2100,6 +2261,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2100
2261
|
padding: effectivePadding,
|
|
2101
2262
|
grid: grid,
|
|
2102
2263
|
gridColumns: gridColumns,
|
|
2264
|
+
gridAutoRows: gridAutoRows,
|
|
2103
2265
|
reverse: reverse,
|
|
2104
2266
|
wrapReverse: wrapReverse,
|
|
2105
2267
|
children: nodes.compactMap { $0 }
|
|
@@ -3072,7 +3234,7 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
3072
3234
|
/// these indices stay at the top of the visible area once scrolled to,
|
|
3073
3235
|
/// each pushed up by the next. Vertical scroll views only.
|
|
3074
3236
|
func setStickyHeaderIndices(_ value: Any?) {
|
|
3075
|
-
let next = Array(Set(((value as? [Any]) ?? []).compactMap { ($0
|
|
3237
|
+
let next = Array(Set(((value as? [Any]) ?? []).compactMap { number($0).map { Int($0) } }.filter { $0 >= 0 })).sorted()
|
|
3076
3238
|
guard next != stickyIndices else { return }
|
|
3077
3239
|
stickyIndices = next
|
|
3078
3240
|
setNeedsLayout()
|
|
@@ -3302,7 +3464,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3302
3464
|
var viewBox: CGRect? {
|
|
3303
3465
|
let props = drawing["props"] as? [String: Any] ?? [:]
|
|
3304
3466
|
guard let box = props["viewBox"] as? [Any], box.count == 4 else { return nil }
|
|
3305
|
-
let n = box.compactMap
|
|
3467
|
+
let n = box.compactMap(number)
|
|
3306
3468
|
guard n.count == 4, n.allSatisfy(\.isFinite), n[2] > 0, n[3] > 0 else { return nil }
|
|
3307
3469
|
return CGRect(x: n[0], y: n[1], width: n[2], height: n[3])
|
|
3308
3470
|
}
|
|
@@ -3339,9 +3501,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3339
3501
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
3340
3502
|
func value(_ key: String) -> Any? { style[key] ?? props[key] }
|
|
3341
3503
|
func finite(_ raw: Any?) -> CGFloat? {
|
|
3342
|
-
|
|
3343
|
-
let value = CGFloat(truncating: number)
|
|
3344
|
-
return value.isFinite ? value : nil
|
|
3504
|
+
return number(raw)
|
|
3345
3505
|
}
|
|
3346
3506
|
func string(_ raw: Any?) -> String? {
|
|
3347
3507
|
if let value = raw as? String { return value }
|
|
@@ -3500,7 +3660,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3500
3660
|
|
|
3501
3661
|
static func path(_ value: Any?) -> UIBezierPath? {
|
|
3502
3662
|
guard let list = value as? [Any] else { return nil }
|
|
3503
|
-
let n = list.compactMap
|
|
3663
|
+
let n = list.compactMap(number)
|
|
3504
3664
|
guard !n.isEmpty, n.count == list.count, n.allSatisfy(\.isFinite) else { return nil }
|
|
3505
3665
|
let path = UIBezierPath()
|
|
3506
3666
|
var i = 0
|
|
@@ -4330,6 +4490,74 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4330
4490
|
jsContext.setObject(snapshotClear, forKeyedSubscript: "craftNativeSnapshotClear" as NSString)
|
|
4331
4491
|
jsContext.setObject(secureGet, forKeyedSubscript: "craftNativeSecureGet" as NSString)
|
|
4332
4492
|
jsContext.setObject(setOptions, forKeyedSubscript: "craftNativeSetOptions" as NSString)
|
|
4493
|
+
// Older native bundles call the page-shaped `craft.dialog` API
|
|
4494
|
+
// directly. Keep that API backed by the same capability protocol as
|
|
4495
|
+
// current stx bundles, rather than letting their DOM fallback draw a
|
|
4496
|
+
// fake sheet in a JavaScriptCore screen.
|
|
4497
|
+
jsContext.evaluateScript("""
|
|
4498
|
+
(function() {
|
|
4499
|
+
var bridge = globalThis.__stxNativeBridge;
|
|
4500
|
+
if (!bridge || typeof bridge.postMessage !== 'function') return;
|
|
4501
|
+
var listeners = [];
|
|
4502
|
+
var runtimeCallback = null;
|
|
4503
|
+
bridge.onMessage = function(callback) {
|
|
4504
|
+
runtimeCallback = typeof callback === 'function' ? callback : null;
|
|
4505
|
+
};
|
|
4506
|
+
bridge.addMessageListener = function(callback) {
|
|
4507
|
+
if (typeof callback !== 'function') return function() {};
|
|
4508
|
+
listeners.push(callback);
|
|
4509
|
+
return function() {
|
|
4510
|
+
var index = listeners.indexOf(callback);
|
|
4511
|
+
if (index >= 0) listeners.splice(index, 1);
|
|
4512
|
+
};
|
|
4513
|
+
};
|
|
4514
|
+
globalThis.__stxNativeCallback = function(raw) {
|
|
4515
|
+
listeners.slice().forEach(function(callback) {
|
|
4516
|
+
try { callback(raw); } catch (error) {}
|
|
4517
|
+
});
|
|
4518
|
+
if (runtimeCallback) runtimeCallback(raw);
|
|
4519
|
+
};
|
|
4520
|
+
var nextRequest = 0;
|
|
4521
|
+
function request(method, args) {
|
|
4522
|
+
return new Promise(function(resolve, reject) {
|
|
4523
|
+
var id = 'craft-dialog-' + (++nextRequest);
|
|
4524
|
+
var remove = bridge.addMessageListener(function(raw) {
|
|
4525
|
+
var message;
|
|
4526
|
+
try { message = typeof raw === 'string' ? JSON.parse(raw) : raw; } catch (error) { return; }
|
|
4527
|
+
var payload = message && message.payload;
|
|
4528
|
+
if (!payload || payload.requestId !== id) return;
|
|
4529
|
+
remove();
|
|
4530
|
+
if (message.type === 'API_ERROR') {
|
|
4531
|
+
var failure = new Error(payload.message || 'Native dialog failed');
|
|
4532
|
+
failure.code = payload.code || 'NATIVE_CALL_FAILED';
|
|
4533
|
+
reject(failure);
|
|
4534
|
+
} else if (message.type === 'API_RESPONSE') {
|
|
4535
|
+
resolve(payload.data === undefined ? null : payload.data);
|
|
4536
|
+
}
|
|
4537
|
+
});
|
|
4538
|
+
bridge.postMessage(JSON.stringify({
|
|
4539
|
+
type: 'API_REQUEST',
|
|
4540
|
+
id: id,
|
|
4541
|
+
payload: {
|
|
4542
|
+
version: bridge.capabilityProtocolVersion || 1,
|
|
4543
|
+
module: 'Dialog',
|
|
4544
|
+
method: method,
|
|
4545
|
+
args: args,
|
|
4546
|
+
untimed: true
|
|
4547
|
+
}
|
|
4548
|
+
}));
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
var craft = globalThis.craft = globalThis.craft || {};
|
|
4552
|
+
if (Array.isArray(bridge.capabilities) && bridge.capabilities.indexOf('dialog') >= 0 && !craft.dialog) {
|
|
4553
|
+
craft.dialog = {
|
|
4554
|
+
alert: function(options) { return request('alert', [options || {}]).then(function() {}); },
|
|
4555
|
+
confirm: function(options) { return request('confirm', [options || {}]).then(Boolean); },
|
|
4556
|
+
actionSheet: function(options) { return request('actionSheet', [options || {}]).then(function(id) { return typeof id === 'string' ? id : null; }); }
|
|
4557
|
+
};
|
|
4558
|
+
}
|
|
4559
|
+
})();
|
|
4560
|
+
""")
|
|
4333
4561
|
// `AudioContext` and the rest of Web Audio, rendered natively.
|
|
4334
4562
|
audio.install(in: jsContext)
|
|
4335
4563
|
// `crypto` for code shared with the page (a sign-in's nonce and its
|
|
@@ -4772,7 +5000,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4772
5000
|
else if let symbol = spec["symbol"] as? String { button.setImage(UIImage(systemName: symbol, withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)), for: .normal) }
|
|
4773
5001
|
button.accessibilityIdentifier = spec["id"] as? String
|
|
4774
5002
|
button.accessibilityLabel = spec["accessibilityLabel"] as? String
|
|
4775
|
-
if let value = spec["badge"] as? NSNumber, value.intValue > 0 {
|
|
5003
|
+
if !(spec["badge"] is Bool), let value = spec["badge"] as? NSNumber, value.intValue > 0 {
|
|
4776
5004
|
let dot = spec["badgeStyle"] as? String == "dot"
|
|
4777
5005
|
let size: CGFloat = dot ? 9 : 18
|
|
4778
5006
|
// The fill is a tinted symbol, not a background colour: on iOS 26
|
|
@@ -5524,7 +5752,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5524
5752
|
result.isUserInteractionEnabled = false
|
|
5525
5753
|
case "Text":
|
|
5526
5754
|
let label = result as! UILabel
|
|
5527
|
-
label.numberOfLines = max(0, (props["numberOfLines"]
|
|
5755
|
+
label.numberOfLines = max(0, Int(number(props["numberOfLines"]) ?? 0))
|
|
5528
5756
|
label.lineBreakMode = textLineBreakMode(props["ellipsizeMode"])
|
|
5529
5757
|
(label as? CraftNativeLabel)?.insets = paddingInsets(style)
|
|
5530
5758
|
configureText(label, text: children.compactMap { $0 as? String }.joined(), style: style)
|
|
@@ -5598,7 +5826,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5598
5826
|
field.tintColor = color(props["selectionColor"])
|
|
5599
5827
|
let fieldId = ObjectIdentifier(field)
|
|
5600
5828
|
inputIdentities[fieldId] = current.identity
|
|
5601
|
-
|
|
5829
|
+
let maxLength = Int(number(props["maxLength"]) ?? 0)
|
|
5830
|
+
if maxLength > 0 {
|
|
5602
5831
|
textMaxLengths[fieldId] = maxLength
|
|
5603
5832
|
} else {
|
|
5604
5833
|
textMaxLengths.removeValue(forKey: fieldId)
|
|
@@ -5681,7 +5910,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5681
5910
|
if picker.preferredDatePickerStyle != display { picker.preferredDatePickerStyle = display }
|
|
5682
5911
|
}
|
|
5683
5912
|
// UIKit takes an interval that divides an hour, and ignores others.
|
|
5684
|
-
let interval = (props["minuteInterval"]
|
|
5913
|
+
let interval = Int(number(props["minuteInterval"]) ?? 1)
|
|
5685
5914
|
picker.minuteInterval = (1...30).contains(interval) && 60 % interval == 0 ? interval : 1
|
|
5686
5915
|
picker.minimumDate = Self.pickerDate(props["minimumDate"])
|
|
5687
5916
|
picker.maximumDate = Self.pickerDate(props["maximumDate"])
|
|
@@ -5737,7 +5966,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5737
5966
|
pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
|
|
5738
5967
|
case "Switch":
|
|
5739
5968
|
let toggle = result as! UISwitch
|
|
5740
|
-
|
|
5969
|
+
let value = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
|
|
5970
|
+
// UIKit has already changed the value before emitting the event.
|
|
5971
|
+
// Echoing it through the script must not restart its native animation.
|
|
5972
|
+
if toggle.isOn != value { toggle.isOn = value }
|
|
5741
5973
|
toggle.isEnabled = props["disabled"] as? Bool != true
|
|
5742
5974
|
if let track = props["trackColor"] as? [String: Any] {
|
|
5743
5975
|
toggle.onTintColor = color(track["true"])
|
|
@@ -5751,13 +5983,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5751
5983
|
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: toggle)
|
|
5752
5984
|
case "Slider":
|
|
5753
5985
|
let slider = result as! UISlider
|
|
5754
|
-
let minimum = (props["minimumValue"]
|
|
5986
|
+
let minimum = Float(number(props["minimumValue"]) ?? 0)
|
|
5755
5987
|
slider.minimumValue = minimum.isFinite ? minimum : 0
|
|
5756
|
-
let maximum = (props["maximumValue"]
|
|
5988
|
+
let maximum = Float(number(props["maximumValue"]) ?? 1)
|
|
5757
5989
|
slider.maximumValue = max(slider.minimumValue + .leastNonzeroMagnitude, maximum.isFinite ? maximum : 1)
|
|
5758
|
-
let authoredValue = (props["value"]
|
|
5990
|
+
let authoredValue = Float(number(props["value"]) ?? CGFloat(slider.minimumValue))
|
|
5759
5991
|
let value = authoredValue.isFinite ? authoredValue : slider.minimumValue
|
|
5760
|
-
if let step = (props["step"]
|
|
5992
|
+
if let step = number(props["step"]).map(Float.init), step.isFinite, step > 0 { sliderSteps[ObjectIdentifier(slider)] = step }
|
|
5761
5993
|
else { sliderSteps.removeValue(forKey: ObjectIdentifier(slider)) }
|
|
5762
5994
|
slider.value = snappedSliderValue(value, for: slider)
|
|
5763
5995
|
slider.minimumTrackTintColor = color(props["minimumTrackTintColor"])
|
|
@@ -5771,8 +6003,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5771
6003
|
let requestedSize = props["size"]
|
|
5772
6004
|
indicator.style = (requestedSize as? String) == "large" ? .large : .medium
|
|
5773
6005
|
let scale: CGFloat
|
|
5774
|
-
if let numericSize = requestedSize
|
|
5775
|
-
let value =
|
|
6006
|
+
if let numericSize = number(requestedSize) {
|
|
6007
|
+
let value = numericSize / 20.0
|
|
5776
6008
|
scale = value.isFinite ? max(0.5, value) : 1
|
|
5777
6009
|
} else if (requestedSize as? String) == "small" {
|
|
5778
6010
|
scale = 0.75
|
|
@@ -6230,7 +6462,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6230
6462
|
list.apply(
|
|
6231
6463
|
nodes: children,
|
|
6232
6464
|
horizontal: props["horizontal"] as? Bool == true,
|
|
6233
|
-
columns: (props["numColumns"]
|
|
6465
|
+
columns: Int(number(props["numColumns"]) ?? 1),
|
|
6234
6466
|
inverted: props["inverted"] as? Bool == true,
|
|
6235
6467
|
endReachedThreshold: threshold,
|
|
6236
6468
|
renderItem: { [weak self] node, identity, _ in
|
|
@@ -6591,12 +6823,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6591
6823
|
textView.textContainer.lineFragmentPadding = 0
|
|
6592
6824
|
}
|
|
6593
6825
|
textView.isScrollEnabled = false
|
|
6594
|
-
|
|
6826
|
+
let numberOfLines = Int(number(props["numberOfLines"]) ?? 0)
|
|
6827
|
+
if numberOfLines > 0 {
|
|
6595
6828
|
textView.textContainer.maximumNumberOfLines = numberOfLines
|
|
6596
6829
|
} else {
|
|
6597
6830
|
textView.textContainer.maximumNumberOfLines = 0
|
|
6598
6831
|
}
|
|
6599
|
-
|
|
6832
|
+
let maxLength = Int(number(props["maxLength"]) ?? 0)
|
|
6833
|
+
if maxLength > 0 {
|
|
6600
6834
|
textMaxLengths[id] = maxLength
|
|
6601
6835
|
} else {
|
|
6602
6836
|
textMaxLengths.removeValue(forKey: id)
|
|
@@ -7617,7 +7851,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7617
7851
|
}
|
|
7618
7852
|
|
|
7619
7853
|
private func number(_ value: Any?) -> CGFloat? {
|
|
7620
|
-
guard let number = value as? NSNumber else { return nil }
|
|
7854
|
+
guard !(value is Bool), let number = value as? NSNumber else { return nil }
|
|
7621
7855
|
let value = CGFloat(truncating: number)
|
|
7622
7856
|
return value.isFinite ? value : nil
|
|
7623
7857
|
}
|
|
@@ -1778,6 +1778,43 @@ test "v4 tree layout applies a resolved grid auto-row minimum" {
|
|
|
1778
1778
|
try std.testing.expectEqual(@as(f32, 20), frames[3].y);
|
|
1779
1779
|
}
|
|
1780
1780
|
|
|
1781
|
+
test "v4 tree layout carries auto rows through nested grids" {
|
|
1782
|
+
var root_style = emptyLayoutStyleInputV4();
|
|
1783
|
+
root_style.width = 100;
|
|
1784
|
+
root_style.height = 100;
|
|
1785
|
+
|
|
1786
|
+
var grid_style = emptyLayoutStyleInputV4();
|
|
1787
|
+
grid_style.width = 100;
|
|
1788
|
+
grid_style.height = 100;
|
|
1789
|
+
grid_style.grid = 1;
|
|
1790
|
+
grid_style.grid_columns = 2;
|
|
1791
|
+
grid_style.grid_auto_rows = 18;
|
|
1792
|
+
|
|
1793
|
+
var first_style = emptyLayoutStyleInputV4();
|
|
1794
|
+
first_style.intrinsic_width = 10;
|
|
1795
|
+
first_style.intrinsic_height = 4;
|
|
1796
|
+
var second_style = emptyLayoutStyleInputV4();
|
|
1797
|
+
second_style.intrinsic_width = 10;
|
|
1798
|
+
second_style.intrinsic_height = 6;
|
|
1799
|
+
var third_style = emptyLayoutStyleInputV4();
|
|
1800
|
+
third_style.intrinsic_width = 10;
|
|
1801
|
+
third_style.intrinsic_height = 5;
|
|
1802
|
+
|
|
1803
|
+
const nodes = [_]LayoutNodeInputV4{
|
|
1804
|
+
.{ .style = root_style, .first_child = 1, .child_count = 1 },
|
|
1805
|
+
.{ .style = grid_style, .first_child = 2, .child_count = 3 },
|
|
1806
|
+
.{ .style = first_style, .first_child = 0, .child_count = 0 },
|
|
1807
|
+
.{ .style = second_style, .first_child = 0, .child_count = 0 },
|
|
1808
|
+
.{ .style = third_style, .first_child = 0, .child_count = 0 },
|
|
1809
|
+
};
|
|
1810
|
+
var frames: [5]Frame = undefined;
|
|
1811
|
+
try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree_v4(100, 100, &nodes, nodes.len, &frames, frames.len));
|
|
1812
|
+
try std.testing.expectEqual(@as(f32, 0), frames[1].x);
|
|
1813
|
+
try std.testing.expectEqual(@as(f32, 0), frames[2].y);
|
|
1814
|
+
try std.testing.expectEqual(@as(f32, 0), frames[3].y);
|
|
1815
|
+
try std.testing.expectEqual(@as(f32, 18), frames[4].y);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1781
1818
|
test "lays out nested flat trees with absolute descendant frames" {
|
|
1782
1819
|
var root_style = emptyLayoutStyleInput();
|
|
1783
1820
|
root_style.width = 200;
|