craft-native 0.0.186 → 0.0.189
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +14 -2
- package/dist/android/templates/MainActivityNative.kt.template +312 -24
- 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 +235 -114
- package/dist/index.cjs +178 -35
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +272 -130
- package/dist/ios/src/index.js +8 -3
- 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 +454 -3
- package/dist/macos/templates/native_layout.zig +299 -6
- package/dist/temp-dir.d.ts +65 -0
- 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))
|
|
@@ -3272,6 +3522,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
3272
3522
|
bars.onTabSwipeStart = { dx, x, y -> beginTabSwipe(dx, x, y) }
|
|
3273
3523
|
bars.onTabSwipeMove = { dx -> updateTabSwipe(dx) }
|
|
3274
3524
|
bars.onTabSwipeEnd = { dx, cancelled -> finishTabSwipe(dx, cancelled) }
|
|
3525
|
+
bars.onTabSwipeShouldDefer = { x, y ->
|
|
3526
|
+
val location = IntArray(2)
|
|
3527
|
+
bars.getLocationOnScreen(location)
|
|
3528
|
+
val screen = visibleScreen
|
|
3529
|
+
screen != null && horizontalScrollAtPoint(
|
|
3530
|
+
screen.view,
|
|
3531
|
+
location[0] + x,
|
|
3532
|
+
location[1] + y,
|
|
3533
|
+
)
|
|
3534
|
+
}
|
|
3275
3535
|
root.addView(bars)
|
|
3276
3536
|
chrome = bars
|
|
3277
3537
|
tabStacks.clear()
|
|
@@ -3320,7 +3580,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
3320
3580
|
val target = ensureTabRoot(targetIndex) ?: return false
|
|
3321
3581
|
val host = chrome?.content ?: return false
|
|
3322
3582
|
if (target === current || host.width <= 0) return false
|
|
3323
|
-
|
|
3583
|
+
val chromeLocation = IntArray(2)
|
|
3584
|
+
chrome?.getLocationOnScreen(chromeLocation)
|
|
3585
|
+
if (horizontalScrollCanConsume(current.view, chromeLocation[0] + x, chromeLocation[1] + y, dx)) return false
|
|
3324
3586
|
(target.view.parent as? ViewGroup)?.removeView(target.view)
|
|
3325
3587
|
host.addView(target.view, FrameLayout.LayoutParams(-1, -1))
|
|
3326
3588
|
target.view.translationX = if (direction > 0) host.width.toFloat() else -host.width.toFloat()
|
|
@@ -3375,15 +3637,36 @@ class MainActivity : AppCompatActivity() {
|
|
|
3375
3637
|
}.start()
|
|
3376
3638
|
}
|
|
3377
3639
|
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3640
|
+
/**
|
|
3641
|
+
* Returns true when the exact screen gesture point is over a horizontal
|
|
3642
|
+
* child that still has room in the gesture's direction. Screen coordinates
|
|
3643
|
+
* keep nested scroll offsets and translated parents out of the calculation.
|
|
3644
|
+
*/
|
|
3645
|
+
private fun horizontalScrollCanConsume(view: View, screenX: Float, screenY: Float, dx: Float): Boolean {
|
|
3646
|
+
if (!view.isShown) return false
|
|
3647
|
+
val location = IntArray(2)
|
|
3648
|
+
view.getLocationOnScreen(location)
|
|
3649
|
+
if (screenX < location[0] || screenY < location[1]
|
|
3650
|
+
|| screenX > location[0] + view.width || screenY > location[1] + view.height) return false
|
|
3651
|
+
if (view is android.widget.HorizontalScrollView && view.canScrollHorizontally(if (dx < 0) 1 else -1)) return true
|
|
3652
|
+
if (view !is ViewGroup) return false
|
|
3653
|
+
for (index in view.childCount - 1 downTo 0) {
|
|
3654
|
+
if (horizontalScrollCanConsume(view.getChildAt(index), screenX, screenY, dx)) return true
|
|
3655
|
+
}
|
|
3656
|
+
return false
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
/** Returns true when a native horizontal scroll host owns the touch point. */
|
|
3660
|
+
private fun horizontalScrollAtPoint(view: View, screenX: Float, screenY: Float): Boolean {
|
|
3661
|
+
if (!view.isShown) return false
|
|
3662
|
+
val location = IntArray(2)
|
|
3663
|
+
view.getLocationOnScreen(location)
|
|
3664
|
+
if (screenX < location[0] || screenY < location[1]
|
|
3665
|
+
|| screenX > location[0] + view.width || screenY > location[1] + view.height) return false
|
|
3666
|
+
if (view is android.widget.HorizontalScrollView) return true
|
|
3667
|
+
if (view !is ViewGroup) return false
|
|
3382
3668
|
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
|
|
3669
|
+
if (horizontalScrollAtPoint(view.getChildAt(index), screenX, screenY)) return true
|
|
3387
3670
|
}
|
|
3388
3671
|
return false
|
|
3389
3672
|
}
|
|
@@ -4514,7 +4797,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4514
4797
|
)
|
|
4515
4798
|
} ?: run { trackTintList = null }
|
|
4516
4799
|
craftNativeOptionalString(props.opt("thumbColor"))
|
|
4517
|
-
?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4800
|
+
?.let { thumbTintList = ColorStateList.valueOf(color(it, Color.WHITE)) }
|
|
4518
4801
|
?: run { thumbTintList = null }
|
|
4519
4802
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
4520
4803
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -4751,6 +5034,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4751
5034
|
}
|
|
4752
5035
|
}
|
|
4753
5036
|
configureContainer(content, contentStyle)
|
|
5037
|
+
// Content-style reconciliation configures the flex host again;
|
|
5038
|
+
// restore the scroll axis after it so horizontal children are
|
|
5039
|
+
// measured beyond the viewport rather than clamped to it.
|
|
5040
|
+
content.orientation = if (horizontal) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
|
|
5041
|
+
content.setUnboundedWidth(horizontal)
|
|
4754
5042
|
val contentChildren = JSONArray()
|
|
4755
5043
|
for (index in 0 until children.length()) {
|
|
4756
5044
|
val child = children.opt(index)
|
|
@@ -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>;
|