@apexnative/navigation 1.0.0-alpha.2 → 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 (226) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/android/CLAUDE.md +38 -9
  3. package/android/CORE.md +11 -4
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +29 -1
  6. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  8. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  9. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  12. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/StatusBarOptions.kt +7 -3
  14. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  15. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
  16. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  19. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  20. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  23. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  24. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  25. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  26. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  27. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  28. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/SecondaryScreenPresenter.kt +13 -5
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +92 -19
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentPresenter.kt +28 -7
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenFrame.kt +1 -1
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalAnimations.kt +22 -24
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +79 -13
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalRecord.kt +21 -6
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +29 -12
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +22 -18
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +4 -0
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +45 -6
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +35 -9
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +10 -8
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ContainerBackground.kt +3 -3
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -0
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +5 -1
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +72 -16
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +287 -0
  53. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  54. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +164 -13
  55. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +26 -1
  56. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +136 -40
  57. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackTransitions.kt +26 -8
  58. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +107 -4
  59. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  60. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
  61. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  62. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +131 -9
  63. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +75 -21
  64. package/android/src/main/java/dev/apexnative/navigation/views/animations/Curves.kt +29 -11
  65. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  66. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +15 -0
  67. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +137 -1
  68. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  69. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +8 -1
  70. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +26 -4
  71. package/cli/doctor/checks/app.js +61 -13
  72. package/cli/migrate/native.js +18 -0
  73. package/cli/setup/source.js +2 -1
  74. package/cli/setup/steps/mainApplication.js +134 -19
  75. package/docs-md/architecture/transitions.md +12 -7
  76. package/docs-md/containers/modals.md +4 -2
  77. package/docs-md/containers/overlays.md +3 -1
  78. package/docs-md/containers/side-menu.md +4 -2
  79. package/docs-md/containers/tabs.md +2 -0
  80. package/docs-md/flows/forms-in-a-modal.md +1 -1
  81. package/docs-md/help/glossary.md +2 -2
  82. package/docs-md/help/troubleshooting.md +20 -3
  83. package/docs-md/index.json +12 -0
  84. package/docs-md/migration/from-react-native-navigation.md +8 -2
  85. package/docs-md/screen-ui/back-and-gestures.md +8 -2
  86. package/docs-md/screen-ui/system-ui.md +1 -1
  87. package/docs-md/start/installation.md +3 -1
  88. package/ios/CLAUDE.md +17 -5
  89. package/ios/CORE.md +13 -8
  90. package/ios/Contract/ANNCommandAnswer.mm +5 -20
  91. package/ios/Contract/ANNCommandContract.h +15 -0
  92. package/ios/Contract/ANNCommandContract.mm +78 -0
  93. package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
  94. package/ios/Core/ANNErrorHandler.h +1 -1
  95. package/ios/Core/ANNErrorHandler.mm +3 -1
  96. package/ios/Core/ANNEventEmitter.h +1 -1
  97. package/ios/Core/ANNEventEmitter.mm +2 -2
  98. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  99. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  100. package/ios/Core/ANNNavigationState.h +7 -3
  101. package/ios/Core/ANNNavigationState.mm +10 -5
  102. package/ios/Core/ANNNodeHelper.h +32 -12
  103. package/ios/Core/ANNNodeHelper.mm +100 -3
  104. package/ios/Core/ANNRenderWait.h +15 -1
  105. package/ios/Core/ANNRenderWait.mm +119 -17
  106. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  107. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  108. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  109. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  110. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  111. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  112. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  113. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  114. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  115. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  116. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  117. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  118. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  119. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  120. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  121. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  122. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  123. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  124. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  125. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  126. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  127. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  128. package/ios/Options/ANNOptionsCache.h +7 -6
  129. package/ios/Options/ANNSearchBarOptions.h +4 -3
  130. package/ios/Options/ANNUserChoice.h +57 -0
  131. package/ios/Options/ANNUserChoice.mm +44 -0
  132. package/ios/Options/Params/ANNImageSource.h +61 -0
  133. package/ios/Options/Params/ANNImageSource.mm +101 -0
  134. package/ios/Options/Params/ANNOptionImages.h +37 -0
  135. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  136. package/ios/Options/Params/Image.h +11 -2
  137. package/ios/Options/Params/Image.mm +15 -2
  138. package/ios/Options/Params/ImageParser.h +5 -5
  139. package/ios/Options/Params/ImageParser.mm +28 -46
  140. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  141. package/ios/RECIPES.md +11 -5
  142. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  143. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  144. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  145. package/ios/TopBar/ANNBarButtons.h +9 -5
  146. package/ios/TopBar/ANNBarButtons.mm +211 -63
  147. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  148. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  149. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  150. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  151. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  152. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  153. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  154. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  155. package/lib/module/ARCHITECTURE.md +18 -18
  156. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  157. package/lib/module/api/hooks/useNavigation.js +8 -3
  158. package/lib/module/api/hooks/useNavigation.js.map +1 -1
  159. package/lib/module/api/navigation.js.map +1 -1
  160. package/lib/module/engine/commands.js +9 -7
  161. package/lib/module/engine/commands.js.map +1 -1
  162. package/lib/module/engine/layout.js +13 -9
  163. package/lib/module/engine/layout.js.map +1 -1
  164. package/lib/module/engine/options.js +44 -11
  165. package/lib/module/engine/options.js.map +1 -1
  166. package/lib/module/engine/places.js +44 -7
  167. package/lib/module/engine/places.js.map +1 -1
  168. package/lib/module/engine/props.js +30 -1
  169. package/lib/module/engine/props.js.map +1 -1
  170. package/lib/module/engine/rememberedOptions.js +1 -1
  171. package/lib/module/engine/rememberedOptions.js.map +1 -1
  172. package/lib/module/engine/screens.js +22 -12
  173. package/lib/module/engine/screens.js.map +1 -1
  174. package/lib/module/interfaces/contract/commands.json +101 -0
  175. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  176. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  177. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  178. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  179. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  180. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  181. package/lib/typescript/api/navigation.d.ts +4 -1
  182. package/lib/typescript/api/navigation.d.ts.map +1 -1
  183. package/lib/typescript/engine/commands.d.ts.map +1 -1
  184. package/lib/typescript/engine/options.d.ts +5 -2
  185. package/lib/typescript/engine/options.d.ts.map +1 -1
  186. package/lib/typescript/engine/places.d.ts +5 -2
  187. package/lib/typescript/engine/places.d.ts.map +1 -1
  188. package/lib/typescript/engine/props.d.ts +8 -1
  189. package/lib/typescript/engine/props.d.ts.map +1 -1
  190. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  191. package/lib/typescript/engine/screens.d.ts.map +1 -1
  192. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  193. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  194. package/lib/typescript/interfaces/Events.d.ts +19 -10
  195. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  196. package/lib/typescript/interfaces/options/animations.d.ts +15 -7
  197. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  198. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  199. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  200. package/lib/typescript/interfaces/options/modals.d.ts +18 -13
  201. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  202. package/lib/typescript/interfaces/options/system.d.ts +6 -3
  203. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  204. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  205. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  206. package/package.json +1 -1
  207. package/src/ARCHITECTURE.md +18 -18
  208. package/src/adapters/NativeANNTurboModule.ts +5 -2
  209. package/src/api/events/navigationEvents.ts +1 -1
  210. package/src/api/hooks/useNavigation.ts +9 -4
  211. package/src/api/navigation.ts +4 -1
  212. package/src/engine/commands.ts +19 -7
  213. package/src/engine/layout.ts +16 -9
  214. package/src/engine/options.ts +39 -11
  215. package/src/engine/places.ts +56 -7
  216. package/src/engine/props.ts +30 -1
  217. package/src/engine/rememberedOptions.ts +4 -1
  218. package/src/engine/screens.tsx +23 -13
  219. package/src/interfaces/ComponentEvents.ts +2 -2
  220. package/src/interfaces/Events.ts +19 -10
  221. package/src/interfaces/contract/commands.json +101 -0
  222. package/src/interfaces/options/animations.ts +15 -7
  223. package/src/interfaces/options/buttons.ts +13 -5
  224. package/src/interfaces/options/modals.ts +18 -13
  225. package/src/interfaces/options/system.ts +6 -3
  226. package/src/interfaces/options/topBar.ts +22 -9
