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

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 (163) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +147 -51
  3. package/android/CLAUDE.md +31 -6
  4. package/android/CORE.md +7 -2
  5. package/android/RECIPES.md +4 -3
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
  8. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
  9. package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
  10. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
  11. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
  12. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
  13. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
  14. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
  15. package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
  16. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
  17. package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
  18. package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +76 -13
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +86 -16
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +41 -23
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +24 -20
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +42 -4
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +29 -6
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +2 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -3
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +16 -2
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +148 -6
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +24 -0
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +51 -11
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +105 -2
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +123 -8
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +19 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
  50. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
  51. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
  52. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
  53. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
  54. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
  55. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
  56. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
  57. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
  58. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
  59. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
  60. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
  61. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
  62. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
  63. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
  64. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
  65. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
  66. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
  67. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
  68. package/cli/app.js +1 -1
  69. package/cli/project.js +6 -2
  70. package/cli/setup/index.js +13 -0
  71. package/cli/setup/steps/appDelegate.js +66 -11
  72. package/cli/setup/steps/sceneDelegate.js +108 -0
  73. package/docs-md/architecture/transitions.md +7 -1
  74. package/docs-md/containers/modals.md +4 -2
  75. package/docs-md/containers/overlays.md +3 -1
  76. package/docs-md/containers/side-menu.md +4 -2
  77. package/docs-md/containers/tabs.md +2 -0
  78. package/docs-md/flows/forms-in-a-modal.md +1 -1
  79. package/docs-md/guides/deep-links.md +15 -33
  80. package/docs-md/help/glossary.md +1 -1
  81. package/docs-md/index.json +8 -0
  82. package/docs-md/screen-ui/back-and-gestures.md +6 -0
  83. package/docs-md/screen-ui/system-ui.md +1 -1
  84. package/docs-md/start/installation.md +1 -0
  85. package/ios/Animations/ANNTransitionAnimator.mm +13 -11
  86. package/ios/Animations/ANNTransitionWait.mm +6 -7
  87. package/ios/Animations/Elements/ANNElementFinder.h +57 -0
  88. package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
  89. package/ios/Animations/Elements/ANNElementTransition.h +7 -5
  90. package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
  91. package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
  92. package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
  93. package/ios/CLAUDE.md +10 -2
  94. package/ios/CORE.md +3 -2
  95. package/ios/Contract/ANNCommandAnswer.h +4 -3
  96. package/ios/Contract/ANNCommandAnswer.mm +12 -2
  97. package/ios/Contract/ANNCommandRunner.h +2 -2
  98. package/ios/Contract/ANNCommandRunner.mm +1 -1
  99. package/ios/Contract/ANNErrorTexts.h +6 -2
  100. package/ios/Contract/ANNErrorTexts.mm +18 -10
  101. package/ios/Core/ANNDeepLinkQueue.h +20 -1
  102. package/ios/Core/ANNDeepLinkQueue.mm +107 -12
  103. package/ios/Core/ANNErrorHandler.h +8 -3
  104. package/ios/Core/ANNErrorHandler.mm +17 -1
  105. package/ios/Core/ANNEventContract.h +246 -0
  106. package/ios/Core/ANNEventContract.mm +225 -0
  107. package/ios/Core/ANNEventEmitter.h +5 -1
  108. package/ios/Core/ANNEventEmitter.mm +117 -80
  109. package/ios/Core/ANNHinge.mm +6 -4
  110. package/ios/Core/ANNSceneDelegate.mm +25 -15
  111. package/ios/Core/ANNSystemUI.h +3 -2
  112. package/ios/Core/ANNSystemUI.mm +7 -0
  113. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
  114. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  115. package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
  116. package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
  117. package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
  118. package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
  119. package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
  120. package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
  121. package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
  122. package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
  123. package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
  124. package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
  125. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
  126. package/ios/Options/ANNSheetOptions.h +4 -1
  127. package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
  128. package/ios/RECIPES.md +2 -1
  129. package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
  130. package/ios/TurboModules/ANNTurboModule.mm +5 -2
  131. package/lib/module/ARCHITECTURE.md +7 -4
  132. package/lib/module/api/app/createNavigation.js +2 -0
  133. package/lib/module/api/app/createNavigation.js.map +1 -1
  134. package/lib/module/api/hooks/useNavigation.js +8 -3
  135. package/lib/module/api/hooks/useNavigation.js.map +1 -1
  136. package/lib/module/api/linking/dispatchedLinks.js +23 -0
  137. package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
  138. package/lib/module/interfaces/contract/errors.json +275 -0
  139. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  140. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  141. package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
  142. package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
  143. package/lib/typescript/interfaces/Events.d.ts +9 -0
  144. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  145. package/lib/typescript/interfaces/options/animations.d.ts +27 -18
  146. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  147. package/lib/typescript/interfaces/options/modals.d.ts +33 -24
  148. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  149. package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
  150. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  151. package/lib/typescript/interfaces/options/system.d.ts +10 -6
  152. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  153. package/package.json +1 -1
  154. package/src/ARCHITECTURE.md +7 -4
  155. package/src/api/app/createNavigation.ts +2 -0
  156. package/src/api/hooks/useNavigation.ts +8 -3
  157. package/src/api/linking/dispatchedLinks.ts +18 -0
  158. package/src/interfaces/Events.ts +10 -0
  159. package/src/interfaces/contract/errors.json +275 -0
  160. package/src/interfaces/options/animations.ts +27 -18
  161. package/src/interfaces/options/modals.ts +33 -24
  162. package/src/interfaces/options/sideMenu.ts +7 -6
  163. package/src/interfaces/options/system.ts +10 -6
