craft-native 0.0.187 → 0.0.191
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/CraftNative.kt.template +34 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +92 -9
- package/dist/android/templates/CraftNativeShell.kt.template +58 -5
- package/dist/android/templates/MainActivityNative.kt.template +354 -34
- package/dist/browser/dialogs.d.ts +20 -0
- package/dist/browser/storage.d.ts +31 -0
- package/dist/browser.d.ts +22 -0
- package/dist/browser.js +705 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +43 -4
- package/dist/ios/templates/CraftHybrid.swift +36 -4
- package/dist/ios/templates/CraftNativeActions.swift +16 -4
- package/dist/ios/templates/CraftNativeFiles.swift +131 -25
- package/dist/ios/templates/CraftNativeScreen.swift +457 -3
- package/dist/macos/templates/native_layout.zig +299 -6
- package/package.json +9 -1
|
@@ -771,6 +771,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
771
771
|
private var gridAutoRows: CraftNativeLength? = null
|
|
772
772
|
private var reverse = false
|
|
773
773
|
private var wrapReverse = false
|
|
774
|
+
private var unboundedWidth = false
|
|
774
775
|
private var authoredPadding = intArrayOf(0, 0, 0, 0)
|
|
775
776
|
private var safeAreaPadding = intArrayOf(0, 0, 0, 0)
|
|
776
777
|
private var applyingPadding = false
|
|
@@ -947,6 +948,13 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
947
948
|
invalidateMeasurements()
|
|
948
949
|
}
|
|
949
950
|
|
|
951
|
+
/** A horizontal scroll host measures its content beyond the viewport. */
|
|
952
|
+
fun setUnboundedWidth(value: Boolean) {
|
|
953
|
+
if (unboundedWidth == value) return
|
|
954
|
+
unboundedWidth = value
|
|
955
|
+
invalidateMeasurements()
|
|
956
|
+
}
|
|
957
|
+
|
|
950
958
|
fun setChildStyle(view: View, style: JSONObject, density: Float) {
|
|
951
959
|
styles[view] = CraftNativeLayoutStyle.from(style, density)
|
|
952
960
|
invalidateMeasurements()
|
|
@@ -1237,15 +1245,197 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1237
1245
|
|
|
1238
1246
|
private fun sharedLength(value: CraftNativeLength?, base: Int?): Float = value?.resolve(base)?.toFloat() ?: Float.NaN
|
|
1239
1247
|
|
|
1248
|
+
/** A measured node in the host-neutral tree ABI, retaining its native view
|
|
1249
|
+
* so the returned absolute frames can be mounted back into the hierarchy. */
|
|
1250
|
+
private data class SharedTreeNode(
|
|
1251
|
+
val view: View?,
|
|
1252
|
+
val style: CraftNativeLayoutStyle,
|
|
1253
|
+
val intrinsicWidth: Float,
|
|
1254
|
+
val intrinsicHeight: Float,
|
|
1255
|
+
val baseline: Float,
|
|
1256
|
+
val direction: Int,
|
|
1257
|
+
val gap: Float,
|
|
1258
|
+
val rowGap: Float,
|
|
1259
|
+
val columnGap: Float,
|
|
1260
|
+
val wrap: Boolean,
|
|
1261
|
+
val justify: String,
|
|
1262
|
+
val alignItems: String,
|
|
1263
|
+
val alignContent: String,
|
|
1264
|
+
val padding: IntArray,
|
|
1265
|
+
val margins: IntArray,
|
|
1266
|
+
val grid: Boolean,
|
|
1267
|
+
val gridColumns: Int,
|
|
1268
|
+
val reverse: Boolean,
|
|
1269
|
+
val wrapReverse: Boolean,
|
|
1270
|
+
val children: List<SharedTreeNode>,
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
private data class SharedTreeResult(
|
|
1274
|
+
val nodes: List<SharedTreeNode>,
|
|
1275
|
+
val parents: IntArray,
|
|
1276
|
+
val frames: FloatArray,
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Flatten a flex or equal-track grid subtree for the shared ABI. The local
|
|
1281
|
+
* layout stays authoritative for grid-auto-rows,
|
|
1282
|
+
* absolute/modal children, and scroll hosts; those cases cannot be
|
|
1283
|
+
* represented without changing their native measurement contract.
|
|
1284
|
+
*/
|
|
1285
|
+
private fun sharedTreeChildren(width: Int, height: Int): List<Entry>? {
|
|
1286
|
+
if (gridAutoRows != null) return null
|
|
1287
|
+
if (grid) {
|
|
1288
|
+
val layout = gridLayout(width, height)
|
|
1289
|
+
if (layout.absolute.isNotEmpty()) return null
|
|
1290
|
+
return layout.rows.flatten()
|
|
1291
|
+
}
|
|
1292
|
+
val (lines, absolute) = entries(width, height)
|
|
1293
|
+
if (absolute.isNotEmpty()) return null
|
|
1294
|
+
return lines.flatMap { it.entries }
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private fun sharedTreeNode(entry: Entry): SharedTreeNode? {
|
|
1298
|
+
val nested = entry.view as? CraftNativeFlexLayout
|
|
1299
|
+
val children = if (nested == null) {
|
|
1300
|
+
emptyList()
|
|
1301
|
+
} else {
|
|
1302
|
+
nested.sharedTreeChildren(entry.width, entry.height) ?: return null
|
|
1303
|
+
}
|
|
1304
|
+
val lp = entry.view.layoutParams as? MarginLayoutParams
|
|
1305
|
+
return SharedTreeNode(
|
|
1306
|
+
view = entry.view,
|
|
1307
|
+
style = entry.style,
|
|
1308
|
+
intrinsicWidth = entry.width.toFloat(),
|
|
1309
|
+
intrinsicHeight = entry.height.toFloat(),
|
|
1310
|
+
baseline = baselineOffset(entry.view, entry.height).toFloat(),
|
|
1311
|
+
direction = nested?.let { if (it.orientation == HORIZONTAL) 0 else 1 } ?: 0,
|
|
1312
|
+
gap = nested?.let { if (it.orientation == HORIZONTAL) it.columnGap else it.rowGap } ?: 0f,
|
|
1313
|
+
rowGap = nested?.rowGap ?: 0f,
|
|
1314
|
+
columnGap = nested?.columnGap ?: 0f,
|
|
1315
|
+
wrap = nested?.wrap ?: false,
|
|
1316
|
+
justify = nested?.justifyContent ?: "flex-start",
|
|
1317
|
+
alignItems = nested?.alignItems ?: "stretch",
|
|
1318
|
+
alignContent = nested?.alignContent ?: "flex-start",
|
|
1319
|
+
padding = nested?.let { intArrayOf(it.paddingLeft, it.paddingRight, it.paddingTop, it.paddingBottom) }
|
|
1320
|
+
?: intArrayOf(0, 0, 0, 0),
|
|
1321
|
+
margins = intArrayOf(lp?.leftMargin ?: 0, lp?.rightMargin ?: 0, lp?.topMargin ?: 0, lp?.bottomMargin ?: 0),
|
|
1322
|
+
grid = nested?.grid ?: false,
|
|
1323
|
+
gridColumns = nested?.gridColumns ?: 1,
|
|
1324
|
+
reverse = nested?.reverse ?: false,
|
|
1325
|
+
wrapReverse = nested?.wrapReverse ?: false,
|
|
1326
|
+
children = children.map { child -> sharedTreeNode(child) ?: return null },
|
|
1327
|
+
)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private fun sharedTreeFrames(entries: List<Entry>, width: Int, height: Int): SharedTreeResult? {
|
|
1331
|
+
if (entries.isEmpty() || width < 0 || height < 0 || gridAutoRows != null) return null
|
|
1332
|
+
val children = entries.map { sharedTreeNode(it) ?: return null }
|
|
1333
|
+
val root = SharedTreeNode(
|
|
1334
|
+
view = null,
|
|
1335
|
+
style = CraftNativeLayoutStyle(),
|
|
1336
|
+
intrinsicWidth = 0f,
|
|
1337
|
+
intrinsicHeight = 0f,
|
|
1338
|
+
baseline = 0f,
|
|
1339
|
+
direction = if (orientation == HORIZONTAL) 0 else 1,
|
|
1340
|
+
gap = if (orientation == HORIZONTAL) columnGap else rowGap,
|
|
1341
|
+
rowGap = rowGap,
|
|
1342
|
+
columnGap = columnGap,
|
|
1343
|
+
wrap = wrap,
|
|
1344
|
+
justify = justifyContent,
|
|
1345
|
+
alignItems = alignItems,
|
|
1346
|
+
alignContent = alignContent,
|
|
1347
|
+
padding = intArrayOf(paddingLeft, paddingRight, paddingTop, paddingBottom),
|
|
1348
|
+
margins = intArrayOf(0, 0, 0, 0),
|
|
1349
|
+
grid = grid,
|
|
1350
|
+
gridColumns = gridColumns,
|
|
1351
|
+
reverse = reverse,
|
|
1352
|
+
wrapReverse = wrapReverse,
|
|
1353
|
+
children = children,
|
|
1354
|
+
)
|
|
1355
|
+
val nodes = mutableListOf<SharedTreeNode>()
|
|
1356
|
+
val parents = mutableListOf<Int>()
|
|
1357
|
+
fun append(node: SharedTreeNode, parent: Int) {
|
|
1358
|
+
val index = nodes.size
|
|
1359
|
+
nodes += node
|
|
1360
|
+
parents += parent
|
|
1361
|
+
node.children.forEach { child -> append(child, index) }
|
|
1362
|
+
}
|
|
1363
|
+
append(root, -1)
|
|
1364
|
+
val reverseAware = nodes.any { it.reverse || it.wrapReverse }
|
|
1365
|
+
val wrapReverseAware = nodes.any { it.wrapReverse }
|
|
1366
|
+
val fields = if (reverseAware) 33 else 32
|
|
1367
|
+
val nodeData = FloatArray(nodes.size * fields) { Float.NaN }
|
|
1368
|
+
val treeMeta = FloatArray(nodes.size * 2)
|
|
1369
|
+
fun enumBits(value: Int): Float = Float.fromBits(value)
|
|
1370
|
+
fun put(base: Int, index: Int, value: Float) { nodeData[base + index] = value }
|
|
1371
|
+
fun putEnum(base: Int, index: Int, value: Int) { put(base, index, enumBits(value)) }
|
|
1372
|
+
var cursor = 0
|
|
1373
|
+
fun write(node: SharedTreeNode, parentWidth: Int, parentHeight: Int, rootNode: Boolean) {
|
|
1374
|
+
val index = cursor++
|
|
1375
|
+
val base = index * fields
|
|
1376
|
+
val measuredWidth = if (rootNode) width else node.style.width?.resolve(parentWidth) ?: node.intrinsicWidth.roundToInt()
|
|
1377
|
+
val measuredHeight = if (rootNode) height else node.style.height?.resolve(parentHeight) ?: node.intrinsicHeight.roundToInt()
|
|
1378
|
+
val contentWidth = max(0, measuredWidth - node.padding[0] - node.padding[1])
|
|
1379
|
+
val contentHeight = max(0, measuredHeight - node.padding[2] - node.padding[3])
|
|
1380
|
+
val mainSize = if (node.direction == 0) contentWidth else contentHeight
|
|
1381
|
+
putEnum(base, 0, node.direction)
|
|
1382
|
+
put(base, 1, if (rootNode) width.toFloat() else sharedLength(node.style.width, parentWidth))
|
|
1383
|
+
put(base, 2, if (rootNode) height.toFloat() else sharedLength(node.style.height, parentHeight))
|
|
1384
|
+
put(base, 3, if (rootNode) Float.NaN else node.intrinsicWidth)
|
|
1385
|
+
put(base, 4, if (rootNode) Float.NaN else node.intrinsicHeight)
|
|
1386
|
+
put(base, 5, if (rootNode) Float.NaN else node.baseline)
|
|
1387
|
+
put(base, 6, node.style.aspectRatio?.toFloat() ?: Float.NaN)
|
|
1388
|
+
put(base, 7, sharedLength(node.style.flexBasis, mainSize))
|
|
1389
|
+
put(base, 8, if (rootNode) 0f else node.style.flexGrow.toFloat().coerceAtLeast(0f))
|
|
1390
|
+
put(base, 9, if (rootNode) 0f else (node.style.flexShrink ?: 0.0).toFloat().coerceAtLeast(0f))
|
|
1391
|
+
put(base, 10, sharedLength(node.style.minWidth, contentWidth))
|
|
1392
|
+
put(base, 11, sharedLength(node.style.maxWidth, contentWidth))
|
|
1393
|
+
put(base, 12, sharedLength(node.style.minHeight, contentHeight))
|
|
1394
|
+
put(base, 13, sharedLength(node.style.maxHeight, contentHeight))
|
|
1395
|
+
put(base, 14, node.gap)
|
|
1396
|
+
putEnum(base, 15, sharedJustifyCode(node.justify))
|
|
1397
|
+
putEnum(base, 16, sharedAlignItemsCode(node.alignItems))
|
|
1398
|
+
putEnum(base, 17, if (rootNode) 0 else sharedAlignSelfCode(node.style.alignSelf))
|
|
1399
|
+
putEnum(base, 18, if (node.wrap) 1 else 0)
|
|
1400
|
+
putEnum(base, 19, sharedAlignContentCode(node.alignContent))
|
|
1401
|
+
put(base, 20, node.margins[2].toFloat())
|
|
1402
|
+
put(base, 21, node.margins[1].toFloat())
|
|
1403
|
+
put(base, 22, node.margins[3].toFloat())
|
|
1404
|
+
put(base, 23, node.margins[0].toFloat())
|
|
1405
|
+
put(base, 24, node.padding[2].toFloat())
|
|
1406
|
+
put(base, 25, node.padding[1].toFloat())
|
|
1407
|
+
put(base, 26, node.padding[3].toFloat())
|
|
1408
|
+
put(base, 27, node.padding[0].toFloat())
|
|
1409
|
+
putEnum(base, 28, if (node.grid) 1 else 0)
|
|
1410
|
+
put(base, 29, node.gridColumns.toFloat())
|
|
1411
|
+
put(base, 30, node.rowGap)
|
|
1412
|
+
put(base, 31, node.columnGap)
|
|
1413
|
+
if (reverseAware) putEnum(base, 32, (if (node.reverse) 1 else 0) or (if (node.wrapReverse) 2 else 0))
|
|
1414
|
+
val firstChild = cursor
|
|
1415
|
+
node.children.forEach { child -> write(child, contentWidth, contentHeight, false) }
|
|
1416
|
+
treeMeta[index * 2] = firstChild.toFloat()
|
|
1417
|
+
treeMeta[index * 2 + 1] = (cursor - firstChild).toFloat()
|
|
1418
|
+
}
|
|
1419
|
+
write(root, width, height, true)
|
|
1420
|
+
val frames = (if (wrapReverseAware) {
|
|
1421
|
+
CraftNative.layoutTreeV3(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1422
|
+
} else if (reverseAware) {
|
|
1423
|
+
CraftNative.layoutTreeV2(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1424
|
+
} else {
|
|
1425
|
+
CraftNative.layoutTree(width.toFloat(), height.toFloat(), nodeData, treeMeta)
|
|
1426
|
+
}) ?: return null
|
|
1427
|
+
return SharedTreeResult(nodes, parents.toIntArray(), frames)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1240
1430
|
/**
|
|
1241
1431
|
* Use the host-neutral tree ABI when platform measurements are complete:
|
|
1242
|
-
* definite
|
|
1243
|
-
* independent row and column gaps.
|
|
1244
|
-
* missing runtimes, and malformed results
|
|
1245
|
-
* the established Kotlin layout below.
|
|
1432
|
+
* definite direct-child containers, including wrapped flows, equal-track
|
|
1433
|
+
* grids, and independent row and column gaps. Grid
|
|
1434
|
+
* auto-rows, absolute children, missing runtimes, and malformed results
|
|
1435
|
+
* deliberately fall through to the established Kotlin layout below.
|
|
1246
1436
|
*/
|
|
1247
1437
|
private fun sharedDirectFrames(entries: List<Entry>, width: Int, height: Int): FloatArray? {
|
|
1248
|
-
if (entries.isEmpty() || width < 0 || height < 0 || grid || reverse || wrapReverse) return null
|
|
1438
|
+
if (entries.isEmpty() || width < 0 || height < 0 || grid || reverse || wrapReverse || gridAutoRows != null) return null
|
|
1249
1439
|
val count = entries.size + 1
|
|
1250
1440
|
val fields = 32
|
|
1251
1441
|
val nodeData = FloatArray(count * fields) { Float.NaN }
|
|
@@ -1300,7 +1490,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1300
1490
|
put(base, 26, padding[3].toFloat())
|
|
1301
1491
|
put(base, 27, padding[0].toFloat())
|
|
1302
1492
|
putEnum(base, 28, 0)
|
|
1303
|
-
|
|
1493
|
+
put(base, 29, 1f)
|
|
1304
1494
|
put(base, 30, if (root) rowGapValue else 0f)
|
|
1305
1495
|
put(base, 31, if (root) columnGapValue else 0f)
|
|
1306
1496
|
}
|
|
@@ -1337,6 +1527,15 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1337
1527
|
else -> 0
|
|
1338
1528
|
}
|
|
1339
1529
|
|
|
1530
|
+
private fun sharedJustifyCode(value: String): Int = when (value) {
|
|
1531
|
+
"center" -> 1
|
|
1532
|
+
"flex-end" -> 2
|
|
1533
|
+
"space-between" -> 3
|
|
1534
|
+
"space-around" -> 4
|
|
1535
|
+
"space-evenly" -> 5
|
|
1536
|
+
else -> 0
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1340
1539
|
private fun alignContentCode(): Int = when (alignContent) {
|
|
1341
1540
|
"center" -> 1
|
|
1342
1541
|
"flex-end" -> 2
|
|
@@ -1347,6 +1546,16 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1347
1546
|
else -> 0
|
|
1348
1547
|
}
|
|
1349
1548
|
|
|
1549
|
+
private fun sharedAlignContentCode(value: String): Int = when (value) {
|
|
1550
|
+
"center" -> 1
|
|
1551
|
+
"flex-end" -> 2
|
|
1552
|
+
"space-between" -> 3
|
|
1553
|
+
"space-around" -> 4
|
|
1554
|
+
"stretch" -> 5
|
|
1555
|
+
"space-evenly" -> 6
|
|
1556
|
+
else -> 0
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1350
1559
|
/** Keep the old-runtime shim on the same bounded-flex water-fill rule as the Zig bridge. */
|
|
1351
1560
|
private fun fallbackFlexWidths(line: Line, available: Int, gap: Float): IntArray {
|
|
1352
1561
|
val horizontal = orientation == HORIZONTAL
|
|
@@ -1410,7 +1619,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1410
1619
|
}
|
|
1411
1620
|
|
|
1412
1621
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
1413
|
-
val
|
|
1622
|
+
val unboundedHorizontal = unboundedWidth && orientation == HORIZONTAL
|
|
1623
|
+
val widthMode = if (unboundedHorizontal) View.MeasureSpec.UNSPECIFIED else View.MeasureSpec.getMode(widthMeasureSpec)
|
|
1414
1624
|
widthDefinite = widthMode == View.MeasureSpec.EXACTLY
|
|
1415
1625
|
val width = View.MeasureSpec.getSize(widthMeasureSpec).takeUnless { widthMode == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
1416
1626
|
val height = View.MeasureSpec.getSize(heightMeasureSpec).takeUnless { View.MeasureSpec.getMode(heightMeasureSpec) == View.MeasureSpec.UNSPECIFIED } ?: 1_000_000
|
|
@@ -1437,7 +1647,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1437
1647
|
val absWidth = absolute.maxOfOrNull { it.width } ?: 0
|
|
1438
1648
|
val absHeight = absolute.maxOfOrNull { it.height } ?: 0
|
|
1439
1649
|
setMeasuredDimension(
|
|
1440
|
-
|
|
1650
|
+
if (unboundedHorizontal) max(desiredWidth + paddingLeft + paddingRight, absWidth + paddingLeft + paddingRight)
|
|
1651
|
+
else resolveSize(max(desiredWidth + paddingLeft + paddingRight, absWidth + paddingLeft + paddingRight), widthMeasureSpec),
|
|
1441
1652
|
resolveSize(max(desiredHeight + paddingTop + paddingBottom, absHeight + paddingTop + paddingBottom), heightMeasureSpec),
|
|
1442
1653
|
)
|
|
1443
1654
|
}
|
|
@@ -1547,6 +1758,23 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1547
1758
|
val (lines, absolute) = entries(width, height)
|
|
1548
1759
|
if (absolute.isEmpty() && lines.isNotEmpty() && (lines.size == 1 || wrap)) {
|
|
1549
1760
|
val sharedEntries = if (wrap) lines.flatMap { it.entries } else lines[0].entries
|
|
1761
|
+
val sharedTree = sharedTreeFrames(sharedEntries, width, height)
|
|
1762
|
+
if (sharedTree != null && (sharedTree.nodes.size > sharedEntries.size + 1 || grid)) {
|
|
1763
|
+
for (index in 1 until sharedTree.nodes.size) {
|
|
1764
|
+
val view = sharedTree.nodes[index].view ?: continue
|
|
1765
|
+
val offset = index * 4
|
|
1766
|
+
val parentOffset = sharedTree.parents[index] * 4
|
|
1767
|
+
val parentX = sharedTree.frames[parentOffset]
|
|
1768
|
+
val parentY = sharedTree.frames[parentOffset + 1]
|
|
1769
|
+
val x = (sharedTree.frames[offset] - parentX).roundToInt()
|
|
1770
|
+
val y = (sharedTree.frames[offset + 1] - parentY).roundToInt()
|
|
1771
|
+
val childWidth = sharedTree.frames[offset + 2].roundToInt()
|
|
1772
|
+
val childHeight = sharedTree.frames[offset + 3].roundToInt()
|
|
1773
|
+
place(view, x, y, x + childWidth, y + childHeight)
|
|
1774
|
+
}
|
|
1775
|
+
onLayoutChanged?.invoke()
|
|
1776
|
+
return
|
|
1777
|
+
}
|
|
1550
1778
|
val sharedFrames = sharedDirectFrames(sharedEntries, width, height)
|
|
1551
1779
|
if (sharedFrames != null) {
|
|
1552
1780
|
for ((index, entry) in sharedEntries.withIndex()) {
|
|
@@ -1808,10 +2036,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1808
2036
|
vertical.isFillViewport = true
|
|
1809
2037
|
horizontal.isFillViewport = true
|
|
1810
2038
|
setOnRefreshListener { refreshHandler?.invoke() }
|
|
1811
|
-
|
|
2039
|
+
// The outer refresh host must stay touch-enabled so its real scroll
|
|
2040
|
+
// child can receive gestures even when no pull-to-refresh handler is
|
|
2041
|
+
// configured. Interception is gated below instead.
|
|
2042
|
+
isEnabled = true
|
|
1812
2043
|
setHorizontal(false)
|
|
1813
2044
|
}
|
|
1814
2045
|
|
|
2046
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
2047
|
+
if (refreshHandler == null) return false
|
|
2048
|
+
return super.onInterceptTouchEvent(event)
|
|
2049
|
+
}
|
|
2050
|
+
|
|
1815
2051
|
/**
|
|
1816
2052
|
* Along an axis its parent does not fix, a scroll view is as big as its
|
|
1817
2053
|
* content (up to what the parent allows): a horizontal strip is as tall
|
|
@@ -1824,10 +2060,11 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1824
2060
|
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
|
1825
2061
|
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
|
1826
2062
|
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2063
|
+
// Even when the viewport itself is exact, a horizontal scroll must
|
|
2064
|
+
// measure its content with an unspecified width. Returning straight
|
|
2065
|
+
// to SwipeRefreshLayout here makes HorizontalScrollView remeasure the
|
|
2066
|
+
// content at the viewport width, erasing overflow and disabling the
|
|
2067
|
+
// nested carousel gesture.
|
|
1831
2068
|
val sideways = horizontalMode == true
|
|
1832
2069
|
val unspecified = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
1833
2070
|
val horizontalPadding = paddingLeft + paddingRight
|
|
@@ -1838,6 +2075,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1838
2075
|
if (!sideways || heightMode == MeasureSpec.UNSPECIFIED) unspecified
|
|
1839
2076
|
else MeasureSpec.makeMeasureSpec(max(0, heightSize - verticalPadding), MeasureSpec.AT_MOST),
|
|
1840
2077
|
)
|
|
2078
|
+
if (sideways) {
|
|
2079
|
+
// HorizontalScrollView may turn a WRAP_CONTENT child into an
|
|
2080
|
+
// AT_MOST viewport measurement during its own pass. Preserve the
|
|
2081
|
+
// intrinsic width we just measured as the child's layout width so
|
|
2082
|
+
// the second pass cannot collapse a carousel back to the viewport.
|
|
2083
|
+
val params = content.layoutParams ?: ViewGroup.LayoutParams(
|
|
2084
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
2085
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
2086
|
+
)
|
|
2087
|
+
params.width = max(params.width, content.measuredWidth)
|
|
2088
|
+
content.layoutParams = params
|
|
2089
|
+
}
|
|
1841
2090
|
fun fit(mode: Int, size: Int, wanted: Int): Int = when (mode) {
|
|
1842
2091
|
MeasureSpec.EXACTLY -> size
|
|
1843
2092
|
MeasureSpec.AT_MOST -> min(size, wanted)
|
|
@@ -1853,7 +2102,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1853
2102
|
|
|
1854
2103
|
fun setRefreshHandler(handler: (() -> Unit)?, refreshing: Boolean) {
|
|
1855
2104
|
refreshHandler = handler
|
|
1856
|
-
isEnabled =
|
|
2105
|
+
isEnabled = true
|
|
1857
2106
|
isRefreshing = handler != null && refreshing
|
|
1858
2107
|
if (handler == null) isRefreshing = false
|
|
1859
2108
|
}
|
|
@@ -1884,6 +2133,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
1884
2133
|
val scroller: ViewGroup = if (value) horizontal else vertical
|
|
1885
2134
|
scroller.removeAllViews()
|
|
1886
2135
|
content.orientation = if (value) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
|
2136
|
+
content.setUnboundedWidth(value)
|
|
1887
2137
|
scroller.addView(content, if (value)
|
|
1888
2138
|
ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
1889
2139
|
else ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
|
@@ -2403,6 +2653,37 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2403
2653
|
}
|
|
2404
2654
|
}
|
|
2405
2655
|
|
|
2656
|
+
/** A field's pending native edits and the props acknowledged by the screen. */
|
|
2657
|
+
internal class CraftNativeInputEcho {
|
|
2658
|
+
private val pending = ArrayDeque<String>()
|
|
2659
|
+
private var rendered: String? = null
|
|
2660
|
+
private var initialized = false
|
|
2661
|
+
|
|
2662
|
+
fun record(value: String) {
|
|
2663
|
+
pending.addLast(value)
|
|
2664
|
+
while (pending.size > 64) pending.removeFirst()
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
fun accept(value: String?, nativeValue: String): Boolean {
|
|
2668
|
+
if (value == null) {
|
|
2669
|
+
initialized = false
|
|
2670
|
+
rendered = null
|
|
2671
|
+
pending.clear()
|
|
2672
|
+
return false
|
|
2673
|
+
}
|
|
2674
|
+
// A retained field can render the same prop more than once, after
|
|
2675
|
+
// its echo was consumed. That repeated prop must not overwrite a
|
|
2676
|
+
// keystroke that has not made the native/JS round trip yet.
|
|
2677
|
+
val changed = !initialized || rendered != value
|
|
2678
|
+
initialized = true
|
|
2679
|
+
rendered = value
|
|
2680
|
+
if (!changed) return false
|
|
2681
|
+
val index = pending.indexOf(value)
|
|
2682
|
+
if (index >= 0) repeat(index + 1) { pending.removeFirst() }
|
|
2683
|
+
return nativeValue != value && index < 0
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2406
2687
|
/**
|
|
2407
2688
|
* A native screen's `<Video>`: the platform's VideoView.
|
|
2408
2689
|
*
|
|
@@ -3011,7 +3292,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3011
3292
|
* the screen answer with a value the field has already moved past;
|
|
3012
3293
|
* that echo is not written back over what was typed since.
|
|
3013
3294
|
*/
|
|
3014
|
-
val inputEchoes = mutableMapOf<EditText,
|
|
3295
|
+
val inputEchoes = mutableMapOf<EditText, CraftNativeInputEcho>()
|
|
3015
3296
|
val controls = mutableMapOf<String, View>()
|
|
3016
3297
|
// Protocol ids identify mutation targets; authored keys identify the
|
|
3017
3298
|
// retained view. Keep both mappings so a targeted update can recover
|
|
@@ -3272,6 +3553,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
3272
3553
|
bars.onTabSwipeStart = { dx, x, y -> beginTabSwipe(dx, x, y) }
|
|
3273
3554
|
bars.onTabSwipeMove = { dx -> updateTabSwipe(dx) }
|
|
3274
3555
|
bars.onTabSwipeEnd = { dx, cancelled -> finishTabSwipe(dx, cancelled) }
|
|
3556
|
+
bars.onTabSwipeShouldDefer = { x, y ->
|
|
3557
|
+
val location = IntArray(2)
|
|
3558
|
+
bars.getLocationOnScreen(location)
|
|
3559
|
+
val screen = visibleScreen
|
|
3560
|
+
screen != null && horizontalScrollAtPoint(
|
|
3561
|
+
screen.view,
|
|
3562
|
+
location[0] + x,
|
|
3563
|
+
location[1] + y,
|
|
3564
|
+
)
|
|
3565
|
+
}
|
|
3275
3566
|
root.addView(bars)
|
|
3276
3567
|
chrome = bars
|
|
3277
3568
|
tabStacks.clear()
|
|
@@ -3320,7 +3611,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
3320
3611
|
val target = ensureTabRoot(targetIndex) ?: return false
|
|
3321
3612
|
val host = chrome?.content ?: return false
|
|
3322
3613
|
if (target === current || host.width <= 0) return false
|
|
3323
|
-
|
|
3614
|
+
val chromeLocation = IntArray(2)
|
|
3615
|
+
chrome?.getLocationOnScreen(chromeLocation)
|
|
3616
|
+
if (horizontalScrollCanConsume(current.view, chromeLocation[0] + x, chromeLocation[1] + y, dx)) return false
|
|
3324
3617
|
(target.view.parent as? ViewGroup)?.removeView(target.view)
|
|
3325
3618
|
host.addView(target.view, FrameLayout.LayoutParams(-1, -1))
|
|
3326
3619
|
target.view.translationX = if (direction > 0) host.width.toFloat() else -host.width.toFloat()
|
|
@@ -3375,15 +3668,36 @@ class MainActivity : AppCompatActivity() {
|
|
|
3375
3668
|
}.start()
|
|
3376
3669
|
}
|
|
3377
3670
|
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3671
|
+
/**
|
|
3672
|
+
* Returns true when the exact screen gesture point is over a horizontal
|
|
3673
|
+
* child that still has room in the gesture's direction. Screen coordinates
|
|
3674
|
+
* keep nested scroll offsets and translated parents out of the calculation.
|
|
3675
|
+
*/
|
|
3676
|
+
private fun horizontalScrollCanConsume(view: View, screenX: Float, screenY: Float, dx: Float): Boolean {
|
|
3677
|
+
if (!view.isShown) return false
|
|
3678
|
+
val location = IntArray(2)
|
|
3679
|
+
view.getLocationOnScreen(location)
|
|
3680
|
+
if (screenX < location[0] || screenY < location[1]
|
|
3681
|
+
|| screenX > location[0] + view.width || screenY > location[1] + view.height) return false
|
|
3682
|
+
if (view is android.widget.HorizontalScrollView && view.canScrollHorizontally(if (dx < 0) 1 else -1)) return true
|
|
3683
|
+
if (view !is ViewGroup) return false
|
|
3684
|
+
for (index in view.childCount - 1 downTo 0) {
|
|
3685
|
+
if (horizontalScrollCanConsume(view.getChildAt(index), screenX, screenY, dx)) return true
|
|
3686
|
+
}
|
|
3687
|
+
return false
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
/** Returns true when a native horizontal scroll host owns the touch point. */
|
|
3691
|
+
private fun horizontalScrollAtPoint(view: View, screenX: Float, screenY: Float): Boolean {
|
|
3692
|
+
if (!view.isShown) return false
|
|
3693
|
+
val location = IntArray(2)
|
|
3694
|
+
view.getLocationOnScreen(location)
|
|
3695
|
+
if (screenX < location[0] || screenY < location[1]
|
|
3696
|
+
|| screenX > location[0] + view.width || screenY > location[1] + view.height) return false
|
|
3697
|
+
if (view is android.widget.HorizontalScrollView) return true
|
|
3698
|
+
if (view !is ViewGroup) return false
|
|
3382
3699
|
for (index in view.childCount - 1 downTo 0) {
|
|
3383
|
-
|
|
3384
|
-
if (!child.isShown || localX < child.left || localX > child.right || localY < child.top || localY > child.bottom) continue
|
|
3385
|
-
if (child is android.widget.HorizontalScrollView && child.canScrollHorizontally(if (dx < 0) 1 else -1)) return true
|
|
3386
|
-
if (horizontalScrollCanConsume(child, host, localX - child.left, localY - child.top, dx)) return true
|
|
3700
|
+
if (horizontalScrollAtPoint(view.getChildAt(index), screenX, screenY)) return true
|
|
3387
3701
|
}
|
|
3388
3702
|
return false
|
|
3389
3703
|
}
|
|
@@ -4318,10 +4632,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
4318
4632
|
val autofillHint = autofillHintFor(props)
|
|
4319
4633
|
if (autofillHint == null) setAutofillHints(*emptyArray()) else setAutofillHints(autofillHint)
|
|
4320
4634
|
val controlledValue = props.opt("value")?.takeUnless { it == JSONObject.NULL }?.toString()
|
|
4321
|
-
val echoes = screen.inputEchoes
|
|
4322
|
-
|
|
4323
|
-
if (echoIndex >= 0) repeat(echoIndex + 1) { echoes!!.removeFirst() }
|
|
4324
|
-
if (controlledValue != null && text.toString() != controlledValue && echoIndex < 0) {
|
|
4635
|
+
val echoes = screen.inputEchoes.getOrPut(this) { CraftNativeInputEcho() }
|
|
4636
|
+
if (echoes.accept(controlledValue, text.toString()) && controlledValue != null) {
|
|
4325
4637
|
screen.suppressedTextChanges.add(this)
|
|
4326
4638
|
setTextPreservingSelection(this, controlledValue)
|
|
4327
4639
|
}
|
|
@@ -4379,9 +4691,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4379
4691
|
(screen.inputIdentities[this@apply] ?: identity).let { screen.drafts[it] = updated }
|
|
4380
4692
|
if (screen.suppressedTextChanges.remove(this@apply)) return
|
|
4381
4693
|
val current = screen.handlers[this@apply] ?: return
|
|
4382
|
-
|
|
4383
|
-
echoes.addLast(updated)
|
|
4384
|
-
while (echoes.size > 64) echoes.removeFirst()
|
|
4694
|
+
screen.inputEchoes.getOrPut(this@apply) { CraftNativeInputEcho() }.record(updated)
|
|
4385
4695
|
send(screen, "EVENT", JSONObject()
|
|
4386
4696
|
.put("handlerName", current)
|
|
4387
4697
|
.put("nativeEvent", JSONObject().put("text", updated)))
|
|
@@ -4514,7 +4824,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4514
4824
|
)
|
|
4515
4825
|
} ?: run { trackTintList = null }
|
|
4516
4826
|
craftNativeOptionalString(props.opt("thumbColor"))
|
|
4517
|
-
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4827
|
+
?.let { thumbTintList = ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4518
4828
|
?: run { thumbTintList = null }
|
|
4519
4829
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
4520
4830
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -4751,6 +5061,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4751
5061
|
}
|
|
4752
5062
|
}
|
|
4753
5063
|
configureContainer(content, contentStyle)
|
|
5064
|
+
// Content-style reconciliation configures the flex host again;
|
|
5065
|
+
// restore the scroll axis after it so horizontal children are
|
|
5066
|
+
// measured beyond the viewport rather than clamped to it.
|
|
5067
|
+
content.orientation = if (horizontal) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
|
5068
|
+
content.setUnboundedWidth(horizontal)
|
|
4754
5069
|
val contentChildren = JSONArray()
|
|
4755
5070
|
for (index in 0 until children.length()) {
|
|
4756
5071
|
val child = children.opt(index)
|
|
@@ -5142,6 +5457,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
5142
5457
|
view.elevation = dp(craftNativeNonNegativeDouble(style, "elevation", 0.0)).toFloat()
|
|
5143
5458
|
view.z = craftNativeFiniteFloat(style, "zIndex", 0f)
|
|
5144
5459
|
val background = style.has("backgroundColor") && !style.isNull("backgroundColor")
|
|
5460
|
+
// VideoView's Surface is behind this window. An opaque View
|
|
5461
|
+
// background paints over the decoded picture (only rounded corners
|
|
5462
|
+
// showed it). Keep the outline/borders, with a transparent fill.
|
|
5463
|
+
val backgroundColor = if (view is CraftNativeVideoView) Color.TRANSPARENT
|
|
5464
|
+
else color(style.optString("backgroundColor"), Color.TRANSPARENT)
|
|
5145
5465
|
val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
|
|
5146
5466
|
val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
|
|
5147
5467
|
val sideBorderKeys = listOf("borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth")
|
|
@@ -5164,14 +5484,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
5164
5484
|
if (background || border > 0 || radius > 0 || hasSideBorders) {
|
|
5165
5485
|
if (hasSideBorders) {
|
|
5166
5486
|
view.background = CraftNativeSideBorderDrawable(
|
|
5167
|
-
|
|
5487
|
+
backgroundColor,
|
|
5168
5488
|
cornerRadii,
|
|
5169
5489
|
sideWidths,
|
|
5170
5490
|
sideColors,
|
|
5171
5491
|
)
|
|
5172
5492
|
} else {
|
|
5173
5493
|
view.background = GradientDrawable().apply {
|
|
5174
|
-
setColor(
|
|
5494
|
+
setColor(backgroundColor)
|
|
5175
5495
|
if (hasCornerRadii) setCornerRadii(cornerRadii) else cornerRadius = dp(radius).toFloat()
|
|
5176
5496
|
if (border > 0) {
|
|
5177
5497
|
val borderColor = color(style.optString("borderColor"), Color.TRANSPARENT)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface BrowserDialogOptions {
|
|
2
|
+
title?: string;
|
|
3
|
+
message?: string;
|
|
4
|
+
confirmLabel?: string;
|
|
5
|
+
cancelLabel?: string;
|
|
6
|
+
buttons?: Array<{
|
|
7
|
+
id: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
style?: string;
|
|
11
|
+
}>;
|
|
12
|
+
actions?: Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
style?: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
/** A user-owned browser dialog, with keyboard focus and cancellation, not a timed prompt. */
|
|
20
|
+
export declare function showBrowserDialog(kind: string, options: BrowserDialogOptions, signal: AbortSignal): Promise<string | boolean | undefined>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface BrowserStorageOptions {
|
|
2
|
+
storage: Storage;
|
|
3
|
+
namespace: string;
|
|
4
|
+
tokenKey: string;
|
|
5
|
+
/** Existing website identity keys to forget whenever its token changes. */
|
|
6
|
+
accountKeys?: string[];
|
|
7
|
+
hash: (value: string) => Promise<string>;
|
|
8
|
+
changed: (name: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface BrowserStorage {
|
|
11
|
+
craft: {
|
|
12
|
+
storage: {
|
|
13
|
+
getSync: (key: string) => unknown;
|
|
14
|
+
setSync: (key: string, value: unknown) => void;
|
|
15
|
+
};
|
|
16
|
+
secureStorage: {
|
|
17
|
+
getSync: (key: string) => string | null;
|
|
18
|
+
};
|
|
19
|
+
snapshots: {
|
|
20
|
+
get: (name: string) => unknown;
|
|
21
|
+
set: (name: string, value: unknown) => Promise<boolean>;
|
|
22
|
+
clear: (keep?: string[]) => Promise<boolean>;
|
|
23
|
+
keys: () => string[];
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
call: (module: string, method: string, args: unknown[]) => Promise<unknown>;
|
|
27
|
+
accountChanged: () => Promise<void>;
|
|
28
|
+
scope: () => string;
|
|
29
|
+
}
|
|
30
|
+
/** Browser persistence has browser security semantics; it does not claim to be a Keychain. */
|
|
31
|
+
export declare function createBrowserStorage(options: BrowserStorageOptions): Promise<BrowserStorage>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface BrowserPlatformOptions {
|
|
2
|
+
namespace?: string;
|
|
3
|
+
tokenKey?: string;
|
|
4
|
+
accountKeys?: string[];
|
|
5
|
+
/** App metadata shared with native hosts; no device capabilities are implied. */
|
|
6
|
+
appURL?: string;
|
|
7
|
+
locale?: string;
|
|
8
|
+
/** Application-owned deep-link schemes; arbitrary executable schemes are rejected. */
|
|
9
|
+
urlSchemes?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface BrowserPlatform {
|
|
12
|
+
craft: Record<string, unknown>;
|
|
13
|
+
capabilities: string[];
|
|
14
|
+
call: (module: string, method: string, args: unknown[], signal: AbortSignal) => Promise<unknown>;
|
|
15
|
+
navigate: (path: string, replace?: boolean) => void;
|
|
16
|
+
emit: (listener: (type: string, payload: unknown) => void) => () => void;
|
|
17
|
+
resolveMedia: (url: string) => Promise<string>;
|
|
18
|
+
resolveFile: (value: unknown) => unknown;
|
|
19
|
+
dispose: () => void;
|
|
20
|
+
}
|
|
21
|
+
/** Real browser counterparts for the portable Craft services. Device-only APIs remain absent. */
|
|
22
|
+
export declare function createBrowserPlatform(options?: BrowserPlatformOptions): Promise<BrowserPlatform>;
|