@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.4

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.
Files changed (193) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/android/CLAUDE.md +17 -7
  3. package/android/CORE.md +8 -3
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  6. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  8. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  9. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  12. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  13. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
  14. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  15. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
  16. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  17. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
  19. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  20. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  22. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  24. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  25. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  26. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +16 -6
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -3
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +6 -3
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +58 -15
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +16 -7
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +10 -2
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
  50. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  52. package/cli/doctor/checks/app.js +61 -13
  53. package/cli/migrate/native.js +18 -0
  54. package/cli/setup/source.js +2 -1
  55. package/cli/setup/steps/mainApplication.js +134 -19
  56. package/docs-md/architecture/transitions.md +6 -7
  57. package/docs-md/help/glossary.md +1 -1
  58. package/docs-md/help/troubleshooting.md +20 -3
  59. package/docs-md/index.json +4 -0
  60. package/docs-md/migration/from-react-native-navigation.md +8 -2
  61. package/docs-md/screen-ui/back-and-gestures.md +2 -2
  62. package/docs-md/start/installation.md +3 -1
  63. package/ios/CLAUDE.md +17 -5
  64. package/ios/CORE.md +13 -8
  65. package/ios/Contract/ANNCommandAnswer.mm +5 -20
  66. package/ios/Contract/ANNCommandContract.h +15 -0
  67. package/ios/Contract/ANNCommandContract.mm +78 -0
  68. package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
  69. package/ios/Core/ANNErrorHandler.h +1 -1
  70. package/ios/Core/ANNErrorHandler.mm +3 -1
  71. package/ios/Core/ANNEventEmitter.h +1 -1
  72. package/ios/Core/ANNEventEmitter.mm +2 -2
  73. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  74. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  75. package/ios/Core/ANNNavigationState.h +7 -3
  76. package/ios/Core/ANNNavigationState.mm +10 -5
  77. package/ios/Core/ANNNodeHelper.h +32 -12
  78. package/ios/Core/ANNNodeHelper.mm +100 -3
  79. package/ios/Core/ANNRenderWait.h +15 -1
  80. package/ios/Core/ANNRenderWait.mm +119 -17
  81. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  82. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  83. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  84. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  85. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  86. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  87. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  88. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  89. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  90. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  91. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  92. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  93. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  94. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  95. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  96. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  97. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  98. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  99. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  100. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  101. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  102. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  103. package/ios/Options/ANNOptionsCache.h +7 -6
  104. package/ios/Options/ANNSearchBarOptions.h +4 -3
  105. package/ios/Options/ANNUserChoice.h +57 -0
  106. package/ios/Options/ANNUserChoice.mm +44 -0
  107. package/ios/Options/Params/ANNImageSource.h +61 -0
  108. package/ios/Options/Params/ANNImageSource.mm +101 -0
  109. package/ios/Options/Params/ANNOptionImages.h +37 -0
  110. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  111. package/ios/Options/Params/Image.h +11 -2
  112. package/ios/Options/Params/Image.mm +15 -2
  113. package/ios/Options/Params/ImageParser.h +5 -5
  114. package/ios/Options/Params/ImageParser.mm +28 -46
  115. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  116. package/ios/RECIPES.md +11 -5
  117. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  118. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  119. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  120. package/ios/TopBar/ANNBarButtons.h +9 -5
  121. package/ios/TopBar/ANNBarButtons.mm +211 -63
  122. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  123. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  124. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  125. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  126. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  127. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  128. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  129. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  130. package/lib/module/ARCHITECTURE.md +18 -18
  131. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  132. package/lib/module/api/navigation.js.map +1 -1
  133. package/lib/module/engine/commands.js +9 -7
  134. package/lib/module/engine/commands.js.map +1 -1
  135. package/lib/module/engine/layout.js +13 -9
  136. package/lib/module/engine/layout.js.map +1 -1
  137. package/lib/module/engine/options.js +44 -11
  138. package/lib/module/engine/options.js.map +1 -1
  139. package/lib/module/engine/places.js +44 -7
  140. package/lib/module/engine/places.js.map +1 -1
  141. package/lib/module/engine/props.js +30 -1
  142. package/lib/module/engine/props.js.map +1 -1
  143. package/lib/module/engine/rememberedOptions.js +1 -1
  144. package/lib/module/engine/rememberedOptions.js.map +1 -1
  145. package/lib/module/engine/screens.js +22 -12
  146. package/lib/module/engine/screens.js.map +1 -1
  147. package/lib/module/interfaces/contract/commands.json +101 -0
  148. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  149. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  150. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  151. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  152. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  153. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  154. package/lib/typescript/api/navigation.d.ts +4 -1
  155. package/lib/typescript/api/navigation.d.ts.map +1 -1
  156. package/lib/typescript/engine/commands.d.ts.map +1 -1
  157. package/lib/typescript/engine/options.d.ts +5 -2
  158. package/lib/typescript/engine/options.d.ts.map +1 -1
  159. package/lib/typescript/engine/places.d.ts +5 -2
  160. package/lib/typescript/engine/places.d.ts.map +1 -1
  161. package/lib/typescript/engine/props.d.ts +8 -1
  162. package/lib/typescript/engine/props.d.ts.map +1 -1
  163. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  164. package/lib/typescript/engine/screens.d.ts.map +1 -1
  165. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  166. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  167. package/lib/typescript/interfaces/Events.d.ts +19 -10
  168. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  169. package/lib/typescript/interfaces/options/animations.d.ts +5 -2
  170. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  171. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  172. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  173. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  174. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  175. package/package.json +1 -1
  176. package/src/ARCHITECTURE.md +18 -18
  177. package/src/adapters/NativeANNTurboModule.ts +5 -2
  178. package/src/api/events/navigationEvents.ts +1 -1
  179. package/src/api/hooks/useNavigation.ts +1 -1
  180. package/src/api/navigation.ts +4 -1
  181. package/src/engine/commands.ts +19 -7
  182. package/src/engine/layout.ts +16 -9
  183. package/src/engine/options.ts +39 -11
  184. package/src/engine/places.ts +56 -7
  185. package/src/engine/props.ts +30 -1
  186. package/src/engine/rememberedOptions.ts +4 -1
  187. package/src/engine/screens.tsx +23 -13
  188. package/src/interfaces/ComponentEvents.ts +2 -2
  189. package/src/interfaces/Events.ts +19 -10
  190. package/src/interfaces/contract/commands.json +101 -0
  191. package/src/interfaces/options/animations.ts +5 -2
  192. package/src/interfaces/options/buttons.ts +13 -5
  193. package/src/interfaces/options/topBar.ts +22 -9