@@ -31,6 +31,7 @@ import dev.apexnative.navigation.viewcontrollers.component.ContentReadiness
31
31
  import dev.apexnative.navigation.viewcontrollers.component.ScreenReadiness
32
32
  import dev.apexnative.navigation.viewcontrollers.externalcomponent.ExternalComponentViewController
33
33
  import dev.apexnative.navigation.viewcontrollers.modal.ModalCoordinator
34
+ import dev.apexnative.navigation.viewcontrollers.modal.ModalRecord
34
35
  import dev.apexnative.navigation.viewcontrollers.modal.ModalViews
35
36
  import dev.apexnative.navigation.viewcontrollers.overlay.OverlayCoordinator
36
37
  import dev.apexnative.navigation.viewcontrollers.overlay.OverlayRecord
@@ -38,6 +39,7 @@ import dev.apexnative.navigation.viewcontrollers.overlay.OverlayViews
38
39
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
39
40
  import dev.apexnative.navigation.viewcontrollers.stack.PreparedPushes
40
41
  import dev.apexnative.navigation.viewcontrollers.stack.StackCommands
42
+ import dev.apexnative.navigation.viewcontrollers.stack.StackController
41
43
  import dev.apexnative.navigation.viewcontrollers.stack.topbar.BarComponents
42
44
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
43
45
  import dev.apexnative.navigation.views.animations.FadeThrough