@@ -6,7 +6,6 @@ import android.animation.AnimatorSet
6
6
  import android.animation.ObjectAnimator
7
7
  import android.app.Activity
8
8
  import android.graphics.Color
9
- import android.graphics.drawable.Drawable
10
9
  import android.view.View
11
10
  import android.view.animation.LinearInterpolator
12
11
  import android.view.animation.PathInterpolator
@@ -20,8 +19,8 @@ import dev.apexnative.navigation.options.params.LayoutDirection
20
19
  import dev.apexnative.navigation.options.params.ThemeColour
21
20
  import dev.apexnative.navigation.options.parsers.TypefaceLoader
22
21
  import dev.apexnative.navigation.system.SystemUiOwner
23
- import dev.apexnative.navigation.utils.ImageLoader
24
- import dev.apexnative.navigation.utils.ImageLoadingListenerAdapter
22
+ import dev.apexnative.navigation.utils.ImageRequests
23
+ import dev.apexnative.navigation.utils.OptionImages
25
24
  import dev.apexnative.navigation.utils.TestIds
26
25
  import dev.apexnative.navigation.utils.UiUtils
27
26
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
@@ -67,8 +66,9 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
67
66
 
68
67
  private var layout: BottomTabsLayout? = null
69
68
  private val typefaces = TypefaceLoader(activity)
70
- private val icons = HashMap<String, Drawable>()
71
- private val applied = HashMap<Pair<Int, Class<*>>, TabItems.Call>()
69
+ // The icons of the items by item and kind (selected or not): those of the cache at once, the others as below.
70
+ private val icons = ImageRequests<Pair<Int, Boolean>>(OptionImages.source(activity))
71
+ private val applied = AppliedTabCalls(icons::key)
72
72
  private var motion: Animator? = null
73
73
  private var anyIcon = false
74
74
  private var testId: String? = null
@@ -100,25 +100,24 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
100
100
 
101
101
  /**
102
102
  * Runs [calls] on the bar's items; a call equal to the last of its kind for its item is skipped (an icon isn't
103
- * loaded and set again as a screen appears). The badge always goes: its default colors are read at each set.
103
+ * loaded and set again as a screen appears), except the badge and the font, an icon that didn't load and a `file:`
104
+ * icon whose file changed ([AppliedTabCalls]).
104
105
  */
