craft-native 0.0.500 → 0.0.502
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/src/index.d.ts +6 -0
- package/dist/android/src/index.js +2 -0
- package/dist/android/templates/CraftNativeFlatList.kt.template +8 -1
- package/dist/android/templates/CraftNativeShell.kt.template +61 -7
- package/dist/android/templates/MainActivityNative.kt.template +155 -46
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +113 -31
- package/package.json +1 -1
|
@@ -47,6 +47,12 @@ export interface CraftAndroidConfig {
|
|
|
47
47
|
renderer?: 'web' | 'native';
|
|
48
48
|
/** "system" follows the device; light/dark pin the generated host. */
|
|
49
49
|
appearance?: 'light' | 'dark' | 'system';
|
|
50
|
+
/**
|
|
51
|
+
* The native chrome's accent as #RRGGBB: Back, the bar's buttons and the
|
|
52
|
+
* selected tab. Omitted keeps the system blue, as before. iOS has had the
|
|
53
|
+
* same key; without it an app themed on iOS drew its Android chrome blue.
|
|
54
|
+
*/
|
|
55
|
+
tintColor?: string;
|
|
50
56
|
/** Static native tab-bar styling applied before the page's own tabBar.set call. */
|
|
51
57
|
tabBar?: CraftTabBarConfig;
|
|
52
58
|
/** Runs from the generated project root before Gradle compiles the app. */
|
|
@@ -1140,6 +1140,8 @@ function validateAndroidConfig(config) {
|
|
|
1140
1140
|
if (config.appearance !== undefined && !["light", "dark", "system"].includes(config.appearance)) {
|
|
1141
1141
|
throw new Error(`Unknown Android appearance: ${config.appearance}. Expected light, dark, or system.`);
|
|
1142
1142
|
}
|
|
1143
|
+
if (config.tintColor !== undefined && !/^#[0-9a-f]{6}$/i.test(config.tintColor))
|
|
1144
|
+
throw new Error("Android tintColor must be a #RRGGBB color");
|
|
1143
1145
|
const selection = config.tabBar?.selection;
|
|
1144
1146
|
if (selection?.cornerRadius !== undefined && (!Number.isFinite(selection.cornerRadius) || selection.cornerRadius < 0))
|
|
1145
1147
|
throw new Error("Android tabBar.selection.cornerRadius must be a finite non-negative number");
|
|
@@ -17,6 +17,9 @@ import kotlin.math.max
|
|
|
17
17
|
|
|
18
18
|
internal typealias CraftNativeRenderItem = (node: JSONObject, identity: String, previous: View?) -> View
|
|
19
19
|
|
|
20
|
+
/** The most columns a grid is given: more than any screen has pixels across to show. */
|
|
21
|
+
internal const val CRAFT_NATIVE_FLAT_LIST_MAX_COLUMNS = 1024
|
|
22
|
+
|
|
20
23
|
/** A stable-key RecyclerView host for stx native FlatList nodes. */
|
|
21
24
|
internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
22
25
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
@@ -403,7 +406,11 @@ internal class CraftNativeFlatList(context: Context) : RecyclerView(context) {
|
|
|
403
406
|
this.endReached = endReached
|
|
404
407
|
threshold = endReachedThreshold.takeIf { it.isFinite() }?.coerceAtLeast(0.0) ?: 0.1
|
|
405
408
|
|
|
406
|
-
|
|
409
|
+
// A grid keeps an array one longer than its column count, so a
|
|
410
|
+
// count from data (`numColumns={Number.MAX_VALUE}` arrives as the
|
|
411
|
+
// largest Int) asked it for a negative length and ended the process
|
|
412
|
+
// in layout. No screen is as many pixels wide as this many columns.
|
|
413
|
+
val nextColumns = columns.coerceIn(1, CRAFT_NATIVE_FLAT_LIST_MAX_COLUMNS)
|
|
407
414
|
val layoutChanged = this.horizontal != horizontal || this.columns != nextColumns
|
|
408
415
|
val retainedState = if (layoutChanged) null else layoutManager?.onSaveInstanceState()
|
|
409
416
|
this.horizontal = horizontal
|
|
@@ -187,6 +187,10 @@ internal class CraftNativeRoutes(config: JSONObject) {
|
|
|
187
187
|
}
|
|
188
188
|
?: CraftNativeTabBarAppearance()
|
|
189
189
|
|
|
190
|
+
/** The chrome's accent, where the app set one (`tintColor`); the system blue otherwise. */
|
|
191
|
+
val tintColor: Int? = config.optString("tintColor").takeUnless { config.isNull("tintColor") || it.isBlank() }
|
|
192
|
+
?.let { runCatching { Color.parseColor(it) }.getOrNull() }
|
|
193
|
+
|
|
190
194
|
/** CSS stacks for the chrome's labels and titles, as the app configured them. */
|
|
191
195
|
val fontFamily: String? = config.optString("fontFamily").takeUnless { config.isNull("fontFamily") || it.isBlank() }
|
|
192
196
|
val headingFontFamily: String? = config.optString("headingFontFamily").takeUnless { config.isNull("headingFontFamily") || it.isBlank() }
|
|
@@ -993,7 +997,7 @@ internal object CraftNativePrelude {
|
|
|
993
997
|
* screen sets its own chrome with
|
|
994
998
|
*
|
|
995
999
|
* `craft.navigation.setOptions({ title, largeTitle, titleColor, hidden,
|
|
996
|
-
* backgroundColor, leftButtons, rightButtons, tabItem, tabAccessory,
|
|
1000
|
+
* backgroundColor, backButton, leftButtons, rightButtons, tabItem, tabAccessory,
|
|
997
1001
|
* tabBarHidden, tabBarHiddenWhenPushed, tabSwipeEnabled })`
|
|
998
1002
|
*
|
|
999
1003
|
* which `apply` below reads. That list is checked against `apply` itself
|
|
@@ -1013,6 +1017,7 @@ internal class CraftNativeChrome(
|
|
|
1013
1017
|
private val tabs: List<CraftNativeTab>,
|
|
1014
1018
|
private val tabBarAppearance: CraftNativeTabBarAppearance = CraftNativeTabBarAppearance(),
|
|
1015
1019
|
private val fonts: CraftNativeChromeFonts = CraftNativeChromeFonts(),
|
|
1020
|
+
private val tintColor: Int? = null,
|
|
1016
1021
|
) : LinearLayout(context) {
|
|
1017
1022
|
val content = FrameLayout(context)
|
|
1018
1023
|
private val body = FrameLayout(context)
|
|
@@ -1249,6 +1254,8 @@ internal class CraftNativeChrome(
|
|
|
1249
1254
|
val title = options.optString("title").takeIf { options.has("title") } ?: fallbackTitle.orEmpty()
|
|
1250
1255
|
bar.visibility = if (hidden) View.GONE else View.VISIBLE
|
|
1251
1256
|
backButton.visibility = if (depth > 0) View.VISIBLE else View.GONE
|
|
1257
|
+
backButtonSpec = options.optJSONObject("backButton")
|
|
1258
|
+
applyBackButton()
|
|
1252
1259
|
bar.setPadding(if (depth > 0) dp(4) else dp(4), 0, dp(8), 0)
|
|
1253
1260
|
titleView.text = if (large) "" else title
|
|
1254
1261
|
largeTitleView.text = title
|
|
@@ -1368,14 +1375,22 @@ internal class CraftNativeChrome(
|
|
|
1368
1375
|
view.getLocationInWindow(at)
|
|
1369
1376
|
return (at[1] - origin[1] + view.height - top).coerceIn(0, reach)
|
|
1370
1377
|
}
|
|
1371
|
-
|
|
1378
|
+
// `inside` a scroll view that scrolls down the screen, a list or a
|
|
1379
|
+
// second scroll view is brought clear by that one, and gets none of
|
|
1380
|
+
// its own: where it sits in a page not yet scrolled to it says
|
|
1381
|
+
// nothing about the screen, and a short list below the fold was
|
|
1382
|
+
// given the whole reach and scrolled inside its box into blank space.
|
|
1383
|
+
fun visit(view: View, inside: Boolean) {
|
|
1372
1384
|
when (view) {
|
|
1373
|
-
is CraftNativeFlatList -> view.setFloatingCover(cover(view))
|
|
1374
|
-
is CraftNativeScrollView -> {
|
|
1375
|
-
|
|
1385
|
+
is CraftNativeFlatList -> view.setFloatingCover(if (inside) 0 else cover(view))
|
|
1386
|
+
is CraftNativeScrollView -> {
|
|
1387
|
+
view.setFloatingCover(if (inside) 0 else cover(view))
|
|
1388
|
+
visit(view.content, inside || view.scrollsVertically)
|
|
1389
|
+
}
|
|
1390
|
+
is ViewGroup -> for (index in 0 until view.childCount) visit(view.getChildAt(index), inside)
|
|
1376
1391
|
}
|
|
1377
1392
|
}
|
|
1378
|
-
visit(content)
|
|
1393
|
+
visit(content, false)
|
|
1379
1394
|
}
|
|
1380
1395
|
|
|
1381
1396
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
@@ -1497,6 +1512,42 @@ internal class CraftNativeChrome(
|
|
|
1497
1512
|
)
|
|
1498
1513
|
}
|
|
1499
1514
|
|
|
1515
|
+
private var backButtonSpec: JSONObject? = null
|
|
1516
|
+
private var renderedBackButton = ""
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* `backButton`: what this screen's Back is drawn with, when it is not the
|
|
1520
|
+
* arrow (`{ symbol: "xmark", accessibilityLabel: "Close" }` for a screen
|
|
1521
|
+
* that closes rather than goes back). `leftButtons` sit beside Back, so a
|
|
1522
|
+
* screen could not put its own symbol in Back's place (#527). It is still
|
|
1523
|
+
* the bar's back button: the same tap, shown at the same depths.
|
|
1524
|
+
*
|
|
1525
|
+
* The symbol is the app's own icon when it has one, and AppCompat's close
|
|
1526
|
+
* mark for an `xmark` it has none for. Any other symbol the app has no
|
|
1527
|
+
* icon for leaves the arrow, and its "Back".
|
|
1528
|
+
*/
|
|
1529
|
+
private fun applyBackButton() {
|
|
1530
|
+
val symbol = backButtonSpec?.optString("symbol").orEmpty()
|
|
1531
|
+
val label = backButtonSpec?.optString("accessibilityLabel").orEmpty()
|
|
1532
|
+
val tint = backButton.imageTintList?.defaultColor ?: Color.BLACK
|
|
1533
|
+
// The activity passes cumulative options for every live patch: the
|
|
1534
|
+
// same symbol in the same colour keeps the picture it has.
|
|
1535
|
+
val rendered = "$symbol\u0000$label\u0000$tint"
|
|
1536
|
+
if (rendered == renderedBackButton) return
|
|
1537
|
+
renderedBackButton = rendered
|
|
1538
|
+
val icon = if (symbol.isBlank()) null else CraftNativeIcons.drawable(context, null, symbol, tint)
|
|
1539
|
+
?: if (symbol.startsWith("xmark")) {
|
|
1540
|
+
androidx.appcompat.content.res.AppCompatResources.getDrawable(context, androidx.appcompat.R.drawable.abc_ic_clear_material)
|
|
1541
|
+
} else null
|
|
1542
|
+
if (icon == null) {
|
|
1543
|
+
backButton.setImageResource(androidx.appcompat.R.drawable.abc_ic_ab_back_material)
|
|
1544
|
+
backButton.contentDescription = "Back"
|
|
1545
|
+
} else {
|
|
1546
|
+
backButton.setImageDrawable(icon)
|
|
1547
|
+
backButton.contentDescription = label.ifBlank { if (symbol.startsWith("xmark")) "Close" else "Back" }
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1500
1551
|
/** Light or dark changed: the bars follow the screen's background. */
|
|
1501
1552
|
fun refreshColors() {
|
|
1502
1553
|
val background = screenBackgroundColor ?: ContextCompat.getColor(context, R.color.background)
|
|
@@ -1510,6 +1561,8 @@ internal class CraftNativeChrome(
|
|
|
1510
1561
|
titleView.setTextColor(navigationTitleColor ?: text)
|
|
1511
1562
|
largeTitleView.setTextColor(navigationTitleColor ?: text)
|
|
1512
1563
|
backButton.imageTintList = ColorStateList.valueOf(accent)
|
|
1564
|
+
// A screen's own back symbol is drawn in the accent, not tinted by it.
|
|
1565
|
+
applyBackButton()
|
|
1513
1566
|
val surface = if (dark) Color.parseColor("#1C1C1E") else Color.parseColor("#F9F9F9")
|
|
1514
1567
|
val grouped = accessoryGroup.parent === body
|
|
1515
1568
|
accessoryGroup.background = if (grouped) GradientDrawable().apply { setColor(surface); cornerRadius = dp(26).toFloat() } else null
|
|
@@ -1633,7 +1686,8 @@ internal class CraftNativeChrome(
|
|
|
1633
1686
|
}
|
|
1634
1687
|
}
|
|
1635
1688
|
|
|
1636
|
-
|
|
1689
|
+
/** Back, the bar's buttons and the selected tab: the app's own tint in both appearances, else the system blue. */
|
|
1690
|
+
private fun accentColor(dark: Boolean): Int = tintColor ?: if (dark) Color.parseColor("#0A84FF") else Color.parseColor("#007AFF")
|
|
1637
1691
|
|
|
1638
1692
|
private fun tabCellBackground(index: Int, dark: Boolean): android.graphics.drawable.Drawable? {
|
|
1639
1693
|
if (accessorySelected || index != selectedTab || (tabBarAppearance.selectionColor == null && tabBarAppearance.selectionCornerRadius == null)) return rippleBackground()
|
|
@@ -229,6 +229,21 @@ internal fun craftNativeFiniteFloat(style: JSONObject?, name: String, fallback:
|
|
|
229
229
|
return if (value.isFinite() && narrowed.isFinite()) narrowed else fallback
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The most a number may be on a view, either side of zero: 2^25, as on UIKit.
|
|
234
|
+
* It is far past any screen, and a view refuses anything past the largest
|
|
235
|
+
* Float: `setScaleX` and its like throw on infinity and on what is not a
|
|
236
|
+
* number, and a style is whatever the script said.
|
|
237
|
+
*/
|
|
238
|
+
internal const val CRAFT_NATIVE_LAYOUT_LIMIT = 33_554_432f
|
|
239
|
+
|
|
240
|
+
/** A number for a view: inside that limit, and 0 when it is not a number. */
|
|
241
|
+
internal fun craftNativeLayoutNumber(value: Float): Float =
|
|
242
|
+
if (value.isNaN()) 0f else value.coerceIn(-CRAFT_NATIVE_LAYOUT_LIMIT, CRAFT_NATIVE_LAYOUT_LIMIT)
|
|
243
|
+
|
|
244
|
+
internal fun craftNativeLayoutNumber(value: Double): Float =
|
|
245
|
+
if (value.isNaN()) 0f else value.coerceIn(-CRAFT_NATIVE_LAYOUT_LIMIT.toDouble(), CRAFT_NATIVE_LAYOUT_LIMIT.toDouble()).toFloat()
|
|
246
|
+
|
|
232
247
|
internal fun craftNativeFiniteDouble(style: JSONObject?, name: String, fallback: Double): Double {
|
|
233
248
|
val value = craftNativeNumber(style?.opt(name)) ?: Double.NaN
|
|
234
249
|
return value.takeIf { it.isFinite() } ?: fallback
|
|
@@ -355,29 +370,32 @@ internal fun craftNativeComponentLayoutStyle(node: JSONObject): JSONObject {
|
|
|
355
370
|
return merged
|
|
356
371
|
}
|
|
357
372
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return max(1, trimmed.split(Regex("\\s+")).count { it.isNotBlank() })
|
|
367
|
-
}
|
|
373
|
+
/**
|
|
374
|
+
* The most tracks a grid is given, either way. A count is whatever the script
|
|
375
|
+
* said (`gridTemplateRows: Number.MAX_VALUE`, `repeat(2147483647, 1fr)`), and
|
|
376
|
+
* the layout makes a list of rows and an array of columns that long: the
|
|
377
|
+
* process ran out of memory in measure. No screen has room to show more
|
|
378
|
+
* tracks than this.
|
|
379
|
+
*/
|
|
380
|
+
internal const val CRAFT_NATIVE_GRID_TRACK_LIMIT = 1024
|
|
368
381
|
|
|
369
|
-
|
|
370
|
-
if (value is Number) return
|
|
371
|
-
val
|
|
372
|
-
val trimmed = text.trim()
|
|
373
|
-
if (trimmed.isEmpty()) return null
|
|
382
|
+
internal fun craftNativeGridTrackCount(value: Any?): Int? {
|
|
383
|
+
if (value is Number) return value.toInt().coerceIn(1, CRAFT_NATIVE_GRID_TRACK_LIMIT)
|
|
384
|
+
val trimmed = (value as? String)?.trim()?.takeIf { it.isNotEmpty() } ?: return null
|
|
374
385
|
if (trimmed.startsWith("repeat(")) {
|
|
375
|
-
|
|
376
|
-
|
|
386
|
+
// Digits past what an Int holds are still a count, and still too many.
|
|
387
|
+
val digits = trimmed.removePrefix("repeat(").takeWhile { it.isDigit() }
|
|
388
|
+
if (digits.isNotEmpty()) {
|
|
389
|
+
return (digits.toIntOrNull() ?: CRAFT_NATIVE_GRID_TRACK_LIMIT).coerceIn(1, CRAFT_NATIVE_GRID_TRACK_LIMIT)
|
|
390
|
+
}
|
|
377
391
|
}
|
|
378
|
-
return
|
|
392
|
+
return trimmed.split(Regex("\\s+")).count { it.isNotBlank() }.coerceIn(1, CRAFT_NATIVE_GRID_TRACK_LIMIT)
|
|
379
393
|
}
|
|
380
394
|
|
|
395
|
+
private fun craftNativeGridColumnCount(value: Any?): Int = craftNativeGridTrackCount(value) ?: 1
|
|
396
|
+
|
|
397
|
+
private fun craftNativeGridRowCount(value: Any?): Int? = craftNativeGridTrackCount(value)
|
|
398
|
+
|
|
381
399
|
/**
|
|
382
400
|
* A CSS colour as Android's ARGB. STX writes `#RRGGBBAA` (and `#RGBA`) for a
|
|
383
401
|
* colour with alpha, where Android's parser reads `#AARRGGBB`: a translucent
|
|
@@ -412,6 +430,54 @@ internal fun craftNativeColor(raw: String): Int {
|
|
|
412
430
|
}
|
|
413
431
|
|
|
414
432
|
/** Android View has no skew setters before API 29; draw the shared skew around the view's center. */
|
|
433
|
+
/**
|
|
434
|
+
* Times a hold on a view whose touch listener answers every event itself.
|
|
435
|
+
*
|
|
436
|
+
* The platform times a long click in `View.onTouchEvent`, and a listener
|
|
437
|
+
* that consumes the touch keeps that from running. A pressable with
|
|
438
|
+
* `onPress` has such a listener (it sends the press, and the press-in and
|
|
439
|
+
* press-out around it), so its `onLongPress`, and the menu a `contextMenu`
|
|
440
|
+
* opens on a hold, were never reached by a finger: holding a row sent its
|
|
441
|
+
* press when the finger lifted (#529).
|
|
442
|
+
*
|
|
443
|
+
* The same rules as the platform's: the hold is up after its long-press
|
|
444
|
+
* timeout, a finger that moves past the touch slop is not holding, and a
|
|
445
|
+
* hold that something handled is not also a press.
|
|
446
|
+
*/
|
|
447
|
+
private object CraftNativePressHolds {
|
|
448
|
+
private class Hold(val fire: Runnable, val x: Float, val y: Float)
|
|
449
|
+
private val pending = WeakHashMap<View, Hold>()
|
|
450
|
+
private val handled = WeakHashMap<View, Boolean>()
|
|
451
|
+
|
|
452
|
+
fun began(view: View, event: MotionEvent) {
|
|
453
|
+
cancel(view)
|
|
454
|
+
handled.remove(view)
|
|
455
|
+
if (!view.isLongClickable) return
|
|
456
|
+
val fire = Runnable {
|
|
457
|
+
pending.remove(view)
|
|
458
|
+
if (view.isAttachedToWindow && view.isLongClickable && view.performLongClick()) handled[view] = true
|
|
459
|
+
}
|
|
460
|
+
pending[view] = Hold(fire, event.x, event.y)
|
|
461
|
+
view.postDelayed(fire, android.view.ViewConfiguration.getLongPressTimeout().toLong())
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fun moved(view: View, event: MotionEvent) {
|
|
465
|
+
val hold = pending[view] ?: return
|
|
466
|
+
val slop = android.view.ViewConfiguration.get(view.context).scaledTouchSlop
|
|
467
|
+
if (abs(event.x - hold.x) > slop || abs(event.y - hold.y) > slop) cancel(view)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** The touch is over. Whether its hold was handled, so that lifting is not a press. */
|
|
471
|
+
fun ended(view: View): Boolean {
|
|
472
|
+
cancel(view)
|
|
473
|
+
return handled.remove(view) == true
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private fun cancel(view: View) {
|
|
477
|
+
pending.remove(view)?.let { view.removeCallbacks(it.fire) }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
415
481
|
private object CraftNativeSkewTransforms {
|
|
416
482
|
private val values = WeakHashMap<View, Pair<Float, Float>>()
|
|
417
483
|
|
|
@@ -1344,7 +1410,19 @@ internal class CraftNativePointerImageView(context: Context) : ImageView(context
|
|
|
1344
1410
|
?: style.opt(name)?.takeUnless { it == JSONObject.NULL }
|
|
1345
1411
|
val fit = value("objectFit")
|
|
1346
1412
|
val position = value("objectPosition")
|
|
1347
|
-
|
|
1413
|
+
// A position does not choose a fit, as in CSS. STX compiles
|
|
1414
|
+
// `object-cover` to `resizeMode: "cover"` and sets no objectFit, so a
|
|
1415
|
+
// cover image given a position was drawn as "fill": stretched to its
|
|
1416
|
+
// box instead of cropped (#532).
|
|
1417
|
+
val requested = if (fit != null) fit as? String else when (
|
|
1418
|
+
craftNativeOptionalString(style.opt("resizeMode")) ?: craftNativeOptionalString(props.opt("resizeMode"))
|
|
1419
|
+
) {
|
|
1420
|
+
"cover" -> "cover"
|
|
1421
|
+
"contain" -> "contain"
|
|
1422
|
+
"center" -> "none"
|
|
1423
|
+
else -> "fill"
|
|
1424
|
+
}
|
|
1425
|
+
objectFit = if (fit != null || position != null) requested
|
|
1348
1426
|
?.takeIf { it in setOf("fill", "contain", "cover", "none", "scale-down") } ?: "fill" else null
|
|
1349
1427
|
val point = parsePosition(position)
|
|
1350
1428
|
objectX = point.first; objectY = point.second
|
|
@@ -3080,6 +3158,9 @@ internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(cont
|
|
|
3080
3158
|
vertical.setPadding(vertical.paddingLeft, vertical.paddingTop, vertical.paddingRight, next)
|
|
3081
3159
|
}
|
|
3082
3160
|
|
|
3161
|
+
/** Whether this scrolls down the screen: what is inside it is brought clear of the actions by it. */
|
|
3162
|
+
val scrollsVertically: Boolean get() = horizontalMode != true
|
|
3163
|
+
|
|
3083
3164
|
private var bounces = true
|
|
3084
3165
|
private var alwaysBounceVertical = true
|
|
3085
3166
|
private var alwaysBounceHorizontal = true
|
|
@@ -5345,7 +5426,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
5345
5426
|
private fun installChrome() {
|
|
5346
5427
|
val bodyFont = craftNativeChromeTypeface(routes.fontFamily, bundledFonts)
|
|
5347
5428
|
val bars = CraftNativeChrome(this, routes.tabs, routes.tabBarAppearance,
|
|
5348
|
-
CraftNativeChromeFonts(bodyFont, craftNativeChromeTypeface(routes.headingFontFamily, bundledFonts) ?: bodyFont)
|
|
5429
|
+
CraftNativeChromeFonts(bodyFont, craftNativeChromeTypeface(routes.headingFontFamily, bundledFonts) ?: bodyFont),
|
|
5430
|
+
routes.tintColor)
|
|
5349
5431
|
bars.layoutParams = FrameLayout.LayoutParams(-1, -1)
|
|
5350
5432
|
bars.onBack = { onBackPressedDispatcherCompat() }
|
|
5351
5433
|
bars.onAccessory = { path -> navigateOpen(path) }
|
|
@@ -7093,9 +7175,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
7093
7175
|
}
|
|
7094
7176
|
}
|
|
7095
7177
|
"Slider" -> (previous as? SeekBar ?: CraftNativePointerSeekBar(this)).apply {
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7178
|
+
// Held to a quarter of the largest Double, so the range, and
|
|
7179
|
+
// a value's distance along it, are numbers: at the largest
|
|
7180
|
+
// their difference is infinite, and infinity over infinity
|
|
7181
|
+
// is not a number, which `roundToInt` throws on and JSON
|
|
7182
|
+
// will not carry back to the screen.
|
|
7183
|
+
val bound = Double.MAX_VALUE / 4
|
|
7184
|
+
val minimum = craftNativeFiniteNumber(props, "minimumValue", 0.0).coerceIn(-bound, bound)
|
|
7185
|
+
val maximumCandidate = craftNativeFiniteNumber(props, "maximumValue", 1.0).coerceIn(-bound, bound)
|
|
7186
|
+
// The next number up, whatever the size of the first: adding
|
|
7187
|
+
// the smallest Double to anything but zero adds nothing.
|
|
7188
|
+
val maximum = if (maximumCandidate > minimum) maximumCandidate else Math.nextUp(minimum)
|
|
7099
7189
|
setOnSeekBarChangeListener(null)
|
|
7100
7190
|
val step = craftNativeFiniteNumber(props, "step", 0.0).takeIf { it > 0 }
|
|
7101
7191
|
// A stepped slider needs one native position per authored
|
|
@@ -7111,7 +7201,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7111
7201
|
.toInt()
|
|
7112
7202
|
} ?: 1000
|
|
7113
7203
|
max = progressMax
|
|
7114
|
-
val requested = craftNativeFiniteNumber(props, "value", minimum)
|
|
7204
|
+
val requested = craftNativeFiniteNumber(props, "value", minimum).coerceIn(minimum, maximum)
|
|
7115
7205
|
val value = (if (step != null) minimum + ((requested - minimum) / step).roundToInt() * step else requested)
|
|
7116
7206
|
.coerceIn(minimum, maximum)
|
|
7117
7207
|
progress = if (step != null) {
|
|
@@ -7159,7 +7249,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7159
7249
|
val scale = when (requestedSize) {
|
|
7160
7250
|
"large" -> 1.35f
|
|
7161
7251
|
"small" -> 0.75f
|
|
7162
|
-
is Number -> (requestedSize.toDouble() / 24.0).takeIf { it.isFinite() }?.
|
|
7252
|
+
is Number -> (requestedSize.toDouble() / 24.0).takeIf { it.isFinite() }?.let { craftNativeLayoutNumber(it).coerceAtLeast(0.5f) } ?: 1f
|
|
7163
7253
|
else -> 1f
|
|
7164
7254
|
}
|
|
7165
7255
|
indicatorScale = scale
|
|
@@ -7441,9 +7531,17 @@ class MainActivity : AppCompatActivity() {
|
|
|
7441
7531
|
screen.pressUnderlayShowHandlers[view]?.let { current ->
|
|
7442
7532
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
7443
7533
|
}
|
|
7534
|
+
// This listener answers every event, so the
|
|
7535
|
+
// platform never times the hold: do it here.
|
|
7536
|
+
CraftNativePressHolds.began(view, event)
|
|
7537
|
+
true
|
|
7538
|
+
}
|
|
7539
|
+
android.view.MotionEvent.ACTION_MOVE -> {
|
|
7540
|
+
CraftNativePressHolds.moved(view, event)
|
|
7444
7541
|
true
|
|
7445
7542
|
}
|
|
7446
7543
|
android.view.MotionEvent.ACTION_UP, android.view.MotionEvent.ACTION_CANCEL -> {
|
|
7544
|
+
val held = CraftNativePressHolds.ended(view)
|
|
7447
7545
|
view.alpha = screen.pressBaseOpacities[view] ?: 1f
|
|
7448
7546
|
if (screen.pressBaseBackgrounds.containsKey(view)) view.background = screen.pressBaseBackgrounds[view]
|
|
7449
7547
|
screen.pressOutHandlers[view]?.let { current ->
|
|
@@ -7452,7 +7550,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
7452
7550
|
screen.pressUnderlayHideHandlers[view]?.let { current ->
|
|
7453
7551
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
7454
7552
|
}
|
|
7455
|
-
|
|
7553
|
+
// A hold that sent its long press, or opened
|
|
7554
|
+
// its menu, is not a press as well.
|
|
7555
|
+
if (event.actionMasked == android.view.MotionEvent.ACTION_UP && !held) {
|
|
7456
7556
|
dispatchNativePress(screen, view)
|
|
7457
7557
|
}
|
|
7458
7558
|
true
|
|
@@ -7964,37 +8064,46 @@ class MainActivity : AppCompatActivity() {
|
|
|
7964
8064
|
}
|
|
7965
8065
|
|
|
7966
8066
|
private fun applyTransform(view: View, transforms: JSONArray?) {
|
|
7967
|
-
view.translationX = 0f
|
|
7968
|
-
view.translationY = 0f
|
|
7969
8067
|
// Reconciliation reapplies transforms; start from the control's
|
|
7970
8068
|
// intrinsic scale rather than erasing its authored indicator size.
|
|
7971
8069
|
val intrinsicScale = (view as? CraftNativeActivityIndicator)?.indicatorScale ?: 1f
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
8070
|
+
var translationX = 0f
|
|
8071
|
+
var translationY = 0f
|
|
8072
|
+
var scaleX = intrinsicScale
|
|
8073
|
+
var scaleY = intrinsicScale
|
|
8074
|
+
var rotation = 0f
|
|
8075
|
+
var rotationX = 0f
|
|
8076
|
+
var rotationY = 0f
|
|
7979
8077
|
var skewX = 0f
|
|
7980
8078
|
var skewY = 0f
|
|
7981
|
-
for (index in 0 until transforms
|
|
7982
|
-
val transform = transforms
|
|
8079
|
+
for (index in 0 until (transforms?.length() ?: 0)) {
|
|
8080
|
+
val transform = transforms?.optJSONObject(index) ?: continue
|
|
7983
8081
|
val key = transform.keys().asSequence().firstOrNull() ?: continue
|
|
7984
8082
|
when (key) {
|
|
7985
|
-
"translateX" ->
|
|
7986
|
-
"translateY" ->
|
|
7987
|
-
"scale" -> craftNativeFiniteDouble(transform, key, 1.0).toFloat().let {
|
|
7988
|
-
"scaleX" ->
|
|
7989
|
-
"scaleY" ->
|
|
7990
|
-
"rotate", "rotateZ" ->
|
|
7991
|
-
"rotateX" ->
|
|
7992
|
-
"rotateY" ->
|
|
8083
|
+
"translateX" -> translationX += dp(craftNativeFiniteDouble(transform, key, 0.0)).toFloat()
|
|
8084
|
+
"translateY" -> translationY += dp(craftNativeFiniteDouble(transform, key, 0.0)).toFloat()
|
|
8085
|
+
"scale" -> craftNativeFiniteDouble(transform, key, 1.0).toFloat().let { scaleX *= it; scaleY *= it }
|
|
8086
|
+
"scaleX" -> scaleX *= craftNativeFiniteDouble(transform, key, 1.0).toFloat()
|
|
8087
|
+
"scaleY" -> scaleY *= craftNativeFiniteDouble(transform, key, 1.0).toFloat()
|
|
8088
|
+
"rotate", "rotateZ" -> rotation += angleDegrees(transform.opt(key))
|
|
8089
|
+
"rotateX" -> rotationX += angleDegrees(transform.opt(key))
|
|
8090
|
+
"rotateY" -> rotationY += angleDegrees(transform.opt(key))
|
|
7993
8091
|
"skewX" -> skewX += skewFactor(transform.opt(key))
|
|
7994
8092
|
"skewY" -> skewY += skewFactor(transform.opt(key))
|
|
7995
8093
|
}
|
|
7996
8094
|
}
|
|
7997
|
-
|
|
8095
|
+
// Summed and multiplied first, and bounded once: a view throws on a
|
|
8096
|
+
// scale or an offset that is infinite or not a number, and two finite
|
|
8097
|
+
// factors can make either.
|
|
8098
|
+
view.translationX = craftNativeLayoutNumber(translationX)
|
|
8099
|
+
view.translationY = craftNativeLayoutNumber(translationY)
|
|
8100
|
+
view.scaleX = craftNativeLayoutNumber(scaleX)
|
|
8101
|
+
view.scaleY = craftNativeLayoutNumber(scaleY)
|
|
8102
|
+
view.rotation = craftNativeLayoutNumber(rotation)
|
|
8103
|
+
view.rotationX = craftNativeLayoutNumber(rotationX)
|
|
8104
|
+
view.rotationY = craftNativeLayoutNumber(rotationY)
|
|
8105
|
+
CraftNativeSkewTransforms.clear(view)
|
|
8106
|
+
if (transforms != null) CraftNativeSkewTransforms.set(view, craftNativeLayoutNumber(skewX), craftNativeLayoutNumber(skewY))
|
|
7998
8107
|
}
|
|
7999
8108
|
|
|
8000
8109
|
private fun skewFactor(value: Any?): Float =
|
package/dist/cli.js
CHANGED
|
@@ -3266,7 +3266,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3266
3266
|
`);
|
|
3267
3267
|
}
|
|
3268
3268
|
// package.json
|
|
3269
|
-
var version = "0.0.
|
|
3269
|
+
var version = "0.0.502";
|
|
3270
3270
|
|
|
3271
3271
|
// bin/cli.ts
|
|
3272
3272
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -214,31 +214,34 @@ private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
|
214
214
|
return nil
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
/// The most tracks a grid is given, either way. A count is whatever the
|
|
218
|
+
/// script said (`gridTemplateRows: Number.MAX_VALUE`,
|
|
219
|
+
/// `repeat(4294967296, 1fr)`), and the layout makes an array of rows that
|
|
220
|
+
/// long and hands the shared engine both counts as a `UInt32`: the first ran
|
|
221
|
+
/// the process out of memory and the second trapped. No screen has room to
|
|
222
|
+
/// show more tracks than this.
|
|
223
|
+
let craftNativeGridTrackLimit = 1024
|
|
224
|
+
|
|
225
|
+
func craftNativeGridTrackCount(_ value: Any?) -> Int? {
|
|
226
|
+
if let number = craftNativeNumber(value) {
|
|
227
|
+
return min(craftNativeGridTrackLimit, max(1, craftNativeInt(number.doubleValue)))
|
|
224
228
|
}
|
|
225
|
-
let tracks = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" || $0 == "\n" })
|
|
226
|
-
return max(1, tracks.count)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private func craftNativeGridRowCount(_ value: Any?) -> Int? {
|
|
230
|
-
if let number = craftNativeNumber(value) { return max(1, number.intValue) }
|
|
231
229
|
guard let text = value as? String else { return nil }
|
|
232
230
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
233
231
|
guard !trimmed.isEmpty else { return nil }
|
|
234
232
|
if trimmed.hasPrefix("repeat(") {
|
|
235
|
-
let digits = trimmed.dropFirst("repeat(".count).prefix { $0
|
|
236
|
-
|
|
233
|
+
let digits = trimmed.dropFirst("repeat(".count).prefix { ("0"..."9").contains($0) }
|
|
234
|
+
// Digits past what an Int holds are still a count, and still too many.
|
|
235
|
+
if !digits.isEmpty { return min(craftNativeGridTrackLimit, max(1, Int(digits) ?? craftNativeGridTrackLimit)) }
|
|
237
236
|
}
|
|
238
237
|
let tracks = trimmed.split(whereSeparator: { $0 == " " || $0 == "\t" || $0 == "\n" })
|
|
239
|
-
return tracks.isEmpty ? nil :
|
|
238
|
+
return tracks.isEmpty ? nil : min(craftNativeGridTrackLimit, tracks.count)
|
|
240
239
|
}
|
|
241
240
|
|
|
241
|
+
private func craftNativeGridColumnCount(_ value: Any?) -> Int { craftNativeGridTrackCount(value) ?? 1 }
|
|
242
|
+
|
|
243
|
+
private func craftNativeGridRowCount(_ value: Any?) -> Int? { craftNativeGridTrackCount(value) }
|
|
244
|
+
|
|
242
245
|
/// Optional bridge to the shared Zig flex rule. Native hosts retain their
|
|
243
246
|
/// local implementation when no Craft runtime is linked (the normal SDK
|
|
244
247
|
/// fallback), while runtime-backed apps get one cross-platform calculation.
|
|
@@ -4376,7 +4379,11 @@ class CraftNativePositionedImageView: UIView {
|
|
|
4376
4379
|
}
|
|
4377
4380
|
let rawFit = value("objectFit"), rawPosition = value("objectPosition")
|
|
4378
4381
|
if rawFit != nil || rawPosition != nil {
|
|
4379
|
-
|
|
4382
|
+
// A position does not choose a fit, as in CSS. STX compiles
|
|
4383
|
+
// `object-cover` to `resizeMode: "cover"` and sets no objectFit,
|
|
4384
|
+
// so a cover image given a position was drawn as "fill":
|
|
4385
|
+
// stretched to its box instead of cropped (#532).
|
|
4386
|
+
let requested = rawFit as? String ?? Self.objectFit(resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String)
|
|
4380
4387
|
fit = ["fill", "contain", "cover", "none", "scale-down"].contains(requested) ? requested : "fill"
|
|
4381
4388
|
position = Self.objectPosition(rawPosition)
|
|
4382
4389
|
} else { fit = nil }
|
|
@@ -4404,6 +4411,17 @@ class CraftNativePositionedImageView: UIView {
|
|
|
4404
4411
|
var usesObjectGeometry: Bool { fit != nil }
|
|
4405
4412
|
var objectCovers: Bool { fit == "cover" }
|
|
4406
4413
|
|
|
4414
|
+
/// The fit a `resizeMode` already asks for, for an image that has a
|
|
4415
|
+
/// position and no `objectFit`; "fill" when it has neither.
|
|
4416
|
+
static func objectFit(resizeMode: String?) -> String {
|
|
4417
|
+
switch resizeMode {
|
|
4418
|
+
case "cover": return "cover"
|
|
4419
|
+
case "contain": return "contain"
|
|
4420
|
+
case "center": return "none"
|
|
4421
|
+
default: return "fill"
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4407
4425
|
private static func objectPosition(_ raw: Any?) -> CGPoint {
|
|
4408
4426
|
guard let text = raw as? String else { return CGPoint(x: 0.5, y: 0.5) }
|
|
4409
4427
|
var parts = text.lowercased().split(whereSeparator: { $0.isWhitespace }).map(String.init)
|
|
@@ -7184,6 +7202,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7184
7202
|
// a screen that hid its own.
|
|
7185
7203
|
navigationController?.setNavigationBarHidden(false, animated: animated)
|
|
7186
7204
|
}
|
|
7205
|
+
// Options can arrive before the screen is in a stack: whether it has
|
|
7206
|
+
// a screen to go back to is known now.
|
|
7207
|
+
if backButtonSpec != nil { applyLeadingBarItems() }
|
|
7187
7208
|
applyNavigationBarAppearance()
|
|
7188
7209
|
// Each screen says whether it wants the tab bar, so one that hid it
|
|
7189
7210
|
// never leaves it hidden for the next. Only the screen on screen:
|
|
@@ -7858,7 +7879,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7858
7879
|
/// - `craft.secureStorage.getSync(key)`: the Keychain item the web page's
|
|
7859
7880
|
/// `craft.secureStorage.set` wrote.
|
|
7860
7881
|
/// - `craft.navigation.setOptions({ title, titleColor, largeTitle, hidden,
|
|
7861
|
-
/// backTitle, tabBarHidden, tabBarHiddenWhenPushed, tabItem, swipeBack,
|
|
7882
|
+
/// backTitle, backButton, tabBarHidden, tabBarHiddenWhenPushed, tabItem, swipeBack,
|
|
7862
7883
|
/// contentSwipeBack,
|
|
7863
7884
|
/// tabSwipeEnabled, backgroundColor, navigationBarSolid, leftButtons,
|
|
7864
7885
|
/// rightButtons, tabAccessory })`; a button's tap is the event
|
|
@@ -8474,10 +8495,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8474
8495
|
navigationController?.setNavigationBarHidden(hidden, animated: animatesNavigationBarChanges)
|
|
8475
8496
|
}
|
|
8476
8497
|
}
|
|
8498
|
+
if options.keys.contains("backButton") {
|
|
8499
|
+
backButtonSpec = options["backButton"] as? [String: Any]
|
|
8500
|
+
backButtonItem = nil
|
|
8501
|
+
}
|
|
8477
8502
|
if options.keys.contains("leftButtons") {
|
|
8478
8503
|
let buttons = (options["leftButtons"] as? [[String: Any]]) ?? []
|
|
8479
|
-
|
|
8480
|
-
|
|
8504
|
+
leftButtonItems = buttons.compactMap(navigationButton)
|
|
8505
|
+
}
|
|
8506
|
+
if options.keys.contains("backButton") || options.keys.contains("leftButtons") {
|
|
8507
|
+
applyLeadingBarItems()
|
|
8481
8508
|
}
|
|
8482
8509
|
if options.keys.contains("rightButtons") {
|
|
8483
8510
|
let buttons = (options["rightButtons"] as? [[String: Any]]) ?? []
|
|
@@ -8575,6 +8602,44 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8575
8602
|
return actions.isEmpty ? nil : UIMenu(children: actions)
|
|
8576
8603
|
}
|
|
8577
8604
|
|
|
8605
|
+
/// `backButton`: what this screen's Back is drawn with, when it is not
|
|
8606
|
+
/// the chevron (`{ symbol: "xmark", accessibilityLabel: "Close" }` for a
|
|
8607
|
+
/// screen that closes rather than goes back). `leftButtons` are added
|
|
8608
|
+
/// beside Back, so a screen could not put its own symbol in Back's place
|
|
8609
|
+
/// (#527). Nil is the system's chevron.
|
|
8610
|
+
private var backButtonSpec: [String: Any]?
|
|
8611
|
+
private var backButtonItem: UIBarButtonItem?
|
|
8612
|
+
private var leftButtonItems: [UIBarButtonItem] = []
|
|
8613
|
+
|
|
8614
|
+
/// The screen's own Back, when it asked for one and has a screen to go
|
|
8615
|
+
/// back to, then its left buttons. The chevron is hidden only while the
|
|
8616
|
+
/// item that replaces it is in the bar: a symbol UIKit does not have
|
|
8617
|
+
/// leaves the chevron. The back-swipe is the shell's to allow, and is
|
|
8618
|
+
/// not affected.
|
|
8619
|
+
private func applyLeadingBarItems() {
|
|
8620
|
+
// Known to be the first screen of its stack: nothing to go back to.
|
|
8621
|
+
let isRoot = navigationController?.viewControllers.first === self
|
|
8622
|
+
if !isRoot, backButtonItem == nil, let spec = backButtonSpec,
|
|
8623
|
+
let symbol = spec["symbol"] as? String, let image = UIImage(systemName: symbol) {
|
|
8624
|
+
let item = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(backButtonTapped))
|
|
8625
|
+
item.accessibilityIdentifier = "nav-back"
|
|
8626
|
+
item.accessibilityLabel = (spec["accessibilityLabel"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
8627
|
+
?? (symbol.hasPrefix("xmark") ? "Close" : "Back")
|
|
8628
|
+
backButtonItem = item
|
|
8629
|
+
}
|
|
8630
|
+
let back = isRoot ? nil : backButtonItem
|
|
8631
|
+
navigationItem.hidesBackButton = back != nil
|
|
8632
|
+
navigationItem.leftItemsSupplementBackButton = true
|
|
8633
|
+
navigationItem.leftBarButtonItems = (back.map { [$0] } ?? []) + leftButtonItems
|
|
8634
|
+
}
|
|
8635
|
+
|
|
8636
|
+
/// What `craft.navigation.back()` does.
|
|
8637
|
+
@objc private func backButtonTapped() {
|
|
8638
|
+
if !isAppScript, navigationController?.topViewController === self {
|
|
8639
|
+
navigationController?.popViewController(animated: true)
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8578
8643
|
private func navigationButton(_ spec: [String: Any]) -> UIBarButtonItem? {
|
|
8579
8644
|
guard let id = spec["id"] as? String, !id.isEmpty else { return nil }
|
|
8580
8645
|
let action = UIAction { [weak self] _ in
|
|
@@ -9060,27 +9125,44 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9060
9125
|
/// view is does not come into it: a page of a pager that is off to one
|
|
9061
9126
|
/// side now is under the actions once it is swiped to. The safe area and
|
|
9062
9127
|
/// the viewport are left alone: content still scrolls behind the
|
|
9063
|
-
/// actions.
|
|
9128
|
+
/// actions.
|
|
9129
|
+
///
|
|
9130
|
+
/// Two things are not behind the actions, and get none. A scroll view
|
|
9131
|
+
/// that is not inside the shell the actions float in: a sheet is
|
|
9132
|
+
/// presented in front of them, and its scroll view was given the room of
|
|
9133
|
+
/// a capsule it covers, so every sheet could be dragged up into blank
|
|
9134
|
+
/// space. And a scroll view or list inside another that scrolls the same
|
|
9135
|
+
/// way: it is brought clear by the outer one, and where it sits in a page
|
|
9136
|
+
/// not yet scrolled to it says nothing about the glass; a short list
|
|
9137
|
+
/// below the fold was given the height of the page above it. Internal
|
|
9138
|
+
/// for XCTest.
|
|
9064
9139
|
func insetScrollViewsForFloatingActions() {
|
|
9065
9140
|
let chrome = CraftChrome.shared
|
|
9066
|
-
let
|
|
9067
|
-
|
|
9068
|
-
}
|
|
9141
|
+
let host = chrome.floatingActionsHost.flatMap { view.isDescendant(of: $0) ? $0 : nil }
|
|
9142
|
+
let actions = host == nil ? nil : chrome.floatingActions
|
|
9069
9143
|
func cover(_ scroll: UIScrollView) -> CGFloat {
|
|
9070
|
-
guard let actions, let window = scroll.window else { return 0 }
|
|
9144
|
+
guard let actions, let host, let window = scroll.window, scroll.isDescendant(of: host) else { return 0 }
|
|
9071
9145
|
let frame = scroll.convert(scroll.bounds, to: window)
|
|
9146
|
+
// Nothing past the end of the shell is on the glass.
|
|
9147
|
+
let end = min(frame.maxY, host.convert(host.bounds, to: window).maxY)
|
|
9072
9148
|
// An edge-to-edge scroller already clears the tab bar through
|
|
9073
9149
|
// UIKit's safe area. Add only what the actions reach above it.
|
|
9074
9150
|
let automaticBottom = max(0, scroll.adjustedContentInset.bottom - scroll.contentInset.bottom)
|
|
9075
|
-
return max(0,
|
|
9151
|
+
return max(0, end - actions.minY - automaticBottom)
|
|
9076
9152
|
}
|
|
9077
|
-
func visit(_ node: RenderedNode
|
|
9153
|
+
func visit(_ node: RenderedNode?, inside: Bool) {
|
|
9078
9154
|
guard let node else { return }
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9155
|
+
var inside = inside
|
|
9156
|
+
if let scroll = node.view as? CraftNativeScrollView, scroll.isVertical {
|
|
9157
|
+
scroll.floatingCover = inside ? 0 : cover(scroll)
|
|
9158
|
+
inside = true
|
|
9159
|
+
} else if let list = node.view as? CraftNativeFlatList {
|
|
9160
|
+
list.floatingCover = inside ? 0 : cover(list)
|
|
9161
|
+
if (list.collectionViewLayout as? UICollectionViewFlowLayout)?.scrollDirection != .horizontal { inside = true }
|
|
9162
|
+
}
|
|
9163
|
+
node.children.forEach { visit($0, inside: inside) }
|
|
9082
9164
|
}
|
|
9083
|
-
visit(renderedRoot)
|
|
9165
|
+
visit(renderedRoot, inside: false)
|
|
9084
9166
|
floatingClearanceApplied = actions != nil
|
|
9085
9167
|
}
|
|
9086
9168
|
|