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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/android/CLAUDE.md +17 -7
  3. package/android/CORE.md +8 -3
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  6. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  8. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  9. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  12. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  13. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
  14. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  15. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
  16. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  17. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
  19. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  20. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  22. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  24. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  25. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  26. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +16 -6
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -3
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +6 -3
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +58 -15
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +16 -7
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +10 -2
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
  50. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  52. package/cli/doctor/checks/app.js +61 -13
  53. package/cli/migrate/native.js +18 -0
  54. package/cli/setup/source.js +2 -1
  55. package/cli/setup/steps/mainApplication.js +134 -19
  56. package/docs-md/architecture/transitions.md +6 -7
  57. package/docs-md/help/glossary.md +1 -1
  58. package/docs-md/help/troubleshooting.md +20 -3
  59. package/docs-md/index.json +4 -0
  60. package/docs-md/migration/from-react-native-navigation.md +8 -2
  61. package/docs-md/screen-ui/back-and-gestures.md +2 -2
  62. package/docs-md/start/installation.md +3 -1
  63. package/ios/CLAUDE.md +17 -5
  64. package/ios/CORE.md +13 -8
  65. package/ios/Contract/ANNCommandAnswer.mm +5 -20
  66. package/ios/Contract/ANNCommandContract.h +15 -0
  67. package/ios/Contract/ANNCommandContract.mm +78 -0
  68. package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
  69. package/ios/Core/ANNErrorHandler.h +1 -1
  70. package/ios/Core/ANNErrorHandler.mm +3 -1
  71. package/ios/Core/ANNEventEmitter.h +1 -1
  72. package/ios/Core/ANNEventEmitter.mm +2 -2
  73. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  74. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  75. package/ios/Core/ANNNavigationState.h +7 -3
  76. package/ios/Core/ANNNavigationState.mm +10 -5
  77. package/ios/Core/ANNNodeHelper.h +32 -12
  78. package/ios/Core/ANNNodeHelper.mm +100 -3
  79. package/ios/Core/ANNRenderWait.h +15 -1
  80. package/ios/Core/ANNRenderWait.mm +119 -17
  81. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  82. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  83. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  84. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  85. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  86. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  87. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  88. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  89. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  90. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  91. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  92. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  93. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  94. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  95. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  96. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  97. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  98. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  99. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  100. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  101. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  102. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  103. package/ios/Options/ANNOptionsCache.h +7 -6
  104. package/ios/Options/ANNSearchBarOptions.h +4 -3
  105. package/ios/Options/ANNUserChoice.h +57 -0
  106. package/ios/Options/ANNUserChoice.mm +44 -0
  107. package/ios/Options/Params/ANNImageSource.h +61 -0
  108. package/ios/Options/Params/ANNImageSource.mm +101 -0
  109. package/ios/Options/Params/ANNOptionImages.h +37 -0
  110. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  111. package/ios/Options/Params/Image.h +11 -2
  112. package/ios/Options/Params/Image.mm +15 -2
  113. package/ios/Options/Params/ImageParser.h +5 -5
  114. package/ios/Options/Params/ImageParser.mm +28 -46
  115. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  116. package/ios/RECIPES.md +11 -5
  117. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  118. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  119. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  120. package/ios/TopBar/ANNBarButtons.h +9 -5
  121. package/ios/TopBar/ANNBarButtons.mm +211 -63
  122. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  123. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  124. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  125. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  126. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  127. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  128. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  129. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  130. package/lib/module/ARCHITECTURE.md +18 -18
  131. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  132. package/lib/module/api/navigation.js.map +1 -1
  133. package/lib/module/engine/commands.js +9 -7
  134. package/lib/module/engine/commands.js.map +1 -1
  135. package/lib/module/engine/layout.js +13 -9
  136. package/lib/module/engine/layout.js.map +1 -1
  137. package/lib/module/engine/options.js +44 -11
  138. package/lib/module/engine/options.js.map +1 -1
  139. package/lib/module/engine/places.js +44 -7
  140. package/lib/module/engine/places.js.map +1 -1
  141. package/lib/module/engine/props.js +30 -1
  142. package/lib/module/engine/props.js.map +1 -1
  143. package/lib/module/engine/rememberedOptions.js +1 -1
  144. package/lib/module/engine/rememberedOptions.js.map +1 -1
  145. package/lib/module/engine/screens.js +22 -12
  146. package/lib/module/engine/screens.js.map +1 -1
  147. package/lib/module/interfaces/contract/commands.json +101 -0
  148. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  149. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  150. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  151. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  152. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  153. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  154. package/lib/typescript/api/navigation.d.ts +4 -1
  155. package/lib/typescript/api/navigation.d.ts.map +1 -1
  156. package/lib/typescript/engine/commands.d.ts.map +1 -1
  157. package/lib/typescript/engine/options.d.ts +5 -2
  158. package/lib/typescript/engine/options.d.ts.map +1 -1
  159. package/lib/typescript/engine/places.d.ts +5 -2
  160. package/lib/typescript/engine/places.d.ts.map +1 -1
  161. package/lib/typescript/engine/props.d.ts +8 -1
  162. package/lib/typescript/engine/props.d.ts.map +1 -1
  163. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  164. package/lib/typescript/engine/screens.d.ts.map +1 -1
  165. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  166. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  167. package/lib/typescript/interfaces/Events.d.ts +19 -10
  168. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  169. package/lib/typescript/interfaces/options/animations.d.ts +5 -2
  170. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  171. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  172. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  173. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  174. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  175. package/package.json +1 -1
  176. package/src/ARCHITECTURE.md +18 -18
  177. package/src/adapters/NativeANNTurboModule.ts +5 -2
  178. package/src/api/events/navigationEvents.ts +1 -1
  179. package/src/api/hooks/useNavigation.ts +1 -1
  180. package/src/api/navigation.ts +4 -1
  181. package/src/engine/commands.ts +19 -7
  182. package/src/engine/layout.ts +16 -9
  183. package/src/engine/options.ts +39 -11
  184. package/src/engine/places.ts +56 -7
  185. package/src/engine/props.ts +30 -1
  186. package/src/engine/rememberedOptions.ts +4 -1
  187. package/src/engine/screens.tsx +23 -13
  188. package/src/interfaces/ComponentEvents.ts +2 -2
  189. package/src/interfaces/Events.ts +19 -10
  190. package/src/interfaces/contract/commands.json +101 -0
  191. package/src/interfaces/options/animations.ts +5 -2
  192. package/src/interfaces/options/buttons.ts +13 -5
  193. package/src/interfaces/options/topBar.ts +22 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0-alpha.4](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2026-10-23)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** keep a closed React Native issue instead of opening a second one ([#37](https://github.com/apexnative/navigation/issues/37)) ([242030c](https://github.com/apexnative/navigation/commit/242030c8559a6fe186f44781ada85d09970c8318))
9
+ * **ios:** lay a waiting screen out in its place, so the large title comes in with it ([#41](https://github.com/apexnative/navigation/issues/41)) ([85bfaa8](https://github.com/apexnative/navigation/commit/85bfaa877d3775264c1ba1114942cf875f890f29))
10
+ * one native contract, options as layers and pushes that wait for the first render ([#39](https://github.com/apexnative/navigation/issues/39)) ([888f21e](https://github.com/apexnative/navigation/commit/888f21eb2b3e1aec413413bbdab9546a7a8f5a6b))
11
+
3
12
  ## [1.0.0-alpha.3](https://github.com/apexnative/navigation/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2026-10-22)
4
13
 
5
14
 
package/android/CLAUDE.md CHANGED
@@ -21,8 +21,9 @@ top-level folder. Main thread only, unless a line says otherwise.
21
21
  `OptionsReader`, `OptionsCache`, `DefaultOptions`. `params/` has the value types (`Bool`, `Text`, `Fraction`,
22
22
  `Variant`, `ThemeColour`); `parsers/` reads them tolerantly from React Native data.
23
23
  - `react/`: the way in and out: `NavigationTurboModule`, `CommandRunner`, `CommandAnswer`, `ErrorTexts`,
24
- `OptionsCommands`, `ReloadCoordinator`, `ReactGateway`, `ReactView` (a React root), the stores (`ScreenInsets`,
25
- `ScreenRegions`, `ArrangementStates`, `Hinge`, `StartInfo`), `LaunchLink`, `ScrollEvents`.
24
+ `OptionsCommands`, `PendingCalls` (a task behind the calls JavaScript made), `ReloadCoordinator`, `ReactGateway`,
25
+ `ReactView` (a React root), the stores (`ScreenInsets`, `ScreenRegions`, `ArrangementStates`, `Hinge`, `StartInfo`),
26
+ `LaunchLink`, `ScrollEvents`.
26
27
  - `react/events/`: `AppearanceDispatcher`, `ScreenAppearance`, `EventEmitter`, `EventContract` (made by
27
28
  `scripts/contract.mjs`), `NavigationStateReporter`, `ComponentType`.
28
29
  - `viewcontrollers/viewcontroller/`: `ViewController` (the base of every node), `Presenter`, `IReactView` (what a
@@ -53,7 +54,8 @@ top-level folder. Main thread only, unless a line says otherwise.
53
54
  - `slots/`, `safearea/`, `nativescreen/`: the views of `ANNSlot` and `ANNSafeAreaView`, the slot screen, the
54
55
  native-screen context and calls.
55
56
  - `splash/`, `customrow/`, `utils/`: the splash hold, theme and copy; the tabs' custom row options; helpers
56
- (`UiThread`, `ImageLoader`, `MaterialTheme`, `TestIds`, `WindowFolds`, `DevelopmentLog`).
57
+ (`UiThread`, `ImageLoader` with `OptionImages`, `ImageKey`, `ImageRequests`, `MaterialTheme`, `TestIds`, `WindowFolds`,
58
+ `DevelopmentLog`).
57
59
  - `android/src/main/jni/`: `CMakeLists.txt` builds the codegen target with the C++ of `ios/cpp` (the slot's and the
58
60
  safe area view's shadow nodes); `ApexNavigationSpec.h` replaces the generated header so autolinking sees their
59
61
  descriptors.
@@ -106,12 +108,14 @@ top-level folder. Main thread only, unless a line says otherwise.
106
108
  `NavigationStateReporter.kt`; `react/CommandAnswer.kt`.
107
109
  - Options (parsing, merging): `options/Options.kt`, `OptionsReader.kt`, `OptionsCache.kt`, `DefaultOptions.kt`,
108
110
  `params/`, `parsers/`; `viewcontrollers/viewcontroller/ViewController.kt` (resolution).
109
- - Error texts: `react/ErrorTexts.kt`, made by `scripts/contract.mjs` from `src/interfaces/contract/errors.json`.
111
+ - Error texts and answers: `react/ErrorTexts.kt` and `react/CommandContract.kt`, made by `scripts/contract.mjs` from
112
+ `src/interfaces/contract/errors.json` and `commands.json`.
110
113
  - Links: `react/LaunchLink.kt`, `NavigationActivity.kt` (`onNewIntent`); `react/StartInfo.kt`.
111
114
  - Hinge and regions: `react/Hinge.kt`, `react/ScreenRegions.kt`, `react/ScreenInsets.kt`, `utils/WindowFolds.kt`.
112
115
  - Native screens: `nativescreen/NativeScreenContext.kt`, `NativeScreenCalls.kt`, `NativeScreenCreator.kt`;
113
116
  `viewcontrollers/externalcomponent/`.
114
- - Slots: `slots/SlotHost.kt`, `SlotView.kt`, `SlotScreenView.kt`, `SlotContainers.kt`, `SlotViewManager.kt`.
117
+ - Slots: `slots/SlotHost.kt`, `SlotView.kt`, `SlotScreenView.kt`, `SlotContainers.kt` (containers and slot views by
118
+ slot id: a change goes straight to the views of its id, `SlotViews.kt`), `SlotViewManager.kt`.
115
119
  - Safe area: `safearea/SafeAreaView.kt`, `SafeAreaViewManager.kt`; `viewcontrollers/component/ScreenReadiness.kt`;
116
120
  `react/ScreenInsets.kt`.
117
121
  - The C++ of `ANNSlot`, `ANNSafeAreaView`: `android/src/main/jni/CMakeLists.txt`, `ApexNavigationSpec.h`; sources in
@@ -125,7 +129,11 @@ top-level folder. Main thread only, unless a line says otherwise.
125
129
  - **Module thread.** The `check` of every command with a promise: `LayoutReader` and, through it, `OptionsReader` (also
126
130
  for `setDefaultOptions`, `mergeOptions` and the options of `dismissModal`, in the runner's `prepare` step);
127
131
  `ReadableJson` for native-screen props. The registry of native screens is read under its lock. Images are not loaded
128
- here; `ImageLoader` loads them when the options apply, on main.
132
+ here; they load when the options apply, through the one cache of `utils/OptionImages` (by `ImageKey`): at once on
133
+ main where the first frame needs them (`ImageLoader`: the top bar's buttons, menus, back button and search
134
+ suggestions; a tab's first icon; a new FAB), on its background thread for a file or a drawable of the app where the
135
+ view takes them when ready (`ImageRequests`: a changed tab icon or one that didn't load, a FAB's changed icon, the
136
+ icon of a FAB hidden with `visible: false`, the FAB menu's actions).
129
137
  - **Main thread.** Everything else: the command body, nodes, views, events, the stores' writes. Any thread may read
130
138
  `ReloadCoordinator.generation`.
131
139
 
@@ -155,7 +163,9 @@ top-level folder. Main thread only, unless a line says otherwise.
155
163
  skips the table that decides interrupts and answers.
156
164
  - Every event of an appearance goes through `AppearanceDispatcher`. A container with its own loop of
157
165
  `onViewWillAppear` / `onViewDidAppear` / `onViewDisappear` sends the events in the wrong order.
158
- - Never change a resolved options object: it is cached and shared. Copy it, or write through `changeOptions`.
166
+ - Never change a resolved options object: it is cached and shared. Copy it, or write through `changeOptions`; a
167
+ user's choice goes through `changeChoices` (the user layer), never into own options, or the next `mergeOptions`
168
+ wipes it.
159
169
  - A new option field that is not in `mergeWith`, `mergeWithDefault`, `copy` or the fixture is dropped silently somewhere
160
170
  in the chain; `WholeTreeTest` exists to catch it.
161
171
  - A new wait is a new `RenderWait` value, not a new timer. A wait that survives a destroy or a supersede hangs a promise;
package/android/CORE.md CHANGED
@@ -72,8 +72,13 @@ react/events/AppearanceDispatcher ─► NavigationStateReporter ─► EventEmi
72
72
  5. **Make and join.** `LayoutFactory.create` makes Profile (a `ComponentViewController`, registered in the
73
73
  `ChildControllersRegistry`); `StackController.join` adds it; `StackQueue.push` accepts it as an entry.
74
74
  6. **`StackQueue`** is the only way to change a stack's children. `drain` starts the operation when nothing blocks it.
75
- `startPush` decides the wait: none, `waitForRender`, or a prepared push. A wait is a `RenderWait` on Profile, and the
76
- queue's `deliver` posts readiness signals to the next turn of the loop.
75
+ `startPush` decides the wait: a prepared push; `waitForRender: true` (as long as it takes); unset, for a screen with
76
+ React content, Profile's first mount, 0.5 s at most (`PUSH_WAIT_MILLIS`); none for `false`, a native screen or a
77
+ stack off screen. A wait is a `RenderWait` on Profile. Its mount reaches the push through the queue's `afterCalls`
78
+ (`PendingCalls`: once through the JavaScript thread, the module's thread and the main thread, the way of a
79
+ `mergeOptions`), so the `mergeOptions` of the commit that mounted it (`useScreenOptions`) has applied when the
80
+ transition starts, content mounted before the push too; the prepared push starts the same way after its slot
81
+ mounted. Unset, the targets of elements take what is left of the 0.5 s.
77
82
  7. **Begin.** `beginPush` raises Profile's view (`StackViews`), moves the bar and the tab bar
78
83
  (`StackQueue.Host.barTransition`, `tabBarTransition`), calls `StackController` through `currentChildChanged`, then
79
84
  `AppearanceDispatcher.beginTransition`: **`ComponentWillAppear`** of Profile. The transition runs
@@ -82,7 +87,7 @@ react/events/AppearanceDispatcher ─► NavigationStateReporter ─► EventEmi
82
87
  **`ComponentDidAppear`** of Profile, then `reportState` (**`NavigationStateChanged`**, only if it changed), then the
83
88
  transition's own events (none for a push), then `CommandAnswer.resolve` with Profile's id.
84
89
  9. **Answer.** `resolve` sends the state once more if it changed (`AnswerEvents.beforeAnswer`), resolves the promise,
85
- then sends **`CommandCompleted`** (`COMPLETED_COMMANDS`). After the task, `BackCallbackState.update` recomputes what
90
+ then sends **`CommandCompleted`** (`CommandContract.COMPLETED`). After the task, `BackCallbackState.update` recomputes what
86
91
  back does.
87
92
 
88
93
  The events go from the main thread, synchronously, through `EventEmitter.current()`; the promise is answered once.
@@ -14,7 +14,11 @@ The steps for adding an option and for adding a command to the Android core; rea
14
14
  (a repaint after a light/dark switch or `setDefaultOptions`); a system-bar option goes through `SystemUiRules`.
15
15
  Read colors with `ThemeColour.get` at apply so they follow the theme.
16
16
  4. The cache needs nothing: own options are written only through `ViewController.options`, `changeOptions` and
17
- `mergeOptions`, defaults only through `DefaultOptions.set`. Never change a resolved result.
17
+ `mergeOptions` (`mergeOwnOptions`), defaults only through `DefaultOptions.set`. Never change a resolved result.
18
+ What the user chooses in the native UI (a menu's check, a search scope) is not written into own options: it goes
19
+ into the node's user layer through `ViewController.changeChoices` (`options/UserChoices.kt`), which applies it by
20
+ id over the resolved options and lets it go when a merge changes that field. A new kind of choice is a field of
21
+ `UserChoices` with its `applyTo` and `yieldTo`; `changeOptions` stays for the library's own bookkeeping of a merge.
18
22
  5. Tests: add the field to `OptionsFixture.full()`. `WholeTreeTest` walks the tree by reflection and fails when a field
19
23
  is not read, copied, merged or defaulted. Add cases to `ReadersTest`, `MergeRuleTest` and the area's rules test.
20
24
  6. The docs page and a Showcase control; one meaning on both platforms, and where Android can't do it the JSDoc says so
@@ -31,8 +35,10 @@ The steps for adding an option and for adding a command to the Android core; rea
31
35
  4. New layout fields are read and checked in `LayoutReader`; checks against what shows are in `LayoutChecks` or in the
32
36
  command, before any node is made. Every rejection text is a function in `ErrorTexts` (same words as iOS),
33
37
  made by `node scripts/contract.mjs` from `src/interfaces/contract/errors.json`: a new text goes there.
34
- 5. The answer goes through `CommandAnswer`: add the name to `COMPLETED_COMMANDS` when the command changes what shows
35
- (then the state, the promise and `CommandCompleted` follow in that order). A transition ends through
38
+ 5. The answer goes through `CommandAnswer`: give the command its entry in `src/interfaces/contract/commands.json`
39
+ (what it resolves with, `state` when it may change what shows, `completed` when it shows or dismisses) and run
40
+ `node scripts/contract.mjs`, which makes `CommandContract` (then the state, the promise and `CommandCompleted` follow
41
+ in that order). A transition ends through
36
42
  `AppearanceDispatcher.endTransition`.
37
43
  6. Tests: `CommandRunnerTest`, `CommandAnswerTest`, `ErrorTextsTest`, `LayoutReaderTest`; a stack operation also needs a
38
44
  case in the queue tests (`StackQueueHarness`).
@@ -3,32 +3,49 @@ package dev.apexnative.navigation
3
3
  import android.app.Application
4
4
  import android.util.Log
5
5
  import com.facebook.react.ReactApplication
6
- import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
7
- import com.facebook.react.soloader.OpenSourceMergedSoMapping
8
6
  import com.facebook.soloader.SoLoader
9
7
  import dev.apexnative.navigation.nativescreen.NativeScreenCreator
10
8
  import dev.apexnative.navigation.react.ReactGateway
11
9
  import dev.apexnative.navigation.utils.NavigationLog
12
10
 
13
11
  /**
14
- * The application class of an app with the navigation: it loads React Native itself (the app doesn't call
15
- * `loadReactNative`), keeps one React host for every screen ([reactGateway]) and the native screens by name. The app
16
- * gives the host (`reactHost`) and registers its native screens in its `onCreate`, after this one.
12
+ * The application class of an app with the navigation: it keeps one React host for every screen ([reactGateway]) and
13
+ * the native screens by name. The app gives the host (`reactHost`) and loads React Native in its `onCreate` as the
14
+ * React Native template does, so the load follows the app's Gradle settings (the New Architecture, edge-to-edge):
15
+ *
16
+ * ```kotlin
17
+ * override fun onCreate() {
18
+ * super.onCreate()
19
+ * loadReactNative(this)
20
+ * registerNativeScreen("Profile") { activity, context -> ProfileView(activity, context) }
21
+ * }
22
+ * ```
23
+ *
24
+ * The gateway, and with it the host, is made the first time something needs it (the first activity), after the app's
25
+ * `onCreate` loaded React Native.
17
26
  */
18
27
  abstract class NavigationApplication : Application(), ReactApplication {
19
28
  // Written on the main thread as the app registers, read on the module thread by the layout checks: under its lock.
20
29
  private val nativeScreens = LinkedHashMap<String, NativeScreenCreator>()
21
30
 
22
- /** The app's React host as the navigation runs it. */
23
- lateinit var reactGateway: ReactGateway
24
- private set
31
+ // Made once, by the first activity ([reactGateway]); [reactGatewayOrNull] only reads it.
32
+ private val gateway: Lazy<ReactGateway> = lazy {
33
+ check(SoLoader.isInitialized()) {
34
+ "NavigationApplication needs React Native loaded first: call loadReactNative(this) in onCreate, " +
35
+ "as the React Native template does"
36
+ }
37
+ createReactGateway()
38
+ }
39
+
40
+ /**
41
+ * The app's React host as the navigation runs it, made when it is first asked for (the first activity's
42
+ * `onCreate`). React Native must be loaded by then: the app calls `loadReactNative(this)` in its `onCreate`.
43
+ */
44
+ val reactGateway: ReactGateway by gateway
25
45
 
26
46
  override fun onCreate() {
27
47
  super.onCreate()
28
48
  instance = this
29
- SoLoader.init(this, OpenSourceMergedSoMapping)
30
- DefaultNewArchitectureEntryPoint.load()
31
- reactGateway = createReactGateway()
32
49
  registerGeneratedScreens()
33
50
  }
34
51
 
@@ -57,9 +74,13 @@ abstract class NavigationApplication : Application(), ReactApplication {
57
74
  internal fun isNativeScreenRegistered(name: String): Boolean =
58
75
  synchronized(nativeScreens) { nativeScreens.containsKey(name) }
59
76
 
60
- /** [reactGateway] once `onCreate` made it. */
77
+ /**
78
+ * [reactGateway] once the first activity made it; none before. It never makes it: its callers (the log, the
79
+ * events, a React root) may run first, also off the main thread, and the app's `reactHost` isn't theirs to make.
80
+ * Any thread.
81
+ */
61
82
  internal val reactGatewayOrNull: ReactGateway?
62
- get() = if (::reactGateway.isInitialized) reactGateway else null
83
+ get() = if (gateway.isInitialized()) gateway.value else null
63
84
 
64
85
  // The list of `npx apex-navigation codegen`, found by its name: an app without native screens has none.
65
86
  private fun registerGeneratedScreens() {
@@ -47,5 +47,9 @@ internal object LayoutChecks {
47
47
  */
48
48
  @JvmStatic
49
49
  fun checkStackChild(node: LayoutNode, stackId: String): String? =
50
- if (node.type in stackChildren) null else ErrorTexts.stackTakesScreens(stackId, node.type.jsName)
50
+ if (isStackChild(node)) null else ErrorTexts.stackTakesScreens(stackId, node.type.jsName)
51
+
52
+ /** Whether a stack takes [node]: a screen, a native screen or a pair, not a container. */
53
+ @JvmStatic
54
+ fun isStackChild(node: LayoutNode): Boolean = node.type in stackChildren
51
55
  }
@@ -82,10 +82,14 @@ internal data class LayoutNode(
82
82
  private val COLUMN_NAMES = setOf(PRIMARY, SECONDARY, SUPPLEMENTARY, COMPACT, INSPECTOR)
83
83
  private val COLUMNS_WITHOUT_NAMES = listOf(PRIMARY, SECONDARY)
84
84
 
85
- /** The column of the child at [index] of a split view with [columns]; none for a child without one. */
85
+ /**
86
+ * The column of the child at [index] of a split view with [columns]; none for a child without one, and none
87
+ * for a repeat of a name an earlier child has (the first takes the column), as on iOS.
88
+ */
86
89
  internal fun columnName(columns: List<String>, index: Int): String? {
87
90
  val names = columns.ifEmpty { COLUMNS_WITHOUT_NAMES }
88
- return names.getOrNull(index)?.takeIf { it in COLUMN_NAMES }
91
+ val name = names.getOrNull(index)?.takeIf { it in COLUMN_NAMES } ?: return null
92
+ return name.takeIf { names.indexOf(it) == index }
89
93
  }
90
94
  }
91
95
  }
@@ -44,8 +44,8 @@ class NativeScreenContext @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(
44
44
 
45
45
  /**
46
46
  * Runs a command of the navigation from this screen in JS, as `useNavigation()` of a React screen does: push, pop,
47
- * popTo, popToRoot, showInColumn, showModal, dismiss, showOverlay, dismissOverlay, setOptions, selectTab,
48
- * setTabBadge. [arguments] are those of the JS command; [result] gets what it resolved with.
47
+ * popTo, popToRoot, showInColumn, showModal, dismiss, showOverlay, setOptions, selectTab, setTabBadge.
48
+ * [arguments] are those of the JS command; [result] gets what it resolved with.
49
49
  */
50
50
  @JvmOverloads
51
51
  fun navigate(command: String, arguments: JSONArray = JSONArray(), result: ((Result<Any?>) -> Unit)? = null) =
@@ -8,8 +8,8 @@ import dev.apexnative.navigation.options.parsers.IconParser
8
8
  * A menu of the bar: `topBar.overflowMenu`, or the `menu` of a button. Its items, and submenus among them; with
9
9
  * `singleSelection` one item of a level has a checkmark at a time.
10
10
  *
11
- * Copied options share it, so the checkmark a pick moves stays with the screen when it appears again, until new
12
- * options give the menu again.
11
+ * Copied options share it, so nobody changes a menu in place but in a copy of its own: the check a user's pick moves
12
+ * lives in the screen's user layer (`UserChoices`), which goes into a copy as the screen's options resolve.
13
13
  */
14
14
  class ButtonMenuOptions(
15
15
  @JvmField val singleSelection: Boolean,
@@ -197,8 +197,8 @@ class ButtonOptions(@JvmField var id: Text = NullText()) {
197
197
 
198
198
  /**
199
199
  * A copy of the menu with new items and submenus and the same values. A menu taken from the default options is copied
200
- * first, so that the module's in-place `checked` never writes into [DefaultOptions.current]; copies between a node's
201
- * own and resolved options share the menu on purpose.
200
+ * first, so that nothing written into a resolved result reaches [DefaultOptions.current]; the user layer writes its
201
+ * checks into a copy too (`UserChoices`), and copies between a node's own and resolved options share the menu.
202
202
  */
203
203
  internal fun ButtonMenuOptions.copyMenu(): ButtonMenuOptions =
204
204
  ButtonMenuOptions(singleSelection, items.map(::copyElement), testID)
@@ -3,9 +3,10 @@ package dev.apexnative.navigation.options
3
3
  /**
4
4
  * The one point where the options of the app change: a process-wide generation that grows at each change. A node's
5
5
  * resolved options are kept while the generation stays and the nodes they were built along are the same
6
- * (`ViewController.resolveCurrentOptions`). Exactly two places call [optionsChanged]: the write of a node's own options
7
- * (`ViewController.options`, and `changeOptions` through it) and the store of new default options
8
- * ([DefaultOptions.set]). Nothing else changes a node's own options or the defaults. Main thread only.
6
+ * (`ViewController.resolveCurrentOptions`). Three places call [optionsChanged]: the write of a node's own options
7
+ * (`ViewController.options`, and `changeOptions` and `mergeOwnOptions` through it), the write of a node's user layer
8
+ * (`ViewController.changeChoices`, and a merge that takes a choice away) and the store of new default options
9
+ * ([DefaultOptions.set]). Nothing else changes a node's own options, its user layer or the defaults. Main thread only.
9
10
  */
10
11
  object OptionsCache {
11
12
  /** The generation of the options; it only grows. */
@@ -13,7 +14,10 @@ object OptionsCache {
13
14
  var generation: Long = 0
14
15
  private set
15
16
 
16
- /** A node's own options or the default options changed: every resolved result built before is stale. */
17
+ /**
18
+ * A node's own options, its user layer or the default options changed: every resolved result built before is
19
+ * stale.
20
+ */
17
21
  @JvmStatic
18
22
  fun optionsChanged() {
19
23
  generation++
@@ -0,0 +1,159 @@
1
+ package dev.apexnative.navigation.options
2
+
3
+ import dev.apexnative.navigation.options.params.Text
4
+
5
+ /**
6
+ * The user layer of a node: what the user chose in the native UI, kept apart from the node's own options so that the
7
+ * app's next `mergeOptions` or `setDefaultOptions` doesn't wipe it. The choices are the check of a single-selection
8
+ * menu (a bar button's `menu`, by the button's id; `topBar.overflowMenu`) and the search scope
9
+ * (`topBar.searchBar.selectedScope`).
10
+ *
11
+ * It goes over everything else as the node's options resolve (defaults < parents < own < user), by identity rather
12
+ * than as a tree merged over: a menu's check by the button's id and the item's id, the scope by its id. A choice whose
13
+ * button, item or scope is gone is not applied, and leaves at the node's next merge; a merge that gives a chosen field
14
+ * a value other than the one the app had there before takes the choice away ([yieldTo]). `setDefaultOptions` never
15
+ * clears it. The node owns it and writes it through `ViewController.changeChoices`, which moves [OptionsCache]. Main
16
+ * thread only.
17
+ */
18
+ internal class UserChoices {
19
+ // The menus' checks: per menu (a button's id, OVERFLOW for `overflowMenu`) the item picked in each of its levels,
20
+ // in the order of the picks.
21
+ private val checks = LinkedHashMap<String, MutableList<String>>()
22
+ private var scope: String? = null
23
+
24
+ /** The user chose nothing that stays. */
25
+ val isEmpty: Boolean
26
+ get() = checks.isEmpty() && scope == null
27
+
28
+ /**
29
+ * The user picked [itemId] in [menu], the menu of the button [buttonId] or `overflowMenu` (null), as it showed: it
30
+ * takes the place of an earlier pick in the same level.
31
+ */
32
+ fun check(buttonId: String?, menu: ButtonMenuOptions, itemId: String) {
33
+ val picks = checks.getOrPut(keyOf(buttonId)) { mutableListOf() }
34
+ val level = levelItems(menu.items, itemId)?.map { it.id }.orEmpty()
35
+ picks.removeAll { it == itemId || it in level }
36
+ picks += itemId
37
+ }
38
+
39
+ /** The user picked the search scope [scopeId]. */
40
+ fun pickScope(scopeId: String) {
41
+ scope = scopeId
42
+ }
43
+
44
+ /**
45
+ * Writes the choices into [resolved], a result of resolution nobody else holds yet; the menus it changes are copied
46
+ * first, as a resolved result shares its menus with the layers they come from. Returns [resolved].
47
+ */
48
+ fun applyTo(resolved: Options): Options {
49
+ if (isEmpty) return resolved
50
+ val topBar = resolved.topBar
51
+ for ((key, picks) in checks) {
52
+ if (key == OVERFLOW) {
53
+ val menu = topBar.overflowMenu ?: continue
54
+ topBar.overflowMenu = menu.copyMenu().also { copy -> picks.forEach { mark(copy, it) } }
55
+ continue
56
+ }
57
+ val button = buttonIn(topBar.buttons, key) ?: continue
58
+ val menu = button.menu ?: continue
59
+ button.menu = menu.copyMenu().also { copy -> picks.forEach { mark(copy, it) } }
60
+ }
61
+ scope?.let { picked ->
62
+ val searchBar = topBar.searchBar
63
+ if (searchBar.scopes?.any { it.id == picked } == true) searchBar.selectedScope = Text(picked)
64
+ }
65
+ return resolved
66
+ }
67
+
68
+ /**
69
+ * The app merged [delta] into the node. A choice leaves when what it was made in is gone from [after] (the node's
70
+ * options as they resolve after the merge, without this layer), or when [delta] gives its field a value other than
71
+ * the one the node's own options had before the merge ([own]; where they had none, the app's resolution before the
72
+ * merge, [before]). The same value sent again (a re-render) keeps the choice. Returns whether a choice left.
73
+ */
74
+ fun yieldTo(delta: Options, own: Options, before: Options, after: Options): Boolean {
75
+ var changed = false
76
+ val entries = checks.entries.iterator()
77
+ while (entries.hasNext()) {
78
+ val (key, picks) = entries.next()
79
+ val now = menuIn(after, key)
80
+ val given = menuIn(delta, key)
81
+ val was = menuIn(own, key) ?: menuIn(before, key)
82
+ val left = picks.removeAll { itemId ->
83
+ val gone = now == null || levelItems(now.items, itemId) == null
84
+ gone || (given != null && checkedInLevel(given, itemId) != was?.let { checkedInLevel(it, itemId) })
85
+ }
86
+ if (left) changed = true
87
+ if (picks.isEmpty()) entries.remove()
88
+ }
89
+ scope?.let { picked ->
90
+ val given = delta.topBar.searchBar.selectedScope
91
+ val ownScope = own.topBar.searchBar.selectedScope
92
+ val was = (if (ownScope.hasValue()) ownScope else before.topBar.searchBar.selectedScope).get(null)
93
+ val changedByApp = given.hasValue() && given.get(null) != was
94
+ val gone = after.topBar.searchBar.scopes?.any { it.id == picked } != true
95
+ if (changedByApp || gone) {
96
+ scope = null
97
+ changed = true
98
+ }
99
+ }
100
+ return changed
101
+ }
102
+
103
+ companion object {
104
+ // The key of `overflowMenu`, which has no button; no placed button has an empty id.
105
+ private const val OVERFLOW = ""
106
+
107
+ private fun keyOf(buttonId: String?): String = buttonId ?: OVERFLOW
108
+
109
+ /** Whether picking [itemId] in [menu] moves a check: its level has `singleSelection`. */
110
+ fun checks(menu: ButtonMenuOptions, itemId: String): Boolean =
111
+ levelOf(menu.items, menu.singleSelection, itemId) == true
112
+
113
+ /** Moves the check of [itemId]'s level to it, in [menu] itself; nothing in a level without single selection. */
114
+ fun mark(menu: ButtonMenuOptions, itemId: String) {
115
+ mark(menu.items, menu.singleSelection, itemId)
116
+ }
117
+
118
+ private fun buttonIn(buttons: TopBarButtons, id: String): ButtonOptions? =
119
+ buttons.left?.firstOrNull { it.id.get(null) == id } ?: buttons.right?.firstOrNull { it.id.get(null) == id }
120
+
121
+ // The menu the key names in [options]: `overflowMenu`, or the menu of the button with that id.
122
+ private fun menuIn(options: Options, key: String): ButtonMenuOptions? =
123
+ if (key == OVERFLOW) options.topBar.overflowMenu else buttonIn(options.topBar.buttons, key)?.menu
124
+
125
+ // The checked items of the level that holds [itemId] in [menu]: the field a merge gives that level.
126
+ private fun checkedInLevel(menu: ButtonMenuOptions, itemId: String): Set<String>? =
127
+ levelItems(menu.items, itemId)?.filter { it.checked }?.map { it.id }?.toSet()
128
+
129
+ // The items of the level that holds [itemId]; null when it isn't in [elements].
130
+ private fun levelItems(
131
+ elements: List<ButtonMenuOptions.Element>,
132
+ itemId: String,
133
+ ): List<ButtonMenuOptions.Item>? {
134
+ val items = elements.filterIsInstance<ButtonMenuOptions.Item>()
135
+ if (items.any { it.id == itemId }) return items
136
+ return elements.filterIsInstance<ButtonMenuOptions.Submenu>().firstNotNullOfOrNull {
137
+ levelItems(it.items, itemId)
138
+ }
139
+ }
140
+
141
+ // Whether the level holding [itemId] has single selection; null when [itemId] isn't in [elements].
142
+ private fun levelOf(elements: List<ButtonMenuOptions.Element>, single: Boolean, itemId: String): Boolean? {
143
+ if (elements.any { it is ButtonMenuOptions.Item && it.id == itemId }) return single
144
+ for (element in elements) {
145
+ if (element !is ButtonMenuOptions.Submenu) continue
146
+ levelOf(element.items, element.singleSelection, itemId)?.let { return it }
147
+ }
148
+ return null
149
+ }
150
+
151
+ private fun mark(elements: List<ButtonMenuOptions.Element>, single: Boolean, itemId: String): Boolean {
152
+ if (elements.any { it is ButtonMenuOptions.Item && it.id == itemId }) {
153
+ if (single) elements.forEach { if (it is ButtonMenuOptions.Item) it.checked = it.id == itemId }
154
+ return true
155
+ }
156
+ return elements.any { it is ButtonMenuOptions.Submenu && mark(it.items, it.singleSelection, itemId) }
157
+ }
158
+ }
159
+ }
@@ -39,10 +39,10 @@ internal class CommandAnswer(
39
39
  fun resolve(value: Any?) {
40
40
  if (!canAnswer) return
41
41
  isAnswered = true
42
- if (commandName in STATE_COMMANDS) guarded("report the navigation state") { events.beforeAnswer() }
42
+ if (commandName in CommandContract.STATE) guarded("report the navigation state") { events.beforeAnswer() }
43
43
  promise.resolve(value)
44
44
  val commandId = commandId
45
- if (commandName in COMPLETED_COMMANDS && commandId != null) {
45
+ if (commandName in CommandContract.COMPLETED && commandId != null) {
46
46
  guarded("send ANN.CommandCompleted") { events.commandCompleted(commandName, commandId, clock()) }
47
47
  }
48
48
  }
@@ -65,28 +65,6 @@ internal class CommandAnswer(
65
65
  log("[ApexNavigation] $commandName: ${ErrorTexts.couldNot(what, ErrorTexts.reasonOf(e))}")
66
66
  }
67
67
  }
68
-
69
- companion object {
70
- // The commands that send `ANN.CommandCompleted` (EVT-CommandCompleted).
71
- val COMPLETED_COMMANDS = setOf(
72
- "setRoot",
73
- "push",
74
- "pop",
75
- "popTo",
76
- "popToRoot",
77
- "setStackRoot",
78
- "showInColumn",
79
- "showModal",
80
- "dismissModal",
81
- "dismissAllModals",
82
- "showOverlay",
83
- "dismissOverlay",
84
- "dismissAllOverlays",
85
- )
86
-
87
- // The commands whose answer comes after the navigation state: those and `setColumnShown`.
88
- val STATE_COMMANDS = COMPLETED_COMMANDS + "setColumnShown"
89
- }
90
68
  }
91
69
 
92
70
  /** What goes to JavaScript around the answer of a command. Main thread only. */
@@ -0,0 +1,59 @@
1
+ // Made by scripts/contract.mjs from src/interfaces/contract/commands.json: change that file and run the script again.
2
+
3
+ package dev.apexnative.navigation.react
4
+
5
+ /**
6
+ * What the commands with a promise answer, the same table as iOS. What each resolves with:
7
+ * - `setRoot`: the id of the root, once the root and the layout's modals and overlays show.
8
+ * - `push`: the id of the screen that shows; of a pair, the pair's id.
9
+ * - `preparePush`: the frame `{ width, height, padding }` of the prepared screen, or null; always null on iOS.
10
+ * - `pop`: the id of the screen that went.
11
+ * - `popTo`: the id of the screen it went back to.
12
+ * - `popToRoot`: the id of the first screen of the stack.
13
+ * - `setStackRoot`: the id of the new top screen.
14
+ * - `showModal`: the id of the modal's root.
15
+ * - `dismissModal`: the id of the modal's root.
16
+ * - `dismissAllModals`: an empty string.
17
+ * - `showOverlay`: the id of the overlay's root.
18
+ * - `dismissOverlay`: the id of the overlay's root.
19
+ * - `dismissAllOverlays`: an empty string.
20
+ * - `showInColumn`: the id of the screen that shows.
21
+ * - `setColumnShown`: nothing once the columns stopped moving (`undefined` on iOS, `null` on Android).
22
+ * - `getLaunchLink`: the link that launched the app, or null.
23
+ */
24
+ internal object CommandContract {
25
+ /** The commands whose answer comes after the navigation state: every one that may change what shows. */
26
+ val STATE: Set<String> = setOf(
27
+ "setRoot",
28
+ "push",
29
+ "pop",
30
+ "popTo",
31
+ "popToRoot",
32
+ "setStackRoot",
33
+ "showModal",
34
+ "dismissModal",
35
+ "dismissAllModals",
36
+ "showOverlay",
37
+ "dismissOverlay",
38
+ "dismissAllOverlays",
39
+ "showInColumn",
40
+ "setColumnShown",
41
+ )
42
+
43
+ /** The commands that send `ANN.CommandCompleted` after their answer: every one that shows or dismisses. */
44
+ val COMPLETED: Set<String> = setOf(
45
+ "setRoot",
46
+ "push",
47
+ "pop",
48
+ "popTo",
49
+ "popToRoot",
50
+ "setStackRoot",
51
+ "showModal",
52
+ "dismissModal",
53
+ "dismissAllModals",
54
+ "showOverlay",
55
+ "dismissOverlay",
56
+ "dismissAllOverlays",
57
+ "showInColumn",
58
+ )
59
+ }
@@ -19,13 +19,16 @@ import java.lang.ref.WeakReference
19
19
 
20
20
  /**
21
21
  * The device's hinge (`useHinge`): its state from the fold in the activity's window, its angle from the hinge angle
22
- * sensor (Android 11+). Android has no public state of a closed device: closed is a window without a fold and an
23
- * angle under 90°. No sensor and no fold: no hinge, `{}`. It runs while JavaScript has a hook mounted
24
- * (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value is not sent twice. Main
25
- * thread only, but [get].
22
+ * sensor (Android 11+), in degrees to a tenth as on iOS. Android has no public state of a closed device: closed is a
23
+ * window without a fold, or an angle under 90° ([valueOf]). No sensor and no fold: no hinge, `{}`. It runs while
24
+ * JavaScript has a hook mounted (`setHingeUpdates`), the sensor only while the activity is resumed too; the same value
25
+ * is not sent twice. Main thread only, but [get].
26
26
  */
27
27
  internal object Hinge {
28
- data class Value(val state: String, val angle: Float?)
28
+ /** A value of the hinge: its state, and its angle in degrees to a tenth; none while the state is unknown. */
29
+ data class Value(val state: String, val angle: Double?)
30
+
31
+ private const val UNKNOWN = "unknown"
29
32
 
30
33
  private const val CLOSED_BELOW = 90f
31
34
 
@@ -128,28 +131,33 @@ internal object Hinge {
128
131
  if (!enabled) return
129
132
  val folds = folds ?: return
130
133
  if (!folds.hasInfo || (listening && angle == null)) return
131
- val fold = folds.folds.firstOrNull()
132
- val angle = angle
133
- val value = if (fold == null && sensor == null) {
134
- null
135
- } else {
136
- val state = when {
137
- fold?.halfOpened == true -> "partiallyOpen"
138
- fold != null -> "fullyOpen"
139
- angle != null && angle < CLOSED_BELOW -> "closed"
140
- else -> "unknown"
141
- }
142
- Value(state, angle)
143
- }
134
+ val value = valueOf(folds.folds.firstOrNull(), sensor != null, angle)
144
135
  if (value == last) return
145
136
  last = value
146
137
  val reactContext = (activity?.get()?.application as? ReactApplication)?.reactHost?.currentReactContext ?: return
147
138
  EventEmitter(reactContext).emitHingeChanged(toMap(value))
148
139
  }
149
140
 
141
+ /**
142
+ * The value of the window's first [fold] and the sensor's [angle]: none without a fold and without a sensor
143
+ * ([hasSensor]). `closed` without a fold or under 90°, else `partiallyOpen` for a half-opened fold and `fullyOpen`
144
+ * for a flat one. The angle goes to a tenth, as iOS gives it: the sensor's noise doesn't make a new value, and it
145
+ * is left out while the state is unknown.
146
+ */
147
+ fun valueOf(fold: WindowFolds.Fold?, hasSensor: Boolean, angle: Float?): Value? {
148
+ if (fold == null && !hasSensor) return null
149
+ val state = when {
150
+ fold == null || (angle != null && angle < CLOSED_BELOW) -> "closed"
151
+ fold.halfOpened -> "partiallyOpen"
152
+ else -> "fullyOpen"
153
+ }
154
+ val tenths = angle?.let { Math.round(it * 10.0) / 10.0 }
155
+ return Value(state, tenths.takeIf { state != UNKNOWN })
156
+ }
157
+
150
158
  private fun toMap(value: Value?): WritableMap = Arguments.createMap().apply {
151
159
  if (value == null) return@apply
152
160
  putString(EventContract.HingeChanged.STATE, value.state)
153
- value.angle?.let { putDouble(EventContract.HingeChanged.ANGLE, it.toDouble()) }
161
+ if (value.state != UNKNOWN) value.angle?.let { putDouble(EventContract.HingeChanged.ANGLE, it) }
154
162
  }
155
163
  }