105
106
  open fun run(calls: List<TabItems.Call>) {
106
107
  val bar = layout?.bar ?: return
107
108
  for (call in calls) {
108
109
  if (call is TabItems.Call.TestId && !itemTestIds) continue
109
- val key = call.index to call.javaClass
110
- if (call !is TabItems.Call.Badge && call !is TabItems.Call.Font && applied[key] == call) continue
111
- applied[key] = call
112
- run(bar, call)
110
+ val next = applied.next(call) ?: continue
111
+ run(bar, call, next)
113
112
  }
114
113
  }
115
114
 
116
- private fun run(bar: BottomTabs, call: TabItems.Call) {
115
+ private fun run(bar: BottomTabs, call: TabItems.Call, next: AppliedTabCalls.Next) {
117
116
  val index = call.index
118
117
  when (call) {
119
118
  is TabItems.Call.Title -> bar.setText(index, call.text)
120
- is TabItems.Call.Icon -> iconOf(call.uri)?.let { bar.setIcon(index, it) }
121
- is TabItems.Call.SelectedIcon -> iconOf(call.uri)?.let { bar.setSelectedIcon(index, it) }
119
+ is TabItems.Call.Icon -> icon(bar, call, call.uri, false, next)
120
+ is TabItems.Call.SelectedIcon -> icon(bar, call, call.uri, true, next)
122
121
  is TabItems.Call.Colors -> {
123
122
  bar.setIconActiveColor(index, call.iconActive)
124
123
  bar.setIconInactiveColor(index, call.iconInactive)
@@ -344,17 +343,19 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
344
343
 
345
344
  private fun colorOf(colour: ThemeColour): Int? = colour.get(activity, null)
346
345
 
347
- // Icons load as they apply and never throw: an icon that doesn't load leaves the item as it was.
348
- private fun iconOf(uri: String): Drawable? {
349
- icons[uri]?.let { return it.constantState?.newDrawable()?.mutate() ?: it }
350
- var loaded: Drawable? = null
351
- ImageLoader().loadIcon(activity, uri, object : ImageLoadingListenerAdapter() {
352
- override fun onComplete(drawable: Drawable) {
353
- loaded = drawable
354
- }
355
- })
356
- loaded?.let { icons[uri] = it }
357
- return loaded
346
+ // Icons never throw: one that doesn't load leaves the item without it (without its selected icon, the item shows
347
+ // its icon when selected). An item's first icon loads at once, its tab's first frame shows it; a change of it on
348
+ // the bar that shows loads off the main thread when it can, and the item takes it when ready, unless a newer icon
349
+ // of the item came first.
350
+ // The key of a `file:` icon was built for the comparison: the request takes it, its file read once.
351
+ private fun icon(bar: BottomTabs, call: TabItems.Call, uri: String, selected: Boolean, next: AppliedTabCalls.Next) {
352
+ val index = call.index
353
+ val key = next.image ?: icons.key(uri)
354
+ icons.request(index to selected, key, mayWait = next.isChange && bar.isAttachedToWindow) { icon ->
355
+ if (icon == null) applied.failed(call)
356
+ val shown = layout?.bar ?: return@request
357
+ if (selected) shown.setSelectedIcon(index, icon) else shown.setIcon(index, icon)
358
+ }
358
359
  }
359
360
 
360
361
  private companion object {
@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
6
6
  import android.graphics.drawable.Drawable
7
7
  import android.graphics.drawable.LayerDrawable
8
8
  import android.view.View
9
+ import com.google.android.material.color.MaterialColors
9
10
  import dev.apexnative.navigation.options.DefaultOptions
10
11
  import dev.apexnative.navigation.options.Options
11
12
  import dev.apexnative.navigation.options.params.ThemeColour
@@ -15,9 +16,9 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
15
16
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
16
17
 
17
18
  /**
18
- * The presenter of a screen, React or native: its background — the screen's view takes `layout.backgroundColor`, kept
19
- * out of the status bar's height where the status bar shows over it, and the React content takes
20
- * `layout.componentBackgroundColor` over it —, its room for the bars, and its safe area again on `mergeOptions`
19
+ * The presenter of a screen, React or native: its background — the screen's view takes `layout.backgroundColor` (a
20
+ * React screen without it the theme's background), kept out of the status bar's height where the status bar shows over
21
+ * it, and the React content takes `layout.componentBackgroundColor` over it —, its room for the bars, and its safe area again on `mergeOptions`
21
22
  * (`mergeOptions` step 4). It applies as the screen appears, on `mergeOptions` of what it covers, and again with new
22
23
  * default options or after a light/dark switch from the resolved options it is given, which it never writes. Each
23
24
  * appearance tells the activity's system UI owner, which follows the screen on top. Main thread only.
@@ -49,8 +50,8 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
49
50
  applyBackground(controller, options)
50
51
 
51
52
  /**
52
- * The colors of [screen] from its resolved [options]: `layout.backgroundColor` on the screen's view, none without
53
- * it (the window shows); `layout.componentBackgroundColor` on its React content.
53
+ * The colors of [screen] from its resolved [options]: `layout.backgroundColor` on the screen's view, without it the
54
+ * theme's background for a React screen ([defaultColorOf]); `layout.componentBackgroundColor` on its React content.
54
55
  */
55
56
  fun applyBackground(screen: ViewController<*>, options: Options) {
56
57
  val view = screen.peekView() ?: return
@@ -59,11 +60,11 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
59
60
  }
60
61
 
61
62
  private fun screenBackground(screen: ViewController<*>, options: Options): Drawable? {
62
- val color = colorOf(options.layout.backgroundColor) ?: return null
63
+ val color = colorOf(options.layout.backgroundColor) ?: defaultColorOf(screen) ?: return null
63
64
  val frame = frameOf(screen) ?: return ColorDrawable(color)
64
65
  val band = ScreenSafeArea.backgroundBand(
65
66
  statusBarHeight = frame.statusBarHeight,
66
- hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind,
67
+ hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind(screen.isInOverlay),
67
68
  inSideMenu = screen.isInSideMenu,
68
69
  inSheet = screen.isInSheet,
69
70
  inPair = frame.isInPair,
@@ -79,6 +80,15 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
79
80
  private fun colorOf(colour: ThemeColour): Int? =
80
81
  if (colour.hasValue()) colour.get(activity, Color.TRANSPARENT) ?: Color.TRANSPARENT else null
81
82
 
83
+ // A React screen is opaque without a color of its own, as iOS gives it the system background: the screen under it
84
+ // never shows through, also while a push slides it aside. The app theme's `android:colorBackground`, read now, so a
85
+ // light/dark switch takes the new mode's. A screen of a sheet shows the sheet's surface; a native screen paints its
86
+ // own view.
87
+ private fun defaultColorOf(screen: ViewController<*>): Int? {
88
+ if (screen !is ComponentViewController || screen.isInSheet) return null
89
+ return themeBackgroundOf(activity)
90
+ }
91
+
82
92
  private fun frameOf(screen: ViewController<*>): ScreenFrame? = when (screen) {
83
93
  is ComponentViewController -> screen.frame
84
94
  is ExternalComponentViewController -> screen.frame
@@ -87,4 +97,15 @@ open class ComponentPresenter(activity: Activity) : Presenter(activity, DefaultO
87
97
 
88
98
  // The view under the React content: the React root, or the container of a slot screen. A native screen has none.
89
99
  private fun contentOf(screen: ViewController<*>): View? = (screen as? ComponentViewController)?.content?.asView()
100
+
101
+ internal companion object {
102
+ /**
103
+ * The background of a React screen without `layout.backgroundColor`: `android:colorBackground` of [activity]'s
104
+ * theme now. None for an activity without a theme (the unit tests' fakes).
105
+ */
106
+ fun themeBackgroundOf(activity: Activity): Int? {
107
+ if (activity.theme == null) return null
108
+ return MaterialColors.getColor(activity, android.R.attr.colorBackground, Color.WHITE)
109
+ }
110
+ }
90
111
  }
@@ -4,7 +4,7 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
4
4
 
5
5
  /**
6
6
  * Whether a screen's content rendered, which a command waits for before the screen comes in: a new root (`setRoot`),
7
- * a push with `waitForRender: true`, the new top of a `setStackRoot`. A wait never listens to the readiness store
7
+ * a push (unless `waitForRender: false`), the new top of a `setStackRoot`. A wait never listens to the readiness store
8
8
  * itself: the content keeps the one listener of its id and tells every waiter. Main thread only.
9
9
  */
10
10
  internal interface Readiness {
@@ -16,8 +16,10 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
16
16
  * without a limit.
17
17
  *
18
18
  * What differs between its users are its values, never its steps: the readiness and the clock (fakes in the unit
19
- * tests), [countsUnknownContent], how a readiness signal is delivered ([deliver]: at once, or posted to the next turn
20
- * of the main loop), and [step], the guard the user runs every later step in (a timer, a signal). Main thread only.
19
+ * tests), [countsUnknownContent], how a readiness signal is delivered ([deliver]: at once, posted to the next turn of
20
+ * the main loop, or behind the calls of JavaScript), whether content ready at once is delivered too
21
+ * ([deliversReadyContent]), and [step], the guard the user runs every later step in (a timer, a signal). Main thread
22
+ * only.
21
23
  */
22
24
  internal class RenderWait(
23
25
  private val readiness: Readiness,
@@ -32,6 +34,12 @@ internal class RenderWait(
32
34
  private val countsUnknownContent: Boolean,
33
35
  /** How a readiness signal reaches the wait: at once by default; posted, when a signal may come inside a layout. */
34
36
  private val deliver: (() -> Unit) -> Unit = { it() },
37
+ /**
38
+ * Content ready when the wait starts (or retakes) still reaches its end through [deliver], within the limit that
39
+ * runs: what [deliver] waits behind (a push's: the calls JavaScript made in the commit of that content) comes
40
+ * first. Off: such content ends the wait at once.
41
+ */
42
+ private val deliversReadyContent: Boolean = false,
35
43
  /** Runs a later step of the wait (a timer, a signal): the user's guards and protection; as it is by default. */
36
44
  private val step: (() -> Unit) -> Unit = { it() },
37
45
  private val onEnd: () -> Unit,
@@ -70,7 +78,8 @@ internal class RenderWait(
70
78
 
71
79
  /**
72
80
  * Starts waiting for [screens], and for the content of [unseen] within its first [UNSEEN_LIMIT_MILLIS]; with all
73
- * of them ready (and their bars' components, when they count within the limit) it ends at once, before it returns.
81
+ * of them ready (and their bars' components, when they count within the limit) it ends at once, before it returns,
82
+ * or through [deliver] with [deliversReadyContent].
74
83
  */
75
84
  fun start(screens: List<ViewController<*>>, unseen: List<ViewController<*>> = emptyList()) {
76
85
  this.screens = screens
@@ -78,15 +87,18 @@ internal class RenderWait(
78
87
  unseenDone = unseen.isEmpty()
79
88
  waiting = true
80
89
  token = Any()
81
- // Content ready at once ends the wait, unless the bar's components count within a limit: it runs for them.
82
- if (contentReady() && !(bar == Bar.WithinLimit && limitMillis != null)) return contentDone()
90
+ // Content ready at once ends the wait, unless the bar's components count within a limit (it runs for them) or
91
+ // the content goes through [deliver] too.
92
+ val ready = contentReady()
93
+ if (ready && !deliversReadyContent && !(bar == Bar.WithinLimit && limitMillis != null)) return contentDone()
94
+ // The timers first: a [deliver] that runs at once may end the wait before this returns.
83
95
  cancelContentTimer = when {
84
96
  limitMillis != null -> scheduler.schedule(limitMillis) { step { atLimit() } }
85
97
  countsUnknownContent -> scheduler.schedule(UNKNOWN_CONTENT_MILLIS) { step { atUnknownContentLimit() } }
86
98
  else -> null
87
99
  }
88
100
  startUnseenLimit()
89
- await()
101
+ if (ready && deliversReadyContent) deliverReady() else await()
90
102
  }
91
103
 
92
104
  /**
@@ -98,7 +110,7 @@ internal class RenderWait(
98
110
  this.screens = screens
99
111
  this.unseen = unseen
100
112
  token = Any()
101
- await()
113
+ if (deliversReadyContent && contentReady()) deliverReady() else await()
102
114
  }
103
115
 
104
116
  /** Ends the wait without [onEnd]: its timers stop, and the signals still to come do nothing. */
@@ -129,6 +141,14 @@ internal class RenderWait(
129
141
  if (contentReady() && cancelBarPhase == null) contentDone()
130
142
  }
131
143
 
144
+ // The content is ready already: it goes once through [deliver] as a signal would, then on as [await] says.
145
+ private fun deliverReady() {
146
+ val token = token
147
+ deliver {
148
+ step { if (waiting && this.token === token) await() }
149
+ }
150
+ }
151
+
132
152
  // The content of every screen ready: on to the bars; else the signals of those not ready.
133
153
  private fun await() {
134
154
  if (contentReady()) return contentDone()
@@ -143,7 +143,7 @@ internal class ScreenFrame(
143
143
  val parent = node.parentController
144
144
  val statusBar = ScreenSafeArea.statusBarRoom(
145
145
  statusBarHeight = statusBarHeight,
146
- hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind,
146
+ hiddenOrDrawBehind = options.statusBar.isHiddenOrDrawBehind(node.isInOverlay),
147
147
  inSheet = node.isInSheet,
148
148
  inSideMenu = node.isInSideMenu,
149
149
  keptAround = parent?.keepsStatusBarRoomOf(node) == true,
@@ -1,40 +1,38 @@
1
1
  package dev.apexnative.navigation.viewcontrollers.modal
2
2
 
3
- import kotlin.math.PI
4
- import kotlin.math.cos
3
+ import dev.apexnative.navigation.views.animations.Curves
5
4
 
6
5
  /**
7
- * The default animations of a modal on Android, as values: it comes up from the bottom of the window by the window's
8
- * height (in a split screen or a window, the window's, not the display's) and fades in, [DURATION] with a deceleration
9
- * at the end; it goes down and fades out in the same time, accelerating and decelerating (the system's cosine, not the
10
- * option's `accelerateDecelerate`). A sheet moves only by the Material sheet's own motion. Pure arithmetic.
6
+ * The default animations of a modal over the whole screen on Android, as values: the system's motion of a window that
7
+ * opens over the one below it (`activity_translucent_open_enter` and `activity_translucent_close_exit` of the
8
+ * platform, which Android 16 plays for a translucent activity). The modal comes up from the bottom of the window by the
9
+ * window's height (in a split screen or a window, the window's, not the display's) in [SHOW_DURATION], slowing down at
10
+ * the end; it goes down in [DISMISS_DURATION], speeding up. It doesn't fade: the screen below is seen only around it,
11
+ * as under a translucent activity. A sheet moves only by the Material sheet's own motion. Pure arithmetic.
11
12
  */
12
13
  internal object ModalAnimations {
13
- /** How long the default show and the default dismissal last (ms). */
14
- const val DURATION = 300L
14
+ /** How long the default show lasts (ms): `activity_translucent_open_enter`. */
15
+ const val SHOW_DURATION = 300L
16
+
17
+ /** How long the default dismissal lasts (ms): `activity_translucent_close_exit`. */
18
+ const val DISMISS_DURATION = 250L
19
+
20
+ // `linear_out_slow_in` of the platform: the path (0, 0) (0, 0) (0.2, 1) (1, 1).
21
+ private val linearOutSlowIn = Curves.CubicBezier(0.0, 0.0, 0.2, 1.0)
15
22
 
16
23
  /** How far a modal comes up and goes down: the window's height ([windowHeight]), or the display's while it is 0. */
17
24
  fun distance(windowHeight: Int, displayHeight: Int): Float =
18
25
  (if (windowHeight > 0) windowHeight else displayHeight).toFloat()
19
26
 
20
- /** The curve of the show: a deceleration, 1 − (1 − p)². */
21
- fun decelerate(progress: Float): Float = 1f - (1f - progress) * (1f - progress)
27
+ /** The curve of the show: the platform's `linear_out_slow_in`, fast at first and slow at the end. */
28
+ fun linearOutSlowIn(progress: Float): Float = linearOutSlowIn.getInterpolation(progress)
22
29
 
23
- /** The curve of the dismissal: an acceleration then a deceleration, cos((p + 1)π) / 2 + 0.5. */
24
- fun accelerateDecelerate(progress: Float): Float = (cos((progress + 1) * PI) / 2 + 0.5).toFloat()
30
+ /** The curve of the dismissal: the platform's `accelerate_quad`, p². */
31
+ fun accelerate(progress: Float): Float = progress.coerceIn(0f, 1f).let { it * it }
25
32
 
26
- /** Where a modal is at [progress] (0 … 1, in time) of its show over [distance]: its translation and its alpha. */
27
- fun show(progress: Float, distance: Float): Frame {
28
- val value = decelerate(progress)
29
- return Frame(translationY = distance * (1f - value), alpha = value)
30
- }
33
+ /** Where a modal is at [progress] (0 … 1, in time) of its show over [distance]: its translation. */
34
+ fun show(progress: Float, distance: Float): Float = distance * (1f - linearOutSlowIn(progress))
31
35
 
32
36
  /** Where a modal is at [progress] of its dismissal over [distance], from where it rests. */
33
- fun dismiss(progress: Float, distance: Float): Frame {
34
- val value = accelerateDecelerate(progress)
35
- return Frame(translationY = distance * value, alpha = 1f - value)
36
- }
37
-
38
- /** A modal's place in its animation. */
39
- data class Frame(val translationY: Float, val alpha: Float)
37
+ fun dismiss(progress: Float, distance: Float): Float = distance * accelerate(progress)
40
38
  }
@@ -261,6 +261,54 @@ internal class ModalCoordinator(
261
261
  }
262
262
  }
263
263
 
264
+ /**
265
+ * A modal of a `setRoot`'s layout made with the root, before its turn: it joins the modals layer and renders
266
+ * unseen while the root waits and comes in, held — it doesn't show, and commands and back don't count it — until
267
+ * [release]; [animated] as in [showInChain]. A failure is logged, and nothing is made (null).
268
+ */
269
+ fun hold(id: String, make: () -> ViewController<*>, animated: Boolean): ModalRecord? = try {
270
+ start(make, null, ModalRecord.Chain(animated) {}, held = true)
271
+ } catch (e: Throwable) {
272
+ val reason = ErrorTexts.reasonOf(e)
273
+ host.log("[ApexNavigation] setRoot: ${ErrorTexts.couldNot("show the screen '$id'", reason)}")
274
+ null
275
+ }
276
+
277
+ /**
278
+ * The turn of a [hold]'s modal in its `setRoot`'s chain: it shows as soon as it rendered, as [showInChain]'s
279
+ * would, with [done] once it showed or went; at once when it went meanwhile. The function returned stops it while
280
+ * it waits.
281
+ */
282
+ fun release(record: ModalRecord, done: () -> Unit): () -> Unit {
283
+ if (!record.held || record.state != State.Waiting) {
284
+ done()
285
+ return {}
286
+ }
287
+ record.chain = ModalRecord.Chain(record.chain?.animated ?: true, done)
288
+ record.held = false
289
+ guarded(record) {
290
+ host.backChanged()
291
+ // Rendered while held: the targets of its elements are in the layer under it only now.
292
+ if (record.isReady && !record.targetsAwaited && awaitsTargets(record)) {
293
+ record.isReady = false
294
+ awaitTargets(record)
295
+ } else {
296
+ presentReady()
297
+ }
298
+ }
299
+ return {
300
+ record.chain = null
301
+ if (record.state == State.Waiting) cancel(record, null, reports = false)
302
+ }
303
+ }
304
+
305
+ /** A [hold]'s modal whose `setRoot` ended before its turn: it goes without a word. */
306
+ fun drop(record: ModalRecord) {
307
+ if (!record.held) return
308
+ record.chain = null
309
+ if (record.state == State.Waiting) cancel(record, null, reports = false)
310
+ }
311
+
264
312
  /**
265
313
  * `dismissModal` after its shared steps: the record of [componentId], Android's rule of the last modal without a
266
314
  * root, [options] merged into the modal's visible screen, the animation of the command's screen with its parents
@@ -279,6 +327,7 @@ internal class ModalCoordinator(
279
327
  State.ClosedBelow -> return answer.resolve(record.rootId)
280
328
  else -> Unit
281
329
  }
330
+ if (record.held) return answer.reject(ErrorTexts.screenNotShown(componentId))
282
331
  if (isLastWithoutRoot(record)) return answer.reject(ErrorTexts.modalWithoutRoot(componentId))
283
332
  options?.let { mergeInto(record, it) }
284
333
  val from = node?.takeUnless { it.isDestroyed } ?: record.root
@@ -293,7 +342,7 @@ internal class ModalCoordinator(
293
342
  * before anything changes, with the id of the top modal's visible screen.
294
343
  */
295
344
  fun dismissAll(options: Options?, unreadablePath: String?, answer: CommandAnswer) {
296
- val members = records.filter { it.batch == null && it.state in BATCHED }
345
+ val members = records.filter { it.batch == null && !it.held && it.state in BATCHED }
297
346
  if (members.isEmpty()) {
298
347
  // Nothing else to take: the second one answers with the running batch (N6), else at once.
299
348
  val running = batches.lastOrNull()
@@ -347,6 +396,12 @@ internal class ModalCoordinator(
347
396
  return true
348
397
  }
349
398
 
399
+ /**
400
+ * The root of the modal that back goes to first; none without one. A back gesture previews only a pop in it: its
401
+ * dismissal has a preview of its own nowhere yet.
402
+ */
403
+ fun backTargetRoot(): ViewController<*>? = backTarget()?.root
404
+
350
405
  /** The mirror of [handleBack], without acting; null without a modal to ask. */
351
406
  fun canHandleBack(): Boolean? {
352
407
  val record = backTarget() ?: return null
@@ -422,6 +477,8 @@ internal class ModalCoordinator(
422
477
  }
423
478
  batches.clear()
424
479
  for (record in records.toList()) {
480
+ // A modal of the new root's layout, made with it, stays: it shows over the new root in its turn.
481
+ if (record.held) continue
425
482
  if (record.state == State.Waiting) {
426
483
  cancel(record, null, reports = false)
427
484
  continue
@@ -432,7 +489,7 @@ internal class ModalCoordinator(
432
489
  views.carry(record, old)
433
490
  carried += record
434
491
  }
435
- records.clear()
492
+ records.retainAll { it.held }
436
493
  host.backChanged()
437
494
  }
438
495
 
@@ -482,7 +539,12 @@ internal class ModalCoordinator(
482
539
 
483
540
  // The show (3, 4).
484
541
 
485
- private fun start(make: () -> ViewController<*>, answer: CommandAnswer?, chain: ModalRecord.Chain?): ModalRecord {
542
+ private fun start(
543
+ make: () -> ViewController<*>,
544
+ answer: CommandAnswer?,
545
+ chain: ModalRecord.Chain?,
546
+ held: Boolean = false,
547
+ ): ModalRecord {
486
548
  val root = make()
487
549
  val record: ModalRecord
488
550
  val options: Options
@@ -492,6 +554,7 @@ internal class ModalCoordinator(
492
554
  val sheet = ModalRecord.isSheet(options)
493
555
  root.shownAsSheet = sheet
494
556
  record = ModalRecord(root, sheet)
557
+ record.held = held
495
558
  } catch (e: Throwable) {
496
559
  root.destroy()
497
560
  throw e
@@ -555,7 +618,7 @@ internal class ModalCoordinator(
555
618
  private fun ready(record: ModalRecord) {
556
619
  if (record.state != State.Waiting) return
557
620
  record.wait = null
558
- if (!record.targetsAwaited && awaitsTargets(record)) return awaitTargets(record)
621
+ if (!record.targetsAwaited && !record.held && awaitsTargets(record)) return awaitTargets(record)
559
622
  record.cancelLimit()
560
623
  record.isReady = true
561
624
  presentReady()
@@ -607,7 +670,7 @@ internal class ModalCoordinator(
607
670
  if (taking) return
608
671
  for (record in records.toList()) {
609
672
  if (record.state != State.Waiting) continue
610
- if (!record.isReady) return
673
+ if (record.held || !record.isReady) return
611
674
  present(record)
612
675
  }
613
676
  }
@@ -668,7 +731,7 @@ internal class ModalCoordinator(
668
731
  // without the events not sent yet; what waits for it rejects, the layer it took out comes back, and the modals that
669
732
  // wait go on.
670
733
  private fun dismissFailed(record: ModalRecord, e: Throwable) {
671
- val text = ErrorTexts.couldNot("dismiss the modal of the screen '${record.rootId}'", ErrorTexts.reasonOf(e))
734
+ val text = ErrorTexts.couldNot("dismiss the modal '${record.rootId}'", ErrorTexts.reasonOf(e))
672
735
  val listed = record in records
673
736
  val under = layerUnder(record)
674
737
  runCatching { record.stopAnimation?.invoke() }
@@ -772,8 +835,8 @@ internal class ModalCoordinator(
772
835
  State.Presenting -> record.waitingDismissals += dismissal
773
836
  State.Shown -> if (isTop(record)) beginDismissal(record, dismissal) else closeBelow(record, dismissal)
774
837
  State.Dismissing -> dismissal.answer?.let(record.answers::add)
775
- State.ClosedBelow -> dismissal.answer?.resolve(record.rootId)
776
- State.WithOldRoot, State.Gone -> dismissal.answer?.reject(ErrorTexts.screenNotShown(record.rootId))
838
+ // It went already, or goes with the old root: the dismissal just completes, as on iOS.
839
+ State.ClosedBelow, State.WithOldRoot, State.Gone -> dismissal.answer?.resolve(record.rootId)
777
840
  }
778
841
  }
779
842
 
@@ -787,7 +850,7 @@ internal class ModalCoordinator(
787
850
  record.state = State.Gone
788
851
  records.remove(record)
789
852
  views.remove(record)
790
- val screen = AppearanceDispatcher.visibleNode(record.root)
853
+ val screen = AppearanceDispatcher.modalScreen(record.root)
791
854
  record.showAnswer?.resolve(record.rootId)
792
855
  record.showAnswer = null
793
856
  record.chain?.let {
@@ -949,7 +1012,7 @@ internal class ModalCoordinator(
949
1012
  val wasOut = isOut(revealed)
950
1013
  val counted = members.filter { it.state != State.ClosedBelow && !it.reported }
951
1014
  batch.count = counted.size
952
- batch.screen = counted.lastOrNull()?.let { AppearanceDispatcher.visibleNode(it.root) }
1015
+ batch.screen = counted.lastOrNull()?.let { AppearanceDispatcher.modalScreen(it.root) }
953
1016
  // The top that leaves with its animation: one already leaving keeps its own.
954
1017
  val top = members.lastOrNull { it.state in ANIMATED_IN_BATCH }
955
1018
  batch.top = top
@@ -1156,13 +1219,16 @@ internal class ModalCoordinator(
1156
1219
 
1157
1220
  // A record is the top when no record above it waits, comes in or shows; one that leaves doesn't count.
1158
1221
  private fun isTop(record: ModalRecord): Boolean =
1159
- records.drop(records.indexOf(record) + 1).none { it.effective in TOP_STATES }
1222
+ records.drop(records.indexOf(record) + 1).none(::countsAsTop)
1160
1223
 
1161
1224
  // The top record, as the commands see it; none.
1162
- private fun topRecord(): ModalRecord? = records.lastOrNull { it.effective in TOP_STATES }
1225
+ private fun topRecord(): ModalRecord? = records.lastOrNull(::countsAsTop)
1163
1226
 
1164
1227
  // Back goes to the top modal that waits, comes in or shows.
1165
- private fun backTarget(): ModalRecord? = records.lastOrNull { it.effective in TOP_STATES }
1228
+ private fun backTarget(): ModalRecord? = records.lastOrNull(::countsAsTop)
1229
+
1230
+ // A record that waits, comes in or shows; a held one doesn't count before its turn.
1231
+ private fun countsAsTop(record: ModalRecord): Boolean = !record.held && record.effective in TOP_STATES
1166
1232
 
1167
1233
  // `hardwareBackButton.dismissModalOnPress: false` of the modal's options now.
1168
1234
  private fun holdsBack(record: ModalRecord): Boolean =
@@ -17,7 +17,10 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
17
17
  internal class ModalRecord(
18
18
  /** The top node of the modal's layout. */
19
19
  val root: ViewController<*>,
20
- /** The modal shows as a Material sheet (`modal.sheet`), decided before its root's view is made. */
20
+ /**
21
+ * The modal shows as a Material sheet (`pageSheet`, `formSheet`, `popover`, or `modal.sheet` without a style),
22
+ * decided before its root's view is made.
23
+ */
21
24
  val isSheet: Boolean,
22
25
  ) : WaitingLayer {
23
26
  /** Where the modal is. */
@@ -78,6 +81,12 @@ internal class ModalRecord(
78
81
  /** Its wait ended: it shows once every older waiting modal showed or went (N1). */
79
82
  var isReady = false
80
83
 
84
+ /**
85
+ * A modal of a `setRoot`'s layout made with the root, before its turn: it renders unseen while the root waits and
86
+ * comes in, and neither shows nor counts for commands or back until its step of the chain lets it go.
87
+ */
88
+ var held = false
89
+
81
90
  /** The wait for its screens while it waits. */
82
91
  var wait: RenderWait? = null
83
92
 
@@ -160,14 +169,20 @@ internal class ModalRecord(
160
169
  /** `modalPresentationStyle` values that keep the layer under in place. */
161
170
  private val OVER_STYLES = setOf("overFullScreen", "overCurrentContext")
162
171
 
163
- /** The styles a sheet shows with: unset, `pageSheet`, `formSheet`. */
164
- private val SHEET_STYLES = setOf(null, "pageSheet", "formSheet")
172
+ /**
173
+ * The styles that always show as a sheet: the sheets of iOS, and the popover, which an iPhone shows as a sheet
174
+ * too.
175
+ */
176
+ private val SHEET_STYLES = setOf("pageSheet", "formSheet", "popover")
165
177
 
166
178
  /**
167
- * Whether a modal with [options] (resolved, with the defaults) shows as a sheet: any `modal.sheet` field set,
168
- * and the style unset, `pageSheet` or `formSheet`.
179
+ * Whether a modal with [options] (resolved, with the defaults) shows as a sheet: its style is `pageSheet`,
180
+ * `formSheet` or `popover`, or it has none and any `modal.sheet` field is set.
169
181
  */
170
- fun isSheet(options: Options): Boolean = options.modal.sheet.hasValue && styleOf(options) in SHEET_STYLES
182
+ fun isSheet(options: Options): Boolean {
183
+ val style = styleOf(options)
184
+ return style in SHEET_STYLES || (style == null && options.modal.sheet.hasValue)
185
+ }
171
186
 
172
187
  /**
173
188
  * Whether a modal covers the layer under it at the end of its show: full screen, unless it is a sheet or