@@ -153,6 +155,15 @@ class Navigator(
153
155
  fun handleBack(listener: CommandListener): Boolean =
154
156
  modals.handleBack(listener) ?: (root?.handleBack(listener) == true)
155
157
 
158
+ /**
159
+ * The stack whose top [handleBack] would take away with the default pop: in the top modal, else in the root; none
160
+ * when back would do anything else.
161
+ */
162
+ internal fun stackPoppedByBack(): StackController? {
163
+ val modalRoot = modals.backTargetRoot()
164
+ return if (modalRoot != null) modalRoot.stackPoppedByBack() else root?.stackPoppedByBack()
165
+ }
166
+
156
167
  /**
157
168
  * [layer] may wait to show from now on ([WaitingLayer]): the nodes in it ask it through [isInWaitingLayer] and
158
169
  * [shownScreensChanged]. The root's layer is there from the start.
@@ -214,7 +225,7 @@ class Navigator(
214
225
  * it itself, `ComponentPresenter`); the system UI follows (only what changed is written).
215
226
  */
216
227
  internal fun optionsMerged(node: ViewController<*>, options: Options = Options()) {
217
- modals.optionsMergedInto(node)
228
+ modals.optionsMergedInto(node, options)
218
229
  overlays.optionsMergedInto(node)
219
230
  val navigationBar = options.navigationBar
220
231
  val padding = navigationBar.drawBehind.hasValue() || navigationBar.backgroundColor.hasValue()
@@ -264,9 +275,7 @@ class Navigator(
264
275
  if (!made) return answer.reject(ErrorTexts.notDoneYet("setRoot"))
265
276
  val steps = layout.modals.mapIndexed { index, node ->
266
277
  val last = index == layout.modals.lastIndex
267
- RootCoordinator.LayerStep { done ->
268
- modals.showInChain(node.id, { makeLayer(node, isOverlay = false) }, last, done)
269
- }
278
+ ModalLayerStep(node, last)
270
279
  } + layout.overlays.map { node ->
271
280
  RootCoordinator.LayerStep { done ->
272
281
  overlays.showInChain(node.id, { makeLayer(node, isOverlay = true) }, done)
@@ -275,6 +284,35 @@ class Navigator(
275
284
  roots.setRoot({ factory.create(layout.root) }, answer, steps)
276
285
  }
277
286
 
287
+ // A modal of a `setRoot`'s layout: made with the root and held until its turn, so it renders while the root waits
288
+ // and comes in; made in its turn instead while an id of it still shows (a modal of the old root with that id goes
289
+ // with the old root). A modal that couldn't be made is skipped in its turn.
290
+ private inner class ModalLayerStep(private val node: LayoutNode, private val animated: Boolean) :
291
+ RootCoordinator.LayerStep {
292
+ private var prepared = false
293
+ private var held: ModalRecord? = null
294
+
295
+ override fun prepare() {
296
+ if (LayoutChecks.firstShownId(listOf(node), ::isShownNow) { false } != null) return
297
+ prepared = true
298
+ held = modals.hold(node.id, { factory.create(node) }, animated)
299
+ }
300
+
301
+ override fun discard() {
302
+ held?.let(modals::drop)
303
+ held = null
304
+ }
305
+
306
+ override fun show(done: () -> Unit): () -> Unit {
307
+ val record = held
308
+ held = null
309
+ if (record != null) return modals.release(record, done)
310
+ if (!prepared) return modals.showInChain(node.id, { makeLayer(node, isOverlay = false) }, animated, done)
311
+ done()
312
+ return {}
313
+ }
314
+ }
315
+
278
316
  // A modal or an overlay of a `setRoot`'s layout, made as its turn comes: an id that shows by then fails it.
279
317
  private fun makeLayer(node: LayoutNode, isOverlay: Boolean): ViewController<*> {
280
318
  val shown = LayoutChecks.firstShownId(listOf(node), ::isShownNow) { isOverlay && isReplacedOverlay(node, it) }
@@ -36,7 +36,8 @@ import dev.apexnative.navigation.views.animations.AnimationRules
36
36
  * The modals over the old root ([ModalsOverRoot]): taken as the `setRoot` is accepted (commands don't find them), given
37
37
  * back when it ends without replacing the root, carried with the old root into its transition and destroyed with it,
38
38
  * without `ANN.ModalDismissed`. The modals and the overlays of the layout ([LayerStep]) show after the root, one by
39
- * one, before the answer: a newer `setRoot` stops them.
39
+ * one, before the answer: a newer `setRoot` stops them. Its modals are made with the root ([LayerStep.prepare]) and
40
+ * render unseen while it waits and comes in, so each shows in its turn without a wait of its own.
40
41
  *
41
42
  * The steps that come later (the wait's timers and readiness signals, the end of the transition, the end of a layer,
42
43
  * a stack of the waiting root that changed its top) run under no runner: each goes through one guard, and a failure
@@ -128,11 +129,20 @@ internal class RootCoordinator(
128
129
  }
129
130
 
130
131
  /**
131
- * A modal or an overlay of the layout of a `setRoot`, shown after its root: [show] makes it and shows it, and calls
132
- * its argument once it showed or went (a failure is logged and skipped); what it returns stops it while it waits —
133
- * a newer `setRoot`: it goes without a word.
132
+ * A modal or an overlay of the layout of a `setRoot`, shown after its root: [show] shows it (made then, unless
133
+ * [prepare] made it), and calls its argument once it showed or went (a failure is logged and skipped); what it
134
+ * returns stops it while it waits — a newer `setRoot`: it goes without a word.
134
135
  */
135
136
  fun interface LayerStep {
137
+ /**
138
+ * Right after the root is placed, before its wait: a step may make its layer now, so that it renders unseen
139
+ * while the root waits and comes in, and shows without a wait of its own in its turn.
140
+ */
141
+ fun prepare() = Unit
142
+
143
+ /** The `setRoot` ended before this step's turn: what [prepare] made goes without a word. */
144
+ fun discard() = Unit
145
+
136
146
  fun show(done: () -> Unit): () -> Unit
137
147
  }
138
148
 
@@ -221,6 +231,7 @@ internal class RootCoordinator(
221
231
  modals.rootAccepted()
222
232
  val waitForRender = animationsOf(root).enter.waitForRender
223
233
  this.pending = pending
234
+ layers.forEach { it.prepare() }
224
235
  firstRoot.rootSet(waitForRender)
225
236
  await(pending, waitForRender)
226
237
  }
@@ -367,6 +378,14 @@ internal class RootCoordinator(
367
378
  if (!done) pending.stopLayer = stop
368
379
  }
369
380
 
381
+ // The layers of [pending] not shown yet go: what they made ahead goes unseen, the others aren't made. Each on its
382
+ // own: one that fails doesn't keep the others.
383
+ private fun discardLayers(pending: Pending) {
384
+ val rest = pending.layers.drop(pending.nextLayer)
385
+ pending.nextLayer = pending.layers.size
386
+ rest.forEach { attempt { it.discard() } }
387
+ }
388
+
370
389
  // A new `setRoot` came while this one was on its way.
371
390
  private fun supersede() {
372
391
  val pending = pending ?: return
@@ -389,14 +408,15 @@ internal class RootCoordinator(
389
408
  pending.replaced = null
390
409
  modals.replacedRootGone()
391
410
  }
392
- // Its root is the current root: the modal or the overlay that waits now goes without a word, the others
393
- // aren't made; those shown stay over its root, and go with it when the new root replaces it.
411
+ // Its root is the current root: the modal or the overlay that waits now goes without a word, the others go
412
+ // unseen or aren't made; those shown stay over its root, and go with it when the new root replaces it.
394
413
  Phase.Layers -> {
395
414
  pending.stopLayer?.invoke()
396
415
  pending.stopLayer = null
397
416
  releaseState(pending)
398
417
  }
399
418
  }
419
+ discardLayers(pending)
400
420
  pending.answer.reject(ErrorTexts.setRootReplaced(pending.root.id))
401
421
  }
402
422
 
@@ -426,6 +446,7 @@ internal class RootCoordinator(
426
446
  pending.wait = null
427
447
  when (pending.phase) {
428
448
  Phase.Waiting -> {
449
+ discardLayers(pending)
429
450
  attempt { takeAway(pending.root) }
430
451
  attempt { modals.rootKept() }
431
452
  attempt { firstRoot.rootDestroyed() }
@@ -451,6 +472,7 @@ internal class RootCoordinator(
451
472
  pending.stopLayer = null
452
473
  }
453
474
  }
475
+ discardLayers(pending)
454
476
  // A root that is up takes the splash away, as at its show.
455
477
  if (pending.phase != Phase.Waiting) attempt { onRootShown() }
456
478
  attempt { releaseState(pending) }
@@ -482,6 +504,7 @@ internal class RootCoordinator(
482
504
  // The root was destroyed while it waited, by something other than a newer `setRoot` or the activity going.
483
505
  private fun rootGone(pending: Pending) {
484
506
  this.pending = null
507
+ discardLayers(pending)
485
508
  releaseState(pending)
486
509
  layer.remove(pending.root)
487
510
  modals.rootKept()
@@ -185,6 +185,8 @@ internal class OverlayCoordinator(
185
185
 
186
186
  private fun start(make: () -> ViewController<*>, answer: CommandAnswer?, chainDone: (() -> Unit)?): OverlayRecord {
187
187
  val root = make()
188
+ // Before the root's view is made: its screens are drawn under the status bar from their first layout.
189
+ root.shownAsOverlay = true
188
190
  val record = OverlayRecord(root)
189
191
  // A waiting layer before its views join the window: what is in it isn't on screen from its first layout.
190
192
  record.screensChanged = { screensChanged(record) }
@@ -31,9 +31,9 @@ internal object ContainerBackground {
31
31
  if (!colour.hasValue()) return null
32
32
  val color = colour.get(node.activity, Color.TRANSPARENT) ?: Color.TRANSPARENT
33
33
  // The screens' band rule (`ScreenSafeArea.backgroundBand`) for a layout, which keeps no room at its top: the
34
- // status bar's height, but none when it is hidden or the layout is drawn under it, in a side menu, a sheet or
35
- // a pair.
36
- val full = options.statusBar.isHiddenOrDrawBehind || node.isInSideMenu || node.isInSheet ||
34
+ // status bar's height, but none when it is hidden or the layout is drawn under it (in an overlay by default),
35
+ // in a side menu, a sheet or a pair.
36
+ val full = options.statusBar.isHiddenOrDrawBehind(node.isInOverlay) || node.isInSideMenu || node.isInSheet ||
37
37
  node.parentController is KeepsStatusBarRoom
38
38
  val band = if (full) 0 else SystemUiUtils.getStatusBarHeight(node.activity)
39
39
  if (band <= 0) return ColorDrawable(color)
@@ -8,6 +8,7 @@ import dev.apexnative.navigation.react.CommandListener
8
8
  import dev.apexnative.navigation.viewcontrollers.bottomtabs.TabBarMotion
9
9
  import dev.apexnative.navigation.viewcontrollers.child.ChildController
10
10
  import dev.apexnative.navigation.viewcontrollers.child.ChildControllersRegistry
11
+ import dev.apexnative.navigation.viewcontrollers.stack.StackController
11
12
  import dev.apexnative.navigation.viewcontrollers.stack.fab.FabPlacement
12
13
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
13
14
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
@@ -210,6 +211,8 @@ abstract class ParentController<V : ViewGroup>(
210
211
 
211
212
  override fun canHandleBack(): Boolean = getCurrentChild()?.canHandleBack() == true
212
213
 
214
+ override fun stackPoppedByBack(): StackController? = getCurrentChild()?.stackPoppedByBack()
215
+
213
216
  override fun destroy() {
214
217
  if (isDestroyed) return
215
218
  getChildControllers().forEach { it.destroy() }
@@ -16,6 +16,7 @@ import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
16
16
  import dev.apexnative.navigation.viewcontrollers.parent.ChildOffsetFromRest
17
17
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
18
18
  import dev.apexnative.navigation.viewcontrollers.sidemenu.SideMenuMotion.Side
19
+ import dev.apexnative.navigation.viewcontrollers.stack.StackController
19
20
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
20
21
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
21
22
  import dev.apexnative.navigation.views.sidemenu.SideMenuView
@@ -183,6 +184,9 @@ internal class SideMenuController(
183
184
 
184
185
  override fun canHandleBack(): Boolean = motion.canGoBack || center.canHandleBack()
185
186
 
187
+ // An open menu closes first: no preview of a pop then.
188
+ override fun stackPoppedByBack(): StackController? = if (motion.canGoBack) null else center.stackPoppedByBack()
189
+
186
190
  // A button's press addressed to the side menu goes to the center.
187
191
  override fun sendOnNavigationButtonPressed(buttonId: String) = center.sendOnNavigationButtonPressed(buttonId)
188
192
 
@@ -255,9 +259,7 @@ internal class SideMenuController(
255
259
  }
256
260
 
257
261
  // The system UI follows the screen the user sees now, at rest: the open menu's, the center's once a close
258
- // ends. The owner reads it before the next frame, when a menu that closed is hidden already. An open without
259
- // animation runs this inside `AppearanceDispatcher.withoutWillAppear`: what starts to appear from here would
260
- // send no WillAppear.
262
+ // ends. The owner reads it before the next frame, when a menu that closed is hidden already.
261
263
  override fun shownChanged() {
262
264
  navigator?.shownScreensChanged(this@SideMenuController)
263
265
  navigator?.systemUi?.invalidate()
@@ -16,8 +16,8 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
16
16
  * the drawer calls `onDrawerClosed` only for a drawer that had opened.
17
17
  * - The menu screen's events: WillAppear at the side's first movement, DidAppear when it opened, DidDisappear when it
18
18
  * rests closed; only while the side menu has appeared ([Host.appeared]) — otherwise they come with the side menu's
19
- * own appearance (its visible children). An open without animation sends DidAppear without WillAppear
20
- * (`AppearanceDispatcher.withoutWillAppear`).
19
+ * own appearance (its visible children). An open without animation sends both too, WillAppear first, as on iOS:
20
+ * the drawer jumps to its open offset before it calls the open.
21
21
  * - At each rest: one state update after the menu's events, the back callback, and the layers that follow the shown
22
22
  * screen.
23
23
  * - A drawer that doesn't call back (not laid out since it joined the window, or not in one: `DrawerLayout` only
@@ -246,11 +246,8 @@ internal class SideMenuMotion(
246
246
  private fun move(side: Side, open: Boolean, animate: Boolean) {
247
247
  val drawer = drawer()
248
248
  val silent = !drawer.callsBack
249
- val run = {
250
- if (open) drawer.open(side, animate) else drawer.close(side, animate)
251
- if (silent) record(side, open)
252
- }
253
- if (open && !animate && host.appeared) AppearanceDispatcher.withoutWillAppear(run) else run()
249
+ if (open) drawer.open(side, animate) else drawer.close(side, animate)
250
+ if (silent) record(side, open)
254
251
  }
255
252
 
256
253
  private fun record(side: Side, open: Boolean) {
@@ -18,15 +18,14 @@ import dev.apexnative.navigation.views.sidemenu.SideMenuView
18
18
  * `testID`s. Each moment applies its part, from the side menu's resolved options
19
19
  * (`SideMenuController.resolveShownOptions`: the current part's chain, the center's `sideMenu` over an open menu's) —
20
20
  * at creation everything with the heights, at every appearance of a screen inside everything but the heights, at a
21
- * merge the fields it names, at a repaint the background and the `testID`s. The locks of `enabled` and the command
21
+ * merge the fields it names (a width or a height at once, an open menu too), at a repaint the background and the
22
+ * `testID`s. The locks of `enabled` and the command
22
23
  * `visible` are the node's ([SideMenuMotion]). Main thread only.
23
24
  */
24
25
  internal class SideMenuPresenter(activity: Activity) : Presenter(activity, Options()) {
25
- /** The side menu's [view] is made, with [options], resolved: the heights, read only now. */
26
+ /** The side menu's [view] is made, with [options], resolved: the heights too, which only a merge changes after. */
26
27
  fun applyOnCreation(view: SideMenuView, options: Options) {
27
- for (side in Side.entries) {
28
- view.setPartHeight(side, SideMenuRules.height(SideMenuRules.sideOf(options.sideMenu, side), ::toPixels))
29
- }
28
+ applyHeights(view, options.sideMenu)
30
29
  applyWidths(view, options.sideMenu)
31
30
  applyBackground(view, options)
32
31
  applyTestIds(view, options.sideMenu)
@@ -46,14 +45,18 @@ internal class SideMenuPresenter(activity: Activity) : Presenter(activity, Optio
46
45
  /**
47
46
  * A merge reached the side menu, its own or through a screen inside (`mergeOptions` step 3): the fields [options]
48
47
  * name, with the values of the side menu's resolution now, so a screen off what shows changes nothing until it
49
- * appears. A width waits for the next appearance, a height for nothing.
48
+ * appears. A width or a height resizes its menu at once, open or closed, as on iOS.
50
49
  */
51
50
  override fun mergeOptions(viewController: ViewController<*>, options: Options) {
52
51
  val layout = viewController.peekView() as? SideMenuView ?: return
53
52
  val touchesBackground = SideMenuRules.touchesBackground(options)
54
53
  val touchesTestIds = SideMenuRules.touchesTestIds(options.sideMenu)
55
- if (!touchesBackground && !touchesTestIds) return
54
+ val touchesWidths = SideMenuRules.touchesWidths(options.sideMenu)
55
+ val touchesHeights = SideMenuRules.touchesHeights(options.sideMenu)
56
+ if (!touchesBackground && !touchesTestIds && !touchesWidths && !touchesHeights) return
56
57
  val shown = viewController.resolveShownOptions(DefaultOptions.current)
58
+ if (touchesWidths) applyWidths(layout, shown.sideMenu)
59
+ if (touchesHeights) applyHeights(layout, shown.sideMenu)
57
60
  if (touchesBackground) applyBackground(layout, shown)
58
61
  if (touchesTestIds) applyTestIds(layout, shown.sideMenu)
59
62
  }
@@ -71,6 +74,12 @@ internal class SideMenuPresenter(activity: Activity) : Presenter(activity, Optio
71
74
  }
72
75
  }
73
76
 
77
+ private fun applyHeights(view: SideMenuView, options: SideMenuOptions) {
78
+ for (side in Side.entries) {
79
+ view.setPartHeight(side, SideMenuRules.height(SideMenuRules.sideOf(options, side), ::toPixels))
80
+ }
81
+ }
82
+
74
83
  // `layout.backgroundColor` on the drawer, its whole height, under the status bar; unset leaves the view as it is,
75
84
  // `'NoColor'` clears it.
76
85
  private fun applyBackground(view: View, options: Options) {
@@ -54,6 +54,13 @@ internal object SideMenuRules {
54
54
  fun touchesTestIds(merge: SideMenuOptions): Boolean =
55
55
  merge.testID.hasValue() || merge.left.testID.hasValue() || merge.right.testID.hasValue()
56
56
 
57
+ /** A merge names `width` of a side: the menus take their widths again. */
58
+ fun touchesWidths(merge: SideMenuOptions): Boolean = merge.left.width.hasValue() || merge.right.width.hasValue()
59
+
60
+ /** A merge names `height` of a side: the menus take their heights again. */
61
+ fun touchesHeights(merge: SideMenuOptions): Boolean =
62
+ merge.left.height.hasValue() || merge.right.height.hasValue()
63
+
57
64
  /** A merge names the background of the side menu (`layout.backgroundColor`). */
58
65
  fun touchesBackground(merge: Options): Boolean = merge.layout.backgroundColor.hasValue()
59
66
 
@@ -64,7 +71,7 @@ internal object SideMenuRules {
64
71
  fun wantedWidth(options: SideMenuSideOptions, toPixels: (Double) -> Int): Int? =
65
72
  options.width.get(null)?.let { max(0, toPixels(it)) }
66
73
 
67
- /** The height of a side, in pixels: `height` (dp) by [toPixels], read at creation only; none: the whole height. */
74
+ /** The height of a side, in pixels: `height` (dp) by [toPixels]; none: the whole height. */
68
75
  fun height(options: SideMenuSideOptions, toPixels: (Double) -> Int): Int? =
69
76
  options.height.get(null)?.let { max(0, toPixels(it)) }
70
77
 
@@ -26,11 +26,12 @@ import dev.apexnative.navigation.viewcontrollers.navigator.Navigator
26
26
  import dev.apexnative.navigation.viewcontrollers.parent.ChildOffsetFromRest
27
27
  import dev.apexnative.navigation.viewcontrollers.parent.ParentController
28
28
  import dev.apexnative.navigation.viewcontrollers.parent.ReturnsScreenFromColumn
29
- import dev.apexnative.navigation.viewcontrollers.parent.ShowsColumns
30
29
  import dev.apexnative.navigation.viewcontrollers.parent.ShownChildren
30
+ import dev.apexnative.navigation.viewcontrollers.parent.ShowsColumns
31
31
  import dev.apexnative.navigation.viewcontrollers.stack.StackController
32
32
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
33
33
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
34
+ import kotlin.math.roundToInt
34
35
 
35
36
  /**
36
37
  * A split view: the primary pane beside the secondary one when the width takes both (a tablet, a phone in landscape).
@@ -365,6 +366,19 @@ class SplitViewController(
365
366
  return can || super.canHandleBack()
366
367
  }
367
368
 
369
+ // As handleBack: the pane whose back goes first decides; an open sliding pane that would close previews nothing.
370
+ override fun stackPoppedByBack(): StackController? {
371
+ if (!isViewCreated) return super.stackPoppedByBack()
372
+ val layout = getView()
373
+ return when {
374
+ layout.isCompact -> compact?.stackPoppedByBack()
375
+ isSliding && !layout.panes.isOpen -> primary.stackPoppedByBack()
376
+ isSliding -> if (secondary.canHandleBack()) secondary.stackPoppedByBack() else null
377
+ secondary.canHandleBack() -> secondary.stackPoppedByBack()
378
+ else -> primary.stackPoppedByBack()
379
+ }
380
+ }
381
+
368
382
  /**
369
383
  * How far the pane of [child] is from where it rests: the secondary pane sliding over the primary one rests over
370
384
  * it, closed or on its way. Zero for the other children.
@@ -489,7 +503,7 @@ class SplitViewController(
489
503
  root.mergeOptions(options)
490
504
  }
491
505
 
492
- private fun dp(value: Double) = UiUtils.dpToPx(activity, value.toFloat()).toInt()
506
+ private fun dp(value: Double) = UiUtils.dpToPx(activity, value.toFloat()).roundToInt()
493
507
 
494
508
  companion object {
495
509
  private const val PRIMARY = "primary"