@@ -240,8 +240,9 @@ internal class RootCoordinator(
240
240
  /**
241
241
  * The activity's layers go: the `setRoot` on its way ends, then the root it replaced, the current root and its root
242
242
  * are destroyed, as at any destroy (their events reach only their own JavaScript). [answerPending]: the activity
243
- * went, the waiting `setRoot` resolves with `''` (JavaScript sets the root again in the next activity); otherwise
244
- * (a reload) it gets no answer.
243
+ * went; a `setRoot` whose root still waited resolves with `''` (JavaScript sets the root again in the next
244
+ * activity), one whose root showed (its transition ran, or the modals and overlays of its layout were showing)
245
+ * resolves with the root's id. Otherwise (a reload) it gets no answer.
245
246
  */
246
247
  fun destroyAll(answerPending: Boolean) {
247
248
  val pending = pending
@@ -256,7 +257,9 @@ internal class RootCoordinator(
256
257
  modals.replacedRootGone()
257
258
  pending?.let(::releaseState)
258
259
  if (pending != null && pending.phase == Phase.Waiting) firstRoot.rootDestroyed()
259
- if (answerPending) pending?.answer?.resolve("")
260
+ if (!answerPending || pending == null) return
261
+ // An empty id tells JavaScript the root didn't show: its start waits for the next activity and sets it there.
262
+ pending.answer.resolve(if (pending.phase == Phase.Waiting) "" else pending.root.id)
260
263
  }
261
264
 
262
265
  // Waits for the screens that show when the root shows, then runs its transition. Without `waitForRender` one limit
@@ -124,18 +124,18 @@ internal class OverlayCoordinator(
124
124
  }
125
125
 
126
126
  /**
127
- * `dismissOverlay`: the overlays with the root id [componentId] go — one that waits is cancelled, one that shows
128
- * goes with its events (both, when a replacement of a shown one waits) — and [answer] resolves with the id. An id
129
- * no node has: `not shown`; a node that is no overlay's root (Android takes only the root's id): `not in an
130
- * overlay`.
127
+ * `dismissOverlay`: the overlay [componentId] is in — its root or any screen inside it — goes with every overlay of
128
+ * the same root id: one that waits is cancelled, one that shows goes with its events (both, when a replacement of
129
+ * a shown one waits). [answer] resolves with the overlay's root id, as on iOS. An id no node has: `not shown`; a
130
+ * node in no overlay: `not in an overlay`.
131
131
  */
132
132
  fun dismiss(componentId: String, answer: CommandAnswer) {
133
- if (host.find(componentId) == null) return answer.reject(ErrorTexts.screenNotShown(componentId))
134
- val matches = records.filter { it.rootId == componentId }
135
- if (matches.isEmpty()) return answer.reject(ErrorTexts.notInOverlay(componentId))
133
+ val node = host.find(componentId) ?: return answer.reject(ErrorTexts.screenNotShown(componentId))
134
+ val rootId = recordOf(node)?.rootId ?: return answer.reject(ErrorTexts.notInOverlay(componentId))
135
+ val matches = records.filter { it.rootId == rootId }
136
136
  matches.filter { it.state == OverlayRecord.State.Waiting }.forEach(::cancel)
137
137
  val shown = matches.filter { it.state == OverlayRecord.State.Shown }
138
- if (shown.isEmpty()) return answer.resolve(componentId)
138
+ if (shown.isEmpty()) return answer.resolve(rootId)
139
139
  shown.dropLast(1).forEach { remove(it, null) }
140
140
  remove(shown.last(), answer)
141
141
  }
@@ -41,11 +41,18 @@ interface KeepsStatusBarRoom
41
41
  /** A node that shows screens in its columns (the split view): `showInColumn` and `setColumnShown` go to it. */
42
42
  interface ShowsColumns {
43
43
  /**
44
- * Shows [screen] in the column after the first one, in place of what it shows; [listener] once it shows, or with
45
- * the error reason, which names [from], the screen the command comes from. False when the node doesn't take it now
46
- * (its compact layout shows): the core pushes the screen over [from] instead.
44
+ * Shows [screen] in [column], in place of what it shows; [listener] once it shows, or with the error reason, which
45
+ * names [from], the screen the command comes from. [goesInStack] says whether [screen]'s layout is what a stack
46
+ * takes (a screen, a native screen, a pair), not a container. False when the node doesn't take it now (its compact
47
+ * layout shows): the core pushes the screen over [from] instead.
47
48
  */
48
- fun showScreen(screen: ViewController<*>, from: ViewController<*>, listener: CommandListener): Boolean
49
+ fun showScreen(
50
+ screen: ViewController<*>,
51
+ column: String,
52
+ goesInStack: Boolean,
53
+ from: ViewController<*>,
54
+ listener: CommandListener,
55
+ ): Boolean
49
56
 
50
57
  /** Shows or hides [column]; [done] once the columns have stopped moving. */
51
58
  fun setColumnShown(column: String, shown: Boolean, done: Runnable)
@@ -131,7 +131,7 @@ internal class SideMenuController(
131
131
 
132
132
  // `mergeOptions` with the side menu's id: its own options, then what a merge does, then the layouts around.
133
133
  override fun mergeOptions(options: Options) {
134
- this.options = this.options.copy().mergeWith(options)
134
+ mergeOwnOptions(options)
135
135
  applyMerge(options)
136
136
  performOnParentController { it.mergeChildOptions(SideMenuRules.withoutSideMenu(options), this) }
137
137
  }
@@ -1,6 +1,8 @@
1
1
  package dev.apexnative.navigation.viewcontrollers.splitview
2
2
 
3
3
  import android.content.Context
4
+ import android.view.View
5
+ import android.view.ViewGroup
4
6
  import android.view.WindowInsets
5
7
  import androidx.core.graphics.Insets
6
8
  import androidx.core.view.WindowInsetsCompat
@@ -14,6 +16,27 @@ class SplitPanes(context: Context) : SlidingPaneLayout(context) {
14
16
  /** The panes are side by side. */
15
17
  var isSideBySide = false
16
18
 
19
+ /**
20
+ * Puts [next] in the place of the pane [old], with the structure and the params [old] had. The library wraps the
21
+ * second view it is given in a view of its own that holds back touches (`addView` with one child there), and its
22
+ * `removeView` takes that wrapper away: a plain remove and add would wrap a new primary pane, or give a new
23
+ * secondary one the wrapper's params, and lose its width and weight. So the secondary pane's content changes
24
+ * inside the wrapper, and a new primary pane goes in before the old one leaves, while two children are there.
25
+ */
26
+ internal fun replacePane(old: View, next: View) {
27
+ val holder = old.parent as? ViewGroup ?: return
28
+ val params = old.layoutParams
29
+ if (holder === this) {
30
+ // A copy: the width that `splitView.width` changes later is the new pane's own.
31
+ addView(next, indexOfChild(old), if (params is LayoutParams) LayoutParams(params) else params)
32
+ removeView(old)
33
+ } else {
34
+ val index = holder.indexOfChild(old)
35
+ holder.removeView(old)
36
+ holder.addView(next, index, params)
37
+ }
38
+ }
39
+
17
40
  override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets {
18
41
  if (!isSideBySide) return super.dispatchApplyWindowInsets(insets)
19
42
  val own = onApplyWindowInsets(insets)
@@ -15,6 +15,7 @@ import dev.apexnative.navigation.options.SplitViewGroupOptions
15
15
  import dev.apexnative.navigation.options.SplitViewOptions
16
16
  import dev.apexnative.navigation.react.CommandListener
17
17
  import dev.apexnative.navigation.react.CommandListenerAdapter
18
+ import dev.apexnative.navigation.react.ErrorTexts
18
19
  import dev.apexnative.navigation.react.events.EventEmitter
19
20
  import dev.apexnative.navigation.system.SystemUiOwner
20
21
  import dev.apexnative.navigation.utils.TestIds
@@ -24,6 +25,7 @@ import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
24
25
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
25
26
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
26
27
  import dev.apexnative.navigation.viewcontrollers.parent.ChildOffsetFromRest
28
+ import dev.apexnative.navigation.viewcontrollers.parent.OptionsScreen
27
29
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
28
30
  import dev.apexnative.navigation.viewcontrollers.parent.ReturnsScreenFromColumn
29
31
  import dev.apexnative.navigation.viewcontrollers.parent.ShownChildren
@@ -45,8 +47,9 @@ class SplitViewController(
45
47
  presenter: Presenter,
46
48
  initialOptions: Options,
47
49
  private var split: SplitViewOptions,
48
- private val primary: ViewController<*>,
49
- private val secondary: ViewController<*>,
50
+ // The panes' content; `showInColumn` may give a pane a new one.
51
+ private var primary: ViewController<*>,
52
+ private var secondary: ViewController<*>,
50
53
  private val compact: ViewController<*>?,
51
54
  private val eventEmitter: EventEmitter,
52
55
  ) : ParentController<SplitViewLayout>(activity, childRegistry, id, presenter, initialOptions),
@@ -421,27 +424,67 @@ class SplitViewController(
421
424
  }
422
425
 
423
426
  /**
424
- * Shows [screen] in the secondary pane in place of what it shows and brings the pane into view. False with the
425
- * compact layout on screen: the screen goes over [from], the screen the command comes from.
427
+ * Shows [screen] in [column] (`primary` or `secondary`; Android has no other) in place of what the pane shows, and
428
+ * brings the pane into view. A stack pane's screens are replaced by it, unless it is a container
429
+ * (![goesInStack]): `columnIsStack`. Any other pane is taken by it, and its content goes. A column the split view
430
+ * doesn't have: `noSuchColumn`. False with the compact layout on screen: the screen goes over [from], the screen
431
+ * the command comes from.
426
432
  */
427
- override fun showScreen(screen: ViewController<*>, from: ViewController<*>, listener: CommandListener): Boolean {
433
+ override fun showScreen(
434
+ screen: ViewController<*>,
435
+ column: String,
436
+ goesInStack: Boolean,
437
+ from: ViewController<*>,
438
+ listener: CommandListener,
439
+ ): Boolean {
428
440
  if (isViewCreated && getView().isCompact) return false
429
- val stack = secondary as? StackController
430
- if (stack == null) {
431
- listener.onError("the 'secondary' column of the split view of the screen '${from.id}' is not a stack")
432
- return true
441
+ val pane = when (column) {
442
+ PRIMARY -> primary
443
+ SECONDARY -> secondary
444
+ else -> null
433
445
  }
434
- stack.setRoot(listOf(screen), false, object : CommandListenerAdapter() {
435
- override fun onSuccess(childId: String) {
446
+ when {
447
+ pane == null -> listener.onError(ErrorTexts.noSuchColumn(from.id, column))
448
+ pane is StackController && !goesInStack -> listener.onError(ErrorTexts.columnIsStack(from.id, column))
449
+ pane is StackController -> pane.setRoot(listOf(screen), false, object : CommandListenerAdapter() {
450
+ override fun onSuccess(childId: String) {
451
+ updateBackButton()
452
+ bringIntoView(column) { listener.onSuccess(childId) }
453
+ }
454
+
455
+ override fun onError(message: String) = listener.onError(message)
456
+ })
457
+ else -> {
458
+ replacePane(pane, screen)
436
459
  updateBackButton()
437
- showSecondary { listener.onSuccess(childId) }
460
+ val shownId = ((screen as? OptionsScreen)?.optionsScreen ?: screen).id
461
+ bringIntoView(column) { listener.onSuccess(shownId) }
438
462
  }
439
-
440
- override fun onError(message: String) = listener.onError(message)
441
- })
463
+ }
442
464
  return true
443
465
  }
444
466
 
467
+ // [next] takes the pane of [old], a screen alone or a container, as iOS puts a screen in place of a column's: its
468
+ // view goes where the old one's was, the children shown are told as a move of the panes tells them (DidDisappear
469
+ // of what left, DidAppear of what came), and [old] goes with its screens.
470
+ private fun replacePane(old: ViewController<*>, next: ViewController<*>) {
471
+ next.setParentController(this)
472
+ if (old === primary) primary = next else secondary = next
473
+ peekView()?.let { layout ->
474
+ layout.panes.replacePane(old.getView(), next.getView())
475
+ applyTestIds(primary.getView(), secondary.getView())
476
+ hideCoveredPane()
477
+ shownChildren.moved()
478
+ tellShownChanged()
479
+ }
480
+ old.destroy()
481
+ }
482
+
483
+ // The pane of [column] comes into view; [done] once the panes stopped.
484
+ private fun bringIntoView(column: String, done: Runnable) {
485
+ if (column == PRIMARY) setColumnShown(PRIMARY, true, done) else showSecondary(done)
486
+ }
487
+
445
488
  private fun showSecondary(done: Runnable) {
446
489
  if (!isSliding || getView().panes.isOpen) {
447
490
  done.run()
@@ -105,33 +105,42 @@ internal class StackCommands(
105
105
  }
106
106
 
107
107
  /**
108
- * `showInColumn` (Android): [layout] in the secondary pane of the split view of the screen [componentId], or over
109
- * that screen in its stack. Before any node: the screen is there, else `the screen '<id>' is not shown`; the push's
110
- * checks of the layout (an id that shows already; a container, which the stack of the screen, else its split view,
111
- * doesn't take); a screen prepared with the id goes. The screen is made at once, a native screen's view too. The
112
- * split view takes it (the answer once its pane stopped, or the rejection of the pane); with the compact layout on
113
- * screen, or without a split view, it is pushed over [componentId] in its stack, with the push's answer and errors;
114
- * neither: `the screen '<id>' is neither in a split view nor in a stack`. Every rejection after the screen was made
115
- * destroys it. Two calls in a row each make their screen the root of the pane in turn, and each answers its own id.
108
+ * `showInColumn` (Android): [layout] in [column] of the split view of the screen [componentId], or over that screen
109
+ * in its stack. Before any node: the screen is there, else `the screen '<id>' is not shown`; the layout has no id
110
+ * that shows already; out of a split view, the push's check of the layout (a container, which the stack doesn't
111
+ * take); a screen prepared with the id goes. The screen is made at once, a native screen's view too. The split view
112
+ * takes it — in place of a stack column's screens or of a column's content — with the answer once its pane
113
+ * stopped, or rejects (a column it doesn't have, a container for a stack column); with the compact layout on
114
+ * screen, or without a split view, it is pushed over [componentId] in its stack, with the push's checks, answer and
115
+ * errors; neither: `the screen '<id>' is neither in a split view nor in a stack`. Every rejection after the screen
116
+ * was made destroys it. Two calls in a row each make their screen the column's content in turn, and each answers
117
+ * its own id.
116
118
  */
117
- fun showInColumn(componentId: String, layout: LayoutNode, answer: CommandAnswer, what: String) {
119
+ fun showInColumn(componentId: String, column: String, layout: LayoutNode, answer: CommandAnswer, what: String) {
118
120
  val source = find(componentId)?.takeIf { !prepared.isPrepared(it) }
119
121
  ?: return answer.reject(ErrorTexts.screenNotShown(componentId))
120
122
  val shown = LayoutChecks.firstShownId(listOf(layout), ::isShown) { false }
121
123
  if (shown != null) return answer.reject(ErrorTexts.alreadyShown(shown))
122
124
  val columns = nodeWith<ShowsColumns>(source)
123
125
  val stack = source as? StackController ?: source.findParentStack()
124
- val placeId = stack?.id ?: (columns as? ViewController<*>)?.id
125
- placeId?.let { id -> LayoutChecks.checkStackChild(layout, id)?.let { return answer.reject(it) } }
126
+ // Out of a split view it can only be a push: the stack's check comes before any node.
127
+ if (columns == null && stack != null) {
128
+ LayoutChecks.checkStackChild(layout, stack.id)?.let { return answer.reject(it) }
129
+ }
126
130
  if (!factory.canMake(layout)) return answer.reject(ErrorTexts.notDoneYet("showInColumn"))
127
131
  prepared.discard(layout.id)
128
132
  val node = make(layout)
129
133
  try {
130
- if (columns?.showScreen(node, source, destroyingOnError(node, answer)) == true) return
134
+ val goesInStack = LayoutChecks.isStackChild(layout)
135
+ if (columns?.showScreen(node, column, goesInStack, source, destroyingOnError(node, answer)) == true) return
131
136
  if (stack == null) {
132
137
  node.destroy()
133
138
  return answer.reject(ErrorTexts.neitherSplitViewNorStack(componentId))
134
139
  }
140
+ LayoutChecks.checkStackChild(layout, stack.id)?.let {
141
+ node.destroy()
142
+ return answer.reject(it)
143
+ }
135
144
  pushMade(stack, source, node, layout, answer, what)
136
145
  } catch (e: Throwable) {
137
146
  node.destroy()
@@ -225,8 +234,9 @@ internal class StackCommands(
225
234
  fun setStackRoot(componentId: String, layouts: List<LayoutNode>, answer: CommandAnswer, what: String) {
226
235
  if (layouts.isEmpty()) return answer.reject(ErrorTexts.noScreensForStack(componentId))
227
236
  val stack = stackOf(componentId, answer) ?: return
228
- // An id of this stack may come again: the new screen replaces it (`setStackRoot` step 3).
229
- val shown = LayoutChecks.firstShownId(layouts, { isShown(it) && !isOf(stack, it) }) { false }
237
+ // An id of an entry of this stack may come again: the new screen replaces it (`setStackRoot` step 3). The
238
+ // stack's own id, and a screen an accepted cut or replace took away, show elsewhere or still go: as on iOS.
239
+ val shown = LayoutChecks.firstShownId(layouts, ::isShown) { isInEntryOf(stack, it.id) }
230
240
  if (shown != null) return answer.reject(ErrorTexts.alreadyShown(shown))
231
241
  for (layout in layouts) LayoutChecks.checkStackChild(layout, stack.id)?.let { return answer.reject(it) }
232
242
  if (!layouts.all(factory::canMake)) return answer.reject(ErrorTexts.notDoneYet("setStackRoot"))
@@ -357,10 +367,10 @@ internal class StackCommands(
357
367
  // A node of the registry has [id], and it isn't only prepared for a push (`push` step 1, `preparePush`).
358
368
  private fun isShown(id: String): Boolean = registry.find(id)?.let { !prepared.isPrepared(it) } == true
359
369
 
360
- // The node with [id] is [stack] or in it.
361
- private fun isOf(stack: StackController, id: String): Boolean {
362
- val node = find(id) ?: return false
363
- return node === stack || node.findParentStack() === stack
370
+ // The node with [id] is in an entry of [stack] as JavaScript sees it (`StackQueue.entries`), not the stack itself.
371
+ private fun isInEntryOf(stack: StackController, id: String): Boolean {
372
+ val node = find(id)?.takeIf { it !== stack } ?: return false
373
+ return stack.entryOf(node)?.let { it in stack.queue.entries } == true
364
374
  }
365
375
 
366
376
  // A node of [layout], with its view made now when it has a native screen: its creator's failure rejects the
@@ -12,6 +12,7 @@ import dev.apexnative.navigation.options.Options
12
12
  import dev.apexnative.navigation.options.TopBarOptions
13
13
  import dev.apexnative.navigation.react.CommandListener
14
14
  import dev.apexnative.navigation.react.ErrorTexts
15
+ import dev.apexnative.navigation.react.PendingCalls
15
16
  import dev.apexnative.navigation.react.events.AppearanceDispatcher
16
17
  import dev.apexnative.navigation.utils.NavigationLog
17
18
  import dev.apexnative.navigation.utils.UiThread
@@ -21,8 +22,8 @@ import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
21
22
  import dev.apexnative.navigation.viewcontrollers.component.ComponentViewController
22
23
  import dev.apexnative.navigation.viewcontrollers.component.ContentReadiness
23
24
  import dev.apexnative.navigation.viewcontrollers.component.Readiness
25
+ import dev.apexnative.navigation.viewcontrollers.component.RenderWait
24
26
  import dev.apexnative.navigation.viewcontrollers.component.RestPlace
25
- import dev.apexnative.navigation.viewcontrollers.component.ScreenContentFactory
26
27
  import dev.apexnative.navigation.viewcontrollers.navigator.BackCallbackState
27
28
  import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
28
29
  import dev.apexnative.navigation.viewcontrollers.parent.ContainerBackground
@@ -81,26 +82,34 @@ class StackController(
81
82
 
82
83
  /** The one queue of the stack. */
83
84
  // The readiness of the screens reaches the queue's waits on the next turn of the main loop: a signal that comes
84
- // inside a layout pass or a view's attach moves no view of the container there.
85
+ // inside a layout pass or a view's attach moves no view of the container there. A pushed screen's mount reaches
86
+ // its push behind the calls JavaScript made before it (`PendingCalls`), on a later turn too.
85
87
  internal var queue = StackQueue(
86
88
  display = StackViews(this),
87
89
  readiness = ContentReadiness,
88
90
  scheduler = { delay, task -> AppearanceDispatcher.scheduler.schedule(delay, task) },
89
91
  host = QueueHost(),
90
92
  deliver = { signal -> UiThread.post { signal() } },
93
+ afterCalls = PendingCalls::after,
91
94
  )
92
95
  private set
93
96
 
94
97
  /**
95
98
  * The unit tests run the stack over their own display, readiness and host (views are stubs there), before any
96
- * screen joins it; the readiness signals come at once there.
99
+ * screen joins it; the readiness signals come at once there, and a push's behind the calls as [afterCalls] says.
97
100
  */
98
- internal fun useQueueParts(display: StackDisplay, readiness: Readiness, host: StackQueue.Host?) {
101
+ internal fun useQueueParts(
102
+ display: StackDisplay,
103
+ readiness: Readiness,
104
+ host: StackQueue.Host?,
105
+ afterCalls: (() -> Unit) -> Unit = { it() },
106
+ ) {
99
107
  queue = StackQueue(
100
108
  display = display,
101
109
  readiness = readiness,
102
110
  scheduler = { delay, task -> AppearanceDispatcher.scheduler.schedule(delay, task) },
103
111
  host = host ?: QueueHost(),
112
+ afterCalls = afterCalls,
104
113
  )
105
114
  }
106
115
 
@@ -470,7 +479,7 @@ class StackController(
470
479
 
471
480
  // `mergeOptions` with the stack's id: its own options, what it shows, then the layouts around.
472
481
  override fun mergeOptions(options: Options) {
473
- this.options = this.options.copy().mergeWith(options)
482
+ mergeOwnOptions(options)
474
483
  applyOrKeep(options, null)
475
484
  performOnParentController { it.mergeChildOptions(withoutStackGroups(options), this) }
476
485
  }
@@ -730,8 +739,8 @@ class StackController(
730
739
  if (peekView() != null) topBar.stage(node)
731
740
  }
732
741
 
733
- override fun isOwnRootScreen(node: ViewController<*>): Boolean =
734
- (node as? ComponentViewController)?.componentName?.startsWith(ScreenContentFactory.SLOT_PREFIX) == false
742
+ override fun hasReactContent(node: ViewController<*>): Boolean =
743
+ RenderWait.waitedScreens(node).any { it is ComponentViewController }
735
744
 
736
745
  override fun log(text: String) {
737
746
  Log.e(NavigationLog.TAG, text)
@@ -185,7 +185,8 @@ internal interface StackDisplay {
185
185
 
186
186
  /**
187
187
  * The push of a prepared slot screen: [mounted] as the slot's content mounts into the screen's container (or at the
188
- * limit of 0.5 s), then [start] on the pre-draw of that frame, while [waiting] holds.
188
+ * limit of 0.5 s), then [start] on the pre-draw of that frame, while [waiting] holds; the queue starts the
189
+ * transition from there behind the calls JavaScript made before, on a later turn.
189
190
  */
190
191
  fun awaitPrepared(screen: ViewController<*>, waiting: () -> Boolean, mounted: () -> Unit, start: () -> Unit)
191
192
  }
@@ -52,8 +52,12 @@ private typealias Node = ViewController<*>
52
52
  *
53
53
  * Its waits for a screen to render are the core's one wait ([RenderWait]), on the screen it pushes or sets as the top;
54
54
  * the readiness signals reach it through [deliver] (the stack posts them to the next turn of the main loop: a signal
55
- * that comes inside a layout pass or a view's attach moves no view of the container there). A push or a
56
- * `setStackRoot` with elements waits for its targets after that, a wait like the others: a cut or a replace during it
55
+ * that comes inside a layout pass or a view's attach moves no view of the container there). A push of a screen with
56
+ * React content waits for its first mount: [PUSH_WAIT_MILLIS] at most without `waitForRender`, as long as it takes
57
+ * with `true`, not at all with `false`; its signal reaches the wait through [afterCalls], behind the calls JavaScript
58
+ * made in the commit of that mount (its `mergeOptions`), so the transition starts with the options of that commit —
59
+ * content mounted already goes that way too. A push or a `setStackRoot` with elements waits for its targets after
60
+ * that, a wait like the others, within what is left of its limit without `waitForRender`: a cut or a replace during it
57
61
  * interrupts the push, which moves no view into a layer.
58
62
  *
59
63
  * The transition is the screens' own when the options say so (`AnimationRules.stackIsOwn`): the display plays each
@@ -66,6 +70,11 @@ internal class StackQueue(
66
70
  private val scheduler: AppearanceDispatcher.Scheduler,
67
71
  private val host: Host,
68
72
  private val deliver: (() -> Unit) -> Unit = { it() },
73
+ /**
74
+ * How the mount of a pushed screen reaches the push's wait: on the main thread behind every call JavaScript made
75
+ * before it (the stack: `PendingCalls`, on a later turn of the main loop); at once by default.
76
+ */
77
+ private val afterCalls: (() -> Unit) -> Unit = { it() },
69
78
  ) {
70
79
  /** What the queue tells the stack. */
71
80
  interface Host {
@@ -104,10 +113,10 @@ internal class StackQueue(
104
113
  fun operationEnded(topSettled: Boolean)
105
114
 
106
115
  /**
107
- * Whether [node] is a React screen with a root of its own, which a push prepares when its switch is on
108
- * ([StackSwitches.preparesOwnRootPushes]).
116
+ * Whether [node] shows React content whose first mount its push waits for: a React screen (a root of its own or
117
+ * a slot), or an entry that shows one. A native screen has none: its push starts at once.
109
118
  */
110
- fun isOwnRootScreen(node: Node): Boolean = false
119
+ fun hasReactContent(node: Node): Boolean = false
111
120
 
112
121
  /** One line for the log: a failure that has no command to answer. */
113
122
  fun log(text: String) = Unit
@@ -427,11 +436,12 @@ internal class StackQueue(
427
436
  val node = op.node
428
437
  val push = resolved(node).animations.push
429
438
  op.awaitsTargets = awaitsTargets(push, !push.enabled.isFalse)
439
+ // A prepared screen rendered before its push; `false`, and a node without React content, start at once.
430
440
  val wait = when {
431
441
  op.prepared -> Wait.Prepared
432
442
  push.waitForRender.isTrue -> Wait.Content
433
- StackSwitches.preparesOwnRootPushes && host.isOwnRootScreen(node) -> Wait.Limited
434
- else -> Wait.None
443
+ push.waitForRender.isFalse || !host.hasReactContent(node) -> Wait.None
444
+ else -> Wait.Limited
435
445
  }
436
446
  if (wait == Wait.None) return if (op.awaitsTargets) awaitTargets(op) else beginPush(op)
437
447
  waiting = op
@@ -439,18 +449,37 @@ internal class StackQueue(
439
449
  Wait.Prepared -> awaitPrepared(op)
440
450
  Wait.Content -> {
441
451
  // Then the components of the bar drawn in advance, 0.5 s at most after the content (a title or a
442
- // background with its own `waitForRender: false` not).
452
+ // background with its own `waitForRender: false` not). React content mounted already goes behind the
453
+ // calls too; a native screen starts at once.
443
454
  display.placeWaiting(node)
444
- awaitContent(op, node, limit = null, bar = RenderWait.Bar.AfterContent)
455
+ awaitContent(
456
+ op,
457
+ node,
458
+ limit = null,
459
+ bar = RenderWait.Bar.AfterContent,
460
+ deliver = afterCalls,
461
+ deliversReady = host.hasReactContent(node),
462
+ )
445
463
  }
446
464
  else -> {
465
+ // The screen renders hidden under the top, its signal (or its content mounted already) behind the
466
+ // calls; the bar's components don't count. The 0.5 s hold for the content and the targets together.
447
467
  display.placeWaiting(node)
448
- awaitContent(op, node, limit = OWN_ROOT_WAIT_MILLIS, bar = RenderWait.Bar.None)
468
+ op.cancelLimit = scheduler.schedule(PUSH_WAIT_MILLIS) { op.overLimit = true }
469
+ awaitContent(
470
+ op,
471
+ node,
472
+ limit = PUSH_WAIT_MILLIS,
473
+ bar = RenderWait.Bar.None,
474
+ deliver = afterCalls,
475
+ deliversReady = true,
476
+ )
449
477
  }
450
478
  }
451
479
  }
452
480
 
453
481
  private fun beginPush(op: PushOp) {
482
+ op.cancelLimit?.invoke()
454
483
  val node = op.node
455
484
  val covered = shownList.lastOrNull()
456
485
  // The views first: a failure here leaves the lists as they were.
@@ -505,6 +534,7 @@ internal class StackQueue(
505
534
 
506
535
  // Off screen or without a view: at once, without a transition; the top appears when the stack shows.
507
536
  private fun pushAtOnce(op: PushOp) {
537
+ op.cancelLimit?.invoke()
508
538
  val node = op.node
509
539
  val covered = shownList.lastOrNull()
510
540
  if (display.hasView) {
@@ -533,6 +563,7 @@ internal class StackQueue(
533
563
  if (op.done) return
534
564
  op.done = true
535
565
  if (waiting === op) stopWaiting(op)
566
+ op.cancelLimit?.invoke()
536
567
  queued.remove(op)
537
568
  val node = op.node
538
569
  entryList.remove(node)
@@ -799,8 +830,16 @@ internal class StackQueue(
799
830
 
800
831
  // The one wait (RenderWait) for [screen], the node the operation brings: at [limit] the content not ready counts
801
832
  // as mounted; without one, `waitForRender: true` waits as long as it takes, except for content the readiness store
802
- // knows nothing of after 1 s, which counts as ready (the same rule as iOS).
803
- private fun awaitContent(op: Op, screen: Node, limit: Long?, bar: RenderWait.Bar) {
833
+ // knows nothing of after 1 s, which counts as ready (the same rule as iOS). A push's signals come through
834
+ // [afterCalls] ([deliver]); with [deliversReady], so does content ready at the start.
835
+ private fun awaitContent(
836
+ op: Op,
837
+ screen: Node,
838
+ limit: Long?,
839
+ bar: RenderWait.Bar,
840
+ deliver: (() -> Unit) -> Unit = this.deliver,
841
+ deliversReady: Boolean = false,
842
+ ) {
804
843
  val wait = RenderWait(
805
844
  readiness = readiness,
806
845
  scheduler = scheduler,
@@ -808,6 +847,7 @@ internal class StackQueue(
808
847
  bar = bar,
809
848
  countsUnknownContent = true,
810
849
  deliver = deliver,
850
+ deliversReadyContent = deliversReady,
811
851
  step = { work -> step(op) { work() } },
812
852
  onEnd = { endWait(op) },
813
853
  )
@@ -828,15 +868,32 @@ internal class StackQueue(
828
868
  step(op) { wait.retake(RenderWait.waitedScreens(node)) }
829
869
  }
830
870
 
871
+ // The prepared screen's slot mounts (or the display's limit passes), then its transition starts behind the calls
872
+ // JavaScript made before ([afterCalls]): a commit that came after the push (its JavaScript's own limit) brings its
873
+ // `mergeOptions` first. The same 0.5 s from the start hold for both: the transition starts then anyway.
831
874
  private fun awaitPrepared(op: PushOp) {
832
875
  val token = Any()
833
876
  op.waitToken = token
834
877
  val node = op.node
878
+ val still = { !destroyed && !op.done && op.waitToken === token && waiting === op && op.isCurrent() }
879
+ var mounted = false
880
+ val markMounted = {
881
+ if (!mounted && !readiness.isReady(node)) readiness.markReady(node)
882
+ mounted = true
883
+ }
884
+ val start = { step(op) { if (still()) endWait(op) } }
885
+ op.cancelCap = scheduler.schedule(PUSH_WAIT_MILLIS) {
886
+ step(op) {
887
+ if (!still()) return@step
888
+ markMounted()
889
+ endWait(op)
890
+ }
891
+ }
835
892
  display.awaitPrepared(
836
893
  node,
837
- waiting = { !destroyed && !op.done && op.waitToken === token && waiting === op && op.isCurrent() },
838
- mounted = { step(op) { if (!readiness.isReady(node)) readiness.markReady(node) } },
839
- start = { step(op) { if (waiting === op) endWait(op) } },
894
+ waiting = still,
895
+ mounted = { step(op) { markMounted() } },
896
+ start = { afterCalls(start) },
840
897
  )
841
898
  }
842
899
 
@@ -865,7 +922,8 @@ internal class StackQueue(
865
922
  val token = Any()
866
923
  op.waitToken = token
867
924
  val still = { !destroyed && !op.done && op.waitToken === token && waiting === op && op.isCurrent() }
868
- // Without `waitForRender` a `setStackRoot`'s 0.5 s holds for its content and its targets together.
925
+ // Without `waitForRender` the 0.5 s of a `setStackRoot` and of a push of React content hold for the content
926
+ // and the targets together.
869
927
  val from = currentChild
870
928
  op.stopTargets = display.awaitTargets(animation, from, to, { still() && !op.overLimit }) {
871
929
  step(op) { if (still()) endWait(op) }
@@ -874,6 +932,8 @@ internal class StackQueue(
874
932
 
875
933
  private fun stopWaiting(op: Op) {
876
934
  if (waiting === op) waiting = null
935
+ op.cancelCap?.invoke()
936
+ op.cancelCap = null
877
937
  op.waitToken = null
878
938
  op.wait?.cancel()
879
939
  op.wait = null
@@ -1218,9 +1278,12 @@ internal class StackQueue(
1218
1278
  // It waits for its elements' targets after its content; what stops that wait.
1219
1279
  var awaitsTargets = false
1220
1280
  var stopTargets: (() -> Unit)? = null
1221
- // The limit of a `setStackRoot` without `waitForRender` passed: its targets get nothing of it any more.
1281
+ // The limit of a `setStackRoot` or a push without `waitForRender` passed: its targets get nothing of it any
1282
+ // more.
1222
1283
  var overLimit = false
1223
1284
  var cancelLimit: (() -> Unit)? = null
1285
+ // The limit of a prepared push's wait, which ends with the wait.
1286
+ var cancelCap: (() -> Unit)? = null
1224
1287
  // Its screen came by an animation of its own: it stays at that animation's end values.
1225
1288
  var keepsEnd = false
1226
1289
  // The listener of a module heard its answer: it hears one only.
@@ -1295,20 +1358,13 @@ internal class StackQueue(
1295
1358
  /** How long a lower screen stays placed in the container to make its content, at most. */
1296
1359
  const val LOWER_SCREEN_MILLIS = 500L
1297
1360
 
1298
- /** How long the push of a screen with its own root waits for its content, with the switch on. */
1299
- const val OWN_ROOT_WAIT_MILLIS = 500L
1361
+ /**
1362
+ * How long a push waits for its screen's first mount without `waitForRender`, and a prepared push for its slot
1363
+ * and the calls before it.
1364
+ */
1365
+ const val PUSH_WAIT_MILLIS = 500L
1300
1366
 
1301
1367
  // The alpha an arriving screen shows with: its own at rest, never the 0 of its wait.
1302
1368
  private const val ARRIVING_ALPHA = 1f
1303
1369
  }
1304
1370
  }
1305
-
1306
- /**
1307
- * The internal switches of the stack. [preparesOwnRootPushes]: the push of a React screen with a root of its own starts
1308
- * its surface hidden in the stack at its size and insets, and starts its transition once the content rendered, 0.5 s at
1309
- * most. Off by default («б»: the push starts at once); it is measured at the end on a phone.
1310
- */
1311
- internal object StackSwitches {
1312
- @Volatile
1313
- var preparesOwnRootPushes = false
1314
- }