@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
@@ -172,19 +172,20 @@ internal class FabPresenter(
172
172
  alignStart = options.alignHorizontally.get("right") == "left"
173
173
  val source = source
174
174
  fab?.let { fab ->
175
- applyButton(fab, options)
175
+ applyButton(fab, options, mayWait = fab.drawable != null || options.visible.isFalse)
176
176
  TestIds.apply(fab, options.testID.takeIf { it.hasValue() }?.get())
177
177
  val id = shownId.orEmpty()
178
178
  fab.setOnClickListener { press(source, id) }
179
179
  }
180
180
  menu?.let { menu ->
181
- applyButton(menu.main, options)
181
+ applyButton(menu.main, options, mayWait = menu.main.drawable != null || options.visible.isFalse)
182
182
  TestIds.apply(menu.main, options.testID.takeIf { it.hasValue() }?.get())
183
183
  menu.scrimColor = options.scrimColor.get(activity, null)
184
184
  menu.setActions(options.actions.orEmpty().map { action ->
185
185
  val actionId = action.id.get("")
186
186
  Fab(activity, actionId).also { button ->
187
- applyButton(button, action)
187
+ // The actions show only while the menu is open.
188
+ applyButton(button, action, mayWait = !menu.isOpen)
188
189
  TestIds.apply(button, action.testID.takeIf { it.hasValue() }?.get())
189
190
  button.setOnClickListener {
190
191
  press(source, actionId)
@@ -205,9 +206,10 @@ internal class FabPresenter(
205
206
  place()
206
207
  }
207
208
 
208
- private fun applyButton(button: Fab, options: FabOptions) {
209
+ // [mayWait]: the button can take its icon when it is ready (it shows one, or it stays hidden), see [Fab.applyIcon].
210
+ private fun applyButton(button: Fab, options: FabOptions, mayWait: Boolean) {
209
211
  val icon = options.icon.takeIf { it.hasValue() }?.get()?.ifEmpty { null }
210
- if (icon != null) button.applyIcon(icon, options.iconColor)
212
+ if (icon != null) button.applyIcon(icon, options.iconColor, mayWait)
211
213
  applyColors(button, options)
212
214
  button.setMini(options.size.get("regular") == "mini")
213
215
  }
@@ -14,7 +14,6 @@ import androidx.core.view.WindowCompat
14
14
  import androidx.core.view.WindowInsetsCompat
15
15
  import com.facebook.react.ReactApplication
16
16
  import dev.apexnative.navigation.options.SearchBarOptions
17
- import dev.apexnative.navigation.options.params.Text
18
17
  import dev.apexnative.navigation.options.params.ThemeColour
19
18
  import dev.apexnative.navigation.react.events.EventEmitter
20
19
  import dev.apexnative.navigation.utils.dp
@@ -148,10 +147,11 @@ class StackSearchPresenter(
148
147
 
149
148
  private fun searchOf(screen: ViewController<*>): ScreenSearch =
150
149
  searches.getOrPut(screen.view) {
151
- // The search of a pair of screens is its primary's: its events name that screen, which keeps the scope.
150
+ // The search of a pair of screens is its primary's: its events name that screen, which keeps the scope in
151
+ // its user layer, where a merge that sends the same options again doesn't take it away.
152
152
  val owner = (screen as? OptionsScreen)?.optionsScreen ?: screen
153
153
  ScreenSearch(this, owner.id, styleContext(), loadIcon) { scope ->
154
- owner.changeOptions { it.topBar.searchBar.selectedScope = Text(scope) }
154
+ owner.changeChoices { it.pickScope(scope) }
155
155
  }
156
156
  }
157
157
 
@@ -1,16 +1,14 @@
1
1
  package dev.apexnative.navigation.viewcontrollers.stack.topbar
2
2
 
3
- import dev.apexnative.navigation.options.ButtonMenuOptions
4
- import dev.apexnative.navigation.options.ButtonOptions
5
3
  import dev.apexnative.navigation.options.Options
6
- import dev.apexnative.navigation.options.copyMenu
4
+ import dev.apexnative.navigation.options.UserChoices
7
5
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
8
6
 
9
7
  /**
10
- * The check of a single-selection menu the user picked: written through [ViewController.changeOptions] into the
11
- * screen's own menu, never into a menu other screens share (the stack's, the defaults'). A menu that came from
12
- * elsewhere is copied into the screen's own options first — for a button, its whole side, as lists of buttons replace
13
- * whole. The check stays with the screen until new options give the menu. Main thread only.
8
+ * The check of a single-selection menu the user picked: written into the screen's user layer
9
+ * ([ViewController.changeChoices]), never into its own options nor into a menu other screens share (the stack's, the
10
+ * defaults'). It goes over the menu as the screen's options resolve, by the button's id and the item's id, and stays
11
+ * until the app changes that menu's checks or takes the item away. Main thread only.
14
12
  */
15
13
  internal object MenuChecks {
16
14
  /**
@@ -19,54 +17,13 @@ internal object MenuChecks {
19
17
  */
20
18
  fun write(screen: ViewController<*>, resolved: Options, buttonId: String?, itemId: String) {
21
19
  val topBar = resolved.topBar
22
- if (buttonId == null) {
23
- val menu = topBar.overflowMenu ?: return
24
- if (!checks(menu, itemId)) return
25
- screen.changeOptions { own ->
26
- val copy = (own.topBar.overflowMenu ?: menu).copyMenu()
27
- check(copy, itemId)
28
- own.topBar.overflowMenu = copy
29
- }
30
- return
31
- }
32
- val left = topBar.buttons.left?.any { it.id.get(null) == buttonId } == true
33
- val list = (if (left) topBar.buttons.left else topBar.buttons.right) ?: return
34
- val menu = list.firstOrNull { it.id.get(null) == buttonId }?.menu ?: return
35
- if (!checks(menu, itemId)) return
36
- screen.changeOptions { own ->
37
- val ownList = (if (left) own.topBar.buttons.left else own.topBar.buttons.right)
38
- ?: list.map(ButtonOptions::copyWithMenu)
39
- val button = ownList.firstOrNull { it.id.get(null) == buttonId } ?: return@changeOptions
40
- val copy = (button.menu ?: menu).copyMenu()
41
- check(copy, itemId)
42
- button.menu = copy
43
- if (left) own.topBar.buttons.left = ownList else own.topBar.buttons.right = ownList
44
- }
45
- }
46
-
47
- /** Whether picking [itemId] moves a check: its level has `singleSelection`. */
48
- fun checks(menu: ButtonMenuOptions, itemId: String): Boolean = levelOf(menu.items, menu.singleSelection, itemId) == true
49
-
50
- /** Moves the check of [itemId]'s level to it, in [menu] itself. */
51
- fun check(menu: ButtonMenuOptions, itemId: String) {
52
- mark(menu.items, menu.singleSelection, itemId)
53
- }
54
-
55
- // Whether the level holding [itemId] has single selection; null when [itemId] isn't in [elements].
56
- private fun levelOf(elements: List<ButtonMenuOptions.Element>, single: Boolean, itemId: String): Boolean? {
57
- if (elements.any { it is ButtonMenuOptions.Item && it.id == itemId }) return single
58
- for (element in elements) {
59
- if (element !is ButtonMenuOptions.Submenu) continue
60
- levelOf(element.items, element.singleSelection, itemId)?.let { return it }
61
- }
62
- return null
63
- }
64
-
65
- private fun mark(elements: List<ButtonMenuOptions.Element>, single: Boolean, itemId: String): Boolean {
66
- if (elements.any { it is ButtonMenuOptions.Item && it.id == itemId }) {
67
- if (single) elements.forEach { if (it is ButtonMenuOptions.Item) it.checked = it.id == itemId }
68
- return true
69
- }
70
- return elements.any { it is ButtonMenuOptions.Submenu && mark(it.items, it.singleSelection, itemId) }
20
+ val menu = if (buttonId == null) {
21
+ topBar.overflowMenu
22
+ } else {
23
+ (topBar.buttons.left.orEmpty() + topBar.buttons.right.orEmpty()).firstOrNull { it.id.get(null) == buttonId }
24
+ ?.menu
25
+ } ?: return
26
+ if (!UserChoices.checks(menu, itemId)) return
27
+ screen.changeChoices { it.check(buttonId, menu, itemId) }
71
28
  }
72
29
  }
@@ -49,7 +49,9 @@ import dev.apexnative.navigation.viewcontrollers.stack.search.StackSearchPresent
49
49
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.IReactView
50
50
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ScrollEventListener
51
51
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
52
+ import dev.apexnative.navigation.views.animations.AnimationRules
52
53
  import dev.apexnative.navigation.views.animations.SideAnimator
54
+ import dev.apexnative.navigation.views.animations.FadeThrough
53
55
  import dev.apexnative.navigation.views.animations.SystemTransition
54
56
  import dev.apexnative.navigation.views.component.ComponentLayout
55
57
  import dev.apexnative.navigation.views.stack.StackLayout
@@ -124,8 +126,13 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
124
126
  private var largeTitleShown = false
125
127
  private var followed: ScrollEventListener? = null
126
128
  private var rowTarget: RowTarget? = null
129
+ // The large title opening with a push from a screen without one; none at rest.
130
+ private var largeTitleOpening: ValueAnimator? = null
127
131
  // The picture of the part under the row in a leaving screen's overlay: the screen and the picture.
128
132
  private var underRow: Pair<ViewController<*>, Drawable>? = null
133
+ // The bar goes with the screens of a transition of the stack's own, and the screen that leaves with its picture.
134
+ private var travelling = false
135
+ private var travelLeaving: ViewController<*>? = null
129
136
 
130
137
  private enum class Part { Look, Title, Left, Right, Back, LargeTitle, Visible, TestId }
131
138
 
@@ -161,12 +168,16 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
161
168
  * The stack's current child changes to [incoming] ([motion]: the default transition that brings it, none at once):
162
169
  * the row's picture is taken before the bar takes the new screen's options. A push from a large title to a screen
163
170
  * without one makes the bar lower at once: the part under the row rides with the leaving screen as a picture in its
164
- * overlay, and the row's picture holds the row only. So does a pop back to a screen whose large title is collapsed.
171
+ * overlay, and the row's picture holds the row only. So does a pop or a fade through (`setStackRoot`) to a screen
172
+ * without a large title or with its large title collapsed: a picture as tall as the large title would hold the bar
173
+ * at its height to the end. With a transition of the stack's own ([travel]) the bar goes with the screens instead
174
+ * ([beginTravel]).
165
175
  */
166
176
  fun beginRowTransition(
167
177
  motion: BarMotion?,
168
178
  incoming: ViewController<*>?,
169
179
  visibility: StackDisplay.BarVisibility = StackDisplay.BarVisibility.AT_ONCE,
180
+ travel: StackDisplay.BarTravel? = null,
170
181
  ) {
171
182
  this.motion = motion
172
183
  this.visibilityChange = visibility
@@ -179,14 +190,68 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
179
190
  rowTarget?.withLargeTitle = !lowers
180
191
  if (lowers) pictureUnderRow(bar, leaving)
181
192
  rowTransition?.begin(motion)
193
+ endTravel()
194
+ if (travel != null && bar != null && incoming != null) beginTravel(bar, leaving, incoming, travel, visibility)
182
195
  }
183
196
 
184
197
  /** The transition that brought the current child ended: the row at rest, the picture under it gone. */
185
198
  fun endRowTransition() {
199
+ // A large title still opening is open now.
200
+ largeTitleOpening?.let {
201
+ largeTitleOpening = null
202
+ it.end()
203
+ }
186
204
  motion = null
187
205
  visibilityChange = StackDisplay.BarVisibility.AT_ONCE
188
206
  rowTransition?.end()
189
207
  dropPictureUnderRow()
208
+ endTravel()
209
+ }
210
+
211
+ /**
212
+ * The bar goes with the screens of a transition of the stack's own, as each screen's bar would: a picture of the
213
+ * bar as it is goes with [leaving], and the bar, which takes [incoming]'s options next, is drawn flat as [incoming]
214
+ * moves — under [leaving] on a pop ([StackDisplay.BarTravel.Back]) (`StackLayout.barTravels`). No picture when the
215
+ * bar doesn't show. Nothing travels when the new top hides the bar by a side of its own
216
+ * (`animations.*.topBar.exit`), which plays instead, or when the bar can't be drawn in software (a `HARDWARE`
217
+ * bitmap in it): the bar takes the options in its place.
218
+ */
219
+ private fun beginTravel(
220
+ bar: TopBar,
221
+ leaving: ViewController<*>?,
222
+ incoming: ViewController<*>,
223
+ direction: StackDisplay.BarTravel,
224
+ visibility: StackDisplay.BarVisibility,
225
+ ) {
226
+ val layout = layout ?: return
227
+ val screen = incoming.peekView()?.takeIf(layout::holds) ?: return
228
+ val leavingView = leaving?.peekView()?.takeIf(layout::holds)
229
+ // The new top hides the bar by a side of its own: the side plays on the bar in its place instead.
230
+ if (host.resolvedOf(incoming).topBar.visible.isFalse && AnimationRules.barSideGiven(visibility.side?.exit)) return
231
+ val shows = bar.visibility == View.VISIBLE && this.visibility?.isHidden != true
232
+ val picture = if (shows && leavingView != null) pictureOfBar(bar) ?: return else null
233
+ travelling = true
234
+ travelLeaving = leaving.takeIf { picture != null }
235
+ // Flat: with an elevation the renderer would draw the bar over every screen and out of its alpha layer.
236
+ bar.isFlat = true
237
+ layout.barTravels(screen, leavingView, picture, direction == StackDisplay.BarTravel.Back)
238
+ }
239
+
240
+ // The bar's picture where the bar is in the stack, its moves counted.
241
+ private fun pictureOfBar(bar: TopBar): Drawable? {
242
+ val bitmap = bar.picture() ?: return null
243
+ val left = (bar.left + bar.translationX).roundToInt()
244
+ val top = (bar.top + bar.translationY).roundToInt()
245
+ return BitmapDrawable(bar.resources, bitmap).apply { setBounds(left, top, left + bitmap.width, top + bitmap.height) }
246
+ }
247
+
248
+ // The bar's travel ends: the picture goes, the bar is drawn in its place again with its shadow.
249
+ private fun endTravel() {
250
+ if (!travelling) return
251
+ travelling = false
252
+ travelLeaving = null
253
+ bar?.isFlat = false
254
+ layout?.endBarTravel()
190
255
  }
191
256
 
192
257
  /**
@@ -205,8 +270,9 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
205
270
  keys.clear()
206
271
  }
207
272
  shownEntry = entry
208
- // The screen that left with the picture under the row shows again: the picture goes.
273
+ // The screen that left with the picture under the row, or of the bar, shows again: the picture goes.
209
274
  if (underRow?.first === entry) dropPictureUnderRow()
275
+ if (travelLeaving === entry) endTravel()
210
276
  val options = host.resolvedOf(entry)
211
277
  applyAll(bar, entry, screen, options, merge = false)
212
278
  }
@@ -334,7 +400,9 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
334
400
  /** The stack is destroyed: every component stops. */
335
401
  fun destroy() {
336
402
  rowTransition?.end()
403
+ endTravel()
337
404
  largeTitleFollower.stop()
405
+ stopLargeTitleOpening()
338
406
  collapse?.disableCollapse()
339
407
  components.destroyAll()
340
408
  cells.clear()
@@ -370,6 +438,9 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
370
438
  )
371
439
  }
372
440
 
441
+ /** Whether the bar shows [entry]: its parts are applied, so its place in the layout is [entry]'s. */
442
+ fun shows(entry: ViewController<*>): Boolean = entry === shownEntry
443
+
373
444
  /** The height of the search under the row of [entry] when it is the screen the bar shows; 0 otherwise. */
374
445
  fun searchHeightOf(entry: ViewController<*>): Int =
375
446
  if (entry === shownEntry) bar?.searchAreaHeight ?: 0 else 0
@@ -633,11 +704,13 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
633
704
  }
634
705
 
635
706
  // Whether [incoming] shows less of the bar under the row than the large title now: a push to a screen without a
636
- // large title, or a pop back to one whose large title is collapsed. The lower bar would cut the row's picture.
707
+ // large title, or a pop or a fade through to one without a large title or with its large title collapsed. The lower
708
+ // bar would cut the row's picture.
637
709
  private fun lowersBar(motion: BarMotion?, incoming: ViewController<*>, options: Options): Boolean = when (motion) {
638
710
  BarMotion.Push -> !largeTitleShownFor(incoming, options)
639
- BarMotion.Pop -> largeTitleShownFor(incoming, options) && collapseOf(incoming, options) > 0f
640
- else -> false
711
+ BarMotion.Pop, BarMotion.FadeThrough ->
712
+ !largeTitleShownFor(incoming, options) || collapseOf(incoming, options) > 0f
713
+ null -> false
641
714
  }
642
715
 
643
716
  // How far [entry]'s large title is collapsed as it shows: by the offset its list had last, unless it doesn't follow
@@ -675,6 +748,11 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
675
748
  large.fontSize.get(null), fontKey(large.font), large.collapseOnScroll.get(null), testId).joinToString()
676
749
  if (keys[Part.LargeTitle] == key) return
677
750
  keys[Part.LargeTitle] = key
751
+ // The default push or fade through from a screen without a large title opens it with the screens, on a bar that
752
+ // shows at rest: the bar's show and its hide on scroll move the same translation.
753
+ val opening = motion.takeIf { it == BarMotion.Push || it == BarMotion.FadeThrough }
754
+ val opens = shown && !largeTitleShown && bar.visibility == View.VISIBLE && bar.translationY == 0f
755
+ stopLargeTitleOpening()
678
756
  largeTitleShown = shown
679
757
  bar.setLargeTitleShown(shown)
680
758
  largeTitleFollower.stop()
@@ -692,9 +770,39 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
692
770
  // same frame: back to the screen, after light or dark changed, after a merge.
693
771
  if (large.collapseOnScroll.isFalse) scrollOf(entry)?.forgetOffset()
694
772
  largeTitleFollower.collapse(collapseOf(entry, options))
773
+ if (opens) opening?.let { openLargeTitle(bar, collapseOf(entry, options), it) }
695
774
  if (!large.collapseOnScroll.isFalse) scrollOf(entry)?.let(largeTitleFollower::follow)
696
775
  }
697
776
 
777
+ // The bar grows with the screens' [motion], on its curve and in its time, from the row's height to the large title
778
+ // as the screen's list left it: the large title comes out from under the row as it does when the list scrolls
779
+ // back. No frame shows the tall bar before the screens move.
780
+ private fun openLargeTitle(bar: TopBar, offset: Float, motion: BarMotion) {
781
+ val range = UiUtils.dpToPx(bar.context, LargeTitleView.HEIGHT)
782
+ val to = LargeTitleCollapse.shift(offset, range)
783
+ if (to >= range) return
784
+ bar.setLargeTitleCollapse(range.toInt())
785
+ largeTitleOpening = ValueAnimator.ofFloat(range, to).apply {
786
+ duration = if (motion == BarMotion.FadeThrough) FadeThrough.DURATION else SystemTransition.DURATION
787
+ interpolator = if (motion == BarMotion.FadeThrough) FadeThrough.CURVE else SystemTransition.CURVE
788
+ addUpdateListener { bar.setLargeTitleCollapse((it.animatedValue as Float).toInt()) }
789
+ addListener(object : AnimatorListenerAdapter() {
790
+ override fun onAnimationEnd(animation: Animator) {
791
+ if (largeTitleOpening === animation) largeTitleOpening = null
792
+ }
793
+ })
794
+ start()
795
+ }
796
+ }
797
+
798
+ // Stopped where it is: what applies next sets the large title.
799
+ private fun stopLargeTitleOpening() {
800
+ val opening = largeTitleOpening ?: return
801
+ largeTitleOpening = null
802
+ opening.removeAllUpdateListeners()
803
+ opening.cancel()
804
+ }
805
+
698
806
  // Shown or hidden: with the transition by its side, or by `animate` in a merge.
699
807
  private fun applyVisibility(options: Options, merge: Boolean) {
700
808
  val visible = !options.topBar.visible.isFalse
@@ -702,7 +810,10 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
702
810
  if (keys[Part.Visible] == key) return
703
811
  keys[Part.Visible] = key
704
812
  val change = visibilityChange
705
- val animated = !options.topBar.animate.isFalse && (merge || change.animated)
813
+ // While the bar goes with the screens it shows or hides at once, unless its side of its own plays.
814
+ val travels = !merge && travelling &&
815
+ !AnimationRules.barSideGiven(if (visible) change.side?.enter else change.side?.exit)
816
+ val animated = !options.topBar.animate.isFalse && (merge || change.animated) && !travels
706
817
  visibility?.apply(visible, animated, if (merge) null else change.side)
707
818
  }
708
819
 
@@ -897,8 +1008,16 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
897
1008
  button.priority.get(null),
898
1009
  ).joinToString("|")
899
1010
 
900
- private fun menuKey(menu: ButtonMenuOptions): String =
901
- menu.items.joinToString("|") { "${it.title}:${it.icon}:${it.testID}" } + menu.testID
1011
+ // What the overflow menu shows, its submenus and checks with it: a check the app moves, or one of the user layer
1012
+ // that leaves, fills the menu again.
1013
+ private fun menuKey(menu: ButtonMenuOptions): String = elementsKey(menu.items) + menu.testID
1014
+
1015
+ private fun elementsKey(elements: List<ButtonMenuOptions.Element>): String = elements.joinToString("|") {
1016
+ "${it.title}:${it.icon}:${it.testID}" + when (it) {
1017
+ is ButtonMenuOptions.Item -> ":${it.checked}:${it.disabled}"
1018
+ is ButtonMenuOptions.Submenu -> "[${elementsKey(it.items)}]"
1019
+ }
1020
+ }
902
1021
 
903
1022
  private fun fontKey(font: FontOptions): String =
904
1023
  listOf(font.fontFamily, font.fontStyle, font.fontWeight).joinToString { it.get(null).toString() }
@@ -960,8 +1079,11 @@ internal class TopBarPresenter(private val activity: Activity, private val host:
960
1079
  if (large) View.IMPORTANT_FOR_ACCESSIBILITY_NO else View.IMPORTANT_FOR_ACCESSIBILITY_YES
961
1080
  }
962
1081
 
1082
+ // The list scrolled while the large title opens: it follows the list from there.
963
1083
  override fun onScrollOffset(offsetY: Float) {
964
- if (largeTitleShown) collapse(offsetY)
1084
+ if (!largeTitleShown) return
1085
+ stopLargeTitleOpening()
1086
+ collapse(offsetY)
965
1087
  }
966
1088
 
967
1089
  override fun onScrollUp(nextTranslation: Float) = Unit
@@ -6,6 +6,7 @@ import android.view.ViewGroup
6
6
  import dev.apexnative.navigation.options.DefaultOptions
7
7
  import dev.apexnative.navigation.options.Options
8
8
  import dev.apexnative.navigation.options.OptionsCache
9
+ import dev.apexnative.navigation.options.UserChoices
9
10
  import dev.apexnative.navigation.options.params.Bool
10
11
  import dev.apexnative.navigation.options.params.NullBool
11
12
  import dev.apexnative.navigation.react.CommandListener
@@ -30,11 +31,11 @@ abstract class ViewController<V : ViewGroup>(
30
31
  var initialOptions: Options,
31
32
  ) {
32
33
  /**
33
- * The node's own options: those it was made with, with every `mergeOptions` and every user's choice over them.
34
- * Writing them is one of the two places the options change ([OptionsCache]): the setter takes away the one-shot
35
- * options, keeps the object (a stripped copy when it has a one-shot option: the caller's object never changes), and
36
- * makes every resolved result stale. Don't change the object in place: [changeOptions] or [mergeOptions] write a
37
- * changed copy.
34
+ * The node's own options: those it was made with, with every `mergeOptions` over them; what the user chose in the
35
+ * native UI is not here but in [choices]. Writing them is one of the places the options change ([OptionsCache]):
36
+ * the setter takes away the one-shot options, keeps the object (a stripped copy when it has a one-shot option: the
37
+ * caller's object never changes), and makes every resolved result stale. Don't change the object in place:
38
+ * [changeOptions] or [mergeOptions] write a changed copy.
38
39
  */
39
40
  var options: Options = initialOptions.copy().also(Options::clearOneShotOptions)
40
41
  set(value) {
@@ -42,6 +43,10 @@ abstract class ViewController<V : ViewGroup>(
42
43
  OptionsCache.optionsChanged()
43
44
  }
44
45
 
46
+ // The user layer: what the user chose in the native UI (a menu's check, a search scope), over the node's own
47
+ // options as they resolve ([changeChoices]).
48
+ private val choices = UserChoices()
49
+
45
50
  /**
46
51
  * The JavaScript the node was made for (`ReloadCoordinator`): the events of the node and its screens reach only
47
52
  * that one.
@@ -94,6 +99,19 @@ abstract class ViewController<V : ViewGroup>(
94
99
  open val isInSheet: Boolean
95
100
  get() = shownAsSheet || parent?.isInSheet == true
96
101
 
102
+ /**
103
+ * The node is the root of an overlay: the overlays set it before the node's view is made, so its screens are drawn
104
+ * under the status bar from their first layout (`StatusBarOptions.isHiddenOrDrawBehind`).
105
+ */
106
+ internal var shownAsOverlay = false
107
+
108
+ /**
109
+ * The node is in an overlay: it covers the whole window, and is drawn under the status bar by default. It holds from
110
+ * the start of the show, while `OverlayCoordinator.isInOverlay` answers only for the overlays it lists.
111
+ */
112
+ open val isInOverlay: Boolean
113
+ get() = shownAsOverlay || parent?.isInOverlay == true
114
+
97
115
  /**
98
116
  * The node is in a side of a side menu (its left or right menu, not its center): the side goes the whole height of
99
117
  * the window, and the node keeps no room for the status bar. The layout around answers for it
@@ -143,9 +161,9 @@ abstract class ViewController<V : ViewGroup>(
143
161
  }
144
162
 
145
163
  /**
146
- * The node's options over those of the layouts around it, without the default options: the parents' resolution,
147
- * then the node's own. A new object at each call, which the caller may change. For a layout of nodes this is not
148
- * what it shows with: that is [resolveShownOptions].
164
+ * The node's options over those of the layouts around it, without the default options and without the user layer
165
+ * ([changeChoices]): the parents' resolution, then the node's own. A new object at each call, which the caller may
166
+ * change. For a layout of nodes this is not what it shows with: that is [resolveShownOptions].
149
167
  */
150
168
  open fun resolveCurrentOptions(): Options {
151
169
  val resolved = parent?.resolveChildOptions(this) ?: Options()
@@ -153,21 +171,21 @@ abstract class ViewController<V : ViewGroup>(
153
171
  }
154
172
 
155
173
  /**
156
- * The node's options over those of the layouts around it and [defaultOptions]: what it shows with. With the app's
157
- * default options ([DefaultOptions.current]) the result is kept and given again while no options changed
158
- * ([OptionsCache]) and the layouts around the node and its current children are the same: it is read-only, whoever
159
- * changes it copies it first. Main thread only.
174
+ * The node's options over those of the layouts around it and [defaultOptions], with the user layer over them
175
+ * ([changeChoices]): what it shows with. With the app's default options ([DefaultOptions.current]) the result is
176
+ * kept and given again while no options changed ([OptionsCache]) and the layouts around the node and its current
177
+ * children are the same: it is read-only, whoever changes it copies it first. Main thread only.
160
178
  *
161
179
  * For a layout of nodes (a stack, tabs, a side menu) it is the layout's own options over those around it, without
162
180
  * any child's: not what the layout shows with, which [resolveShownOptions] gives.
163
181
  */
164
182
  open fun resolveCurrentOptions(defaultOptions: Options): Options {
165
- if (defaultOptions !== DefaultOptions.current) return resolveCurrentOptions().withDefaultOptions(defaultOptions)
183
+ if (defaultOptions !== DefaultOptions.current) return withChoices(defaultOptions)
166
184
  val generation = OptionsCache.generation
167
185
  val along = resolvedAlong()
168
186
  val kept = resolved
169
187
  if (kept != null && resolvedGeneration == generation && sameNodes(along, resolvedAlong)) return kept
170
- val result = resolveCurrentOptions().withDefaultOptions(defaultOptions)
188
+ val result = withChoices(defaultOptions)
171
189
  resolved = result
172
190
  resolvedGeneration = generation
173
191
  resolvedAlong = along.map(::WeakReference)
@@ -183,6 +201,10 @@ abstract class ViewController<V : ViewGroup>(
183
201
  */
184
202
  open fun resolveShownOptions(defaultOptions: Options): Options = resolveCurrentOptions(defaultOptions)
185
203
 
204
+ // The resolution in its order: the defaults, the parents, the node's own options, then the user layer over them.
205
+ private fun withChoices(defaultOptions: Options): Options =
206
+ choices.applyTo(resolveCurrentOptions().withDefaultOptions(defaultOptions))
207
+
186
208
  // The layouts around this node, nearest first, then its current children down the visible chain.
187
209
  private fun resolvedAlong(): List<ViewController<*>> {
188
210
  val nodes = ArrayList<ViewController<*>>()
@@ -203,9 +225,9 @@ abstract class ViewController<V : ViewGroup>(
203
225
  nodes.size == kept.size && nodes.indices.all { nodes[it] === kept[it].get() }
204
226
 
205
227
  /**
206
- * Changes the node's own options: [change] gets a copy, which is stored ([options]). A presenter or a module that
207
- * writes a user's choice into the options (a menu's checkmark, a search scope, tokens, a toggled button) does it
208
- * this way; a button menu keeps its own shared `checked` (`ButtonMenuOptions`).
228
+ * Changes the node's own options: [change] gets a copy, which is stored ([options]). It is for the library's own
229
+ * bookkeeping of a merge (a side color written into the node's buttons, the back button's visibility); what the
230
+ * user chose goes through [changeChoices].
209
231
  */
210
232
  fun changeOptions(change: (Options) -> Unit) {
211
233
  val copy = options.copy()
@@ -213,14 +235,40 @@ abstract class ViewController<V : ViewGroup>(
213
235
  options = copy
214
236
  }
215
237
 
238
+ /**
239
+ * Changes the node's user layer: what the user chose in the native UI (a single-selection menu's check, a search
240
+ * scope). It goes over the node's own options as they resolve, by the ids of the buttons, items and scopes, and
241
+ * stays through an unrelated `mergeOptions`, the same values sent again and `setDefaultOptions`; a merge that
242
+ * changes the chosen field takes it away ([mergeOwnOptions]). Every resolved result is stale after it
243
+ * ([OptionsCache]).
244
+ */
245
+ internal fun changeChoices(change: (UserChoices) -> Unit) {
246
+ change(choices)
247
+ OptionsCache.optionsChanged()
248
+ }
249
+
250
+ /**
251
+ * [delta] of a `mergeOptions` goes over the node's own options ([options]); a choice of the user layer whose field
252
+ * the merge changes, or whose button, item or scope it took away, leaves, so the app's value shows
253
+ * (`UserChoices.yieldTo`). Every `mergeOptions` into a node writes its own options this way.
254
+ */
255
+ protected fun mergeOwnOptions(delta: Options) {
256
+ val own = options
257
+ val before = if (choices.isEmpty) null else resolveCurrentOptions().withDefaultOptions(DefaultOptions.current)
258
+ options = own.copy().mergeWith(delta)
259
+ before ?: return
260
+ val after = resolveCurrentOptions().withDefaultOptions(DefaultOptions.current)
261
+ if (choices.yieldTo(delta, own, before, after)) OptionsCache.optionsChanged()
262
+ }
263
+
216
264
  /**
217
265
  * `mergeOptions` with the node's id (`mergeOptions`): [options] go over its own and stay there (one-shot options
218
- * not kept); what shows applies them ([applyMergedOptions], when the view shows; otherwise as it appears); the
219
- * layouts around it apply their part (`ParentController.mergeChildOptions`, each passes it on). [options] still
220
- * carry the one-shot options to them. Containers that hand the merge to a child override it.
266
+ * not kept, [mergeOwnOptions]); what shows applies them ([applyMergedOptions], when the view shows; otherwise as it
267
+ * appears); the layouts around it apply their part (`ParentController.mergeChildOptions`, each passes it on).
268
+ * [options] still carry the one-shot options to them. Containers that hand the merge to a child override it.
221
269
  */
222
270
  open fun mergeOptions(options: Options) {
223
- this.options = this.options.copy().mergeWith(options)
271
+ mergeOwnOptions(options)
224
272
  if (isViewShown) applyMergedOptions(options)
225
273
  performOnParentController { it.mergeChildOptions(options, this) }
226
274
  }
@@ -265,6 +313,12 @@ abstract class ViewController<V : ViewGroup>(
265
313
  /** Whether [handleBack] would go back. */
266
314
  open fun canHandleBack(): Boolean = false
267
315
 
316
+ /**
317
+ * The stack whose top [handleBack] would take away with the default pop, which a back gesture previews; none when
318
+ * back would do anything else (hold, close a menu or a pane, a pop of its own) or nothing.
319
+ */
320
+ internal open fun stackPoppedByBack(): StackController? = null
321
+
268
322
  /** Starts the node's content (a native screen's view, a React screen's surface). */
269
323
  open fun start() = Unit
270
324
 
@@ -96,6 +96,9 @@ internal object Curves {
96
96
  * linear between them — the values of AndroidX's `FastOutSlowInInterpolator`, computed here.
97
97
  */
98
98
  object FastOutSlowIn : Curve() {
99
+ // Before the table, which is made from it.
100
+ private val curve = CubicBezier(0.4, 0.0, 0.2, 1.0)
101
+
99
102
  /** The 201 values. */
100
103
  val table: DoubleArray = DoubleArray(TABLE_STEPS + 1) {
101
104
  round(bezierAt(it.toDouble() / TABLE_STEPS) * DECIMALS) / DECIMALS
@@ -109,7 +112,25 @@ internal object Curves {
109
112
  }
110
113
 
111
114
  /** y of the Bézier at its x = [x], exactly (to 1e-12). */
112
- fun bezierAt(x: Double): Double {
115
+ fun bezierAt(x: Double): Double = curve.yAt(x)
116
+
117
+ private const val DECIMALS = 1e4
118
+ }
119
+
120
+ /**
121
+ * A cubic Bézier from (0, 0) to (1, 1) with the control points ([x1], [y1]) and ([x2], [y2]), as the framework's
122
+ * `PathInterpolator` draws it, computed exactly: the system's curves without a framework class.
123
+ */
124
+ class CubicBezier(
125
+ private val x1: Double,
126
+ private val y1: Double,
127
+ private val x2: Double,
128
+ private val y2: Double,
129
+ ) : Curve() {
130
+ override fun at(p: Double): Double = yAt(p)
131
+
132
+ /** y of the curve at its x = [x], exactly (to 1e-12). */
133
+ fun yAt(x: Double): Double {
113
134
  if (x <= 0.0) return 0.0
114
135
  if (x >= 1.0) return 1.0
115
136
  // x(t) only grows with t: a bisection finds t, then y(t).
@@ -118,23 +139,20 @@ internal object Curves {
118
139
  var t = x
119
140
  repeat(BISECTION_STEPS) {
120
141
  t = (low + high) / 2
121
- if (bezier(t, X1, X2) < x) low = t else high = t
142
+ if (coordinate(t, x1, x2) < x) low = t else high = t
122
143
  }
123
- return bezier(t, Y1, Y2)
144
+ return coordinate(t, y1, y2)
124
145
  }
125
146
 
126
- // One coordinate of the curve from (0, 0) to (1, 1) with the control points' coordinates [c1], [c2].
127
- private fun bezier(t: Double, c1: Double, c2: Double): Double {
147
+ // One coordinate of the curve with the control points' coordinates [c1], [c2].
148
+ private fun coordinate(t: Double, c1: Double, c2: Double): Double {
128
149
  val u = 1 - t
129
150
  return 3 * u * u * t * c1 + 3 * u * t * t * c2 + t * t * t
130
151
  }
131
152
 
132
- private const val X1 = 0.4
133
- private const val Y1 = 0.0
134
- private const val X2 = 0.2
135
- private const val Y2 = 1.0
136
- private const val BISECTION_STEPS = 60
137
- private const val DECIMALS = 1e4
153
+ private companion object {
154
+ const val BISECTION_STEPS = 60
155
+ }
138
156
  }
139
157
 
140
158
  /**