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

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 (235) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/android/CLAUDE.md +23 -14
  4. package/android/CORE.md +32 -19
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +60 -13
  6. package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
  7. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
  8. package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
  10. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +28 -13
  11. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +7 -4
  12. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
  13. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
  14. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -6
  15. package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
  16. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +19 -51
  17. package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +30 -39
  19. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
  20. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
  21. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +12 -10
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +10 -7
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +22 -21
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +12 -9
  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/ScreenReadiness.kt +103 -56
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -5
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +2 -2
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +3 -0
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +13 -5
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +2 -5
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +4 -9
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
  53. package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
  56. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
  57. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
  58. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
  59. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
  60. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
  61. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
  62. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
  63. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
  64. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
  65. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
  66. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
  67. package/cli/doctor/checks/splash.js +4 -22
  68. package/cli/splash/index.js +3 -1
  69. package/docs-md/architecture/transitions.md +4 -2
  70. package/docs-md/containers/modals.md +4 -2
  71. package/docs-md/containers/overlays.md +1 -1
  72. package/docs-md/containers/side-menu.md +1 -1
  73. package/docs-md/containers/tabs.md +2 -0
  74. package/docs-md/flows/bottom-sheets.md +1 -1
  75. package/docs-md/fundamentals/starting-the-app.md +1 -1
  76. package/docs-md/guides/cli.md +1 -2
  77. package/docs-md/help/glossary.md +2 -2
  78. package/docs-md/help/troubleshooting.md +23 -2
  79. package/docs-md/index.json +12 -0
  80. package/docs-md/migration/from-expo-router.md +1 -1
  81. package/docs-md/migration/from-react-native-navigation.md +1 -1
  82. package/docs-md/migration/from-react-navigation.md +2 -2
  83. package/docs-md/screen-ui/back-and-gestures.md +36 -0
  84. package/docs-md/screen-ui/bar-buttons.md +4 -4
  85. package/docs-md/screen-ui/search.md +1 -1
  86. package/docs-md/screen-ui/system-ui.md +16 -11
  87. package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
  88. package/docs-md/screen-ui/top-bar.md +8 -4
  89. package/docs-md/start/installation.md +1 -1
  90. package/ios/Animations/ANNFrameDriver.mm +2 -0
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
  92. package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
  93. package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
  94. package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
  95. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
  96. package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
  97. package/ios/CLAUDE.md +17 -15
  98. package/ios/CORE.md +30 -26
  99. package/ios/Contract/ANNCommandAnswer.h +9 -2
  100. package/ios/Contract/ANNCommandAnswer.mm +25 -12
  101. package/ios/Contract/ANNCommandRunner.h +8 -7
  102. package/ios/Contract/ANNCommandRunner.mm +21 -27
  103. package/ios/Contract/ANNLayoutNode.h +3 -3
  104. package/ios/Contract/ANNLayoutReader.h +3 -3
  105. package/ios/Core/ANNAppearanceDispatcher.h +5 -4
  106. package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
  107. package/ios/Core/ANNJavaScriptCalls.h +11 -12
  108. package/ios/Core/ANNJavaScriptCalls.mm +7 -16
  109. package/ios/Core/ANNNavigationState.h +4 -6
  110. package/ios/Core/ANNNavigationState.mm +0 -4
  111. package/ios/Core/ANNReloadCoordinator.mm +4 -8
  112. package/ios/Core/ApexNavigation+Internal.h +1 -1
  113. package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
  114. package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
  115. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
  116. package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
  117. package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
  118. package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
  119. package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
  120. package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
  121. package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
  122. package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
  123. package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
  124. package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
  125. package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
  126. package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
  127. package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
  128. package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
  129. package/ios/Layouts/Stack/ANNPreviewPush.mm +142 -27
  130. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
  131. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
  132. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
  133. package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
  134. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
  135. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
  136. package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
  137. package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
  138. package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
  139. package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
  140. package/ios/Options/ANNBottomTabsOptions.h +2 -0
  141. package/ios/Options/ANNBottomTabsOptions.mm +1 -0
  142. package/ios/Options/ANNOptions.h +18 -0
  143. package/ios/Options/ANNOptions.mm +112 -1
  144. package/ios/Options/ANNOptionsReader.mm +110 -0
  145. package/ios/Options/Params/ANNParam.h +15 -0
  146. package/ios/Options/Params/ANNParam.mm +61 -0
  147. package/ios/Options/Params/Color.h +3 -0
  148. package/ios/Options/Params/Color.mm +21 -0
  149. package/ios/Options/Params/Image.h +4 -0
  150. package/ios/Options/Params/Image.mm +20 -0
  151. package/ios/React/ANNReactView.mm +6 -25
  152. package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
  153. package/ios/React/ANNSurfaceHostingView.h +6 -0
  154. package/ios/React/ANNSurfaceHostingView.mm +116 -0
  155. package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
  156. package/ios/TopBar/ANNBarAppearance.h +2 -1
  157. package/ios/TopBar/ANNBarAppearance.mm +111 -30
  158. package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
  159. package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
  160. package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
  161. package/ios/TopBar/ANNBarButtons.h +5 -3
  162. package/ios/TopBar/ANNBarButtons.mm +186 -17
  163. package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
  164. package/ios/TopBar/ANNMainList.h +3 -0
  165. package/ios/TopBar/ANNMainList.mm +4 -0
  166. package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
  167. package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
  168. package/ios/TopBar/ANNSearchPresenter.mm +2 -2
  169. package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
  170. package/ios/TopBar/ANNTopBarPresenter.h +3 -2
  171. package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
  172. package/ios/TurboModules/ANNTurboModule.mm +20 -16
  173. package/lib/module/ARCHITECTURE.md +8 -4
  174. package/lib/module/api/commands/commands.js +54 -6
  175. package/lib/module/api/commands/commands.js.map +1 -1
  176. package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
  177. package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
  178. package/lib/module/api/options/OptionsChain.js +2 -2
  179. package/lib/module/engine/commands.js +21 -1
  180. package/lib/module/engine/commands.js.map +1 -1
  181. package/lib/module/engine/places.js +8 -0
  182. package/lib/module/engine/places.js.map +1 -1
  183. package/lib/module/engine/screens.js +29 -4
  184. package/lib/module/engine/screens.js.map +1 -1
  185. package/lib/typescript/api/app/createNavigation.d.ts +3 -3
  186. package/lib/typescript/api/commands/commands.d.ts.map +1 -1
  187. package/lib/typescript/api/commands/presentation.d.ts +4 -3
  188. package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
  189. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
  190. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
  191. package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
  192. package/lib/typescript/engine/commands.d.ts.map +1 -1
  193. package/lib/typescript/engine/places.d.ts +2 -0
  194. package/lib/typescript/engine/places.d.ts.map +1 -1
  195. package/lib/typescript/engine/screens.d.ts +20 -6
  196. package/lib/typescript/engine/screens.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/Options.d.ts +1 -1
  198. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/animations.d.ts +30 -9
  200. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  201. package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
  202. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  203. package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
  204. package/lib/typescript/interfaces/options/stack.d.ts +9 -0
  205. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  206. package/lib/typescript/interfaces/options/system.d.ts +8 -5
  207. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  208. package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
  209. package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
  210. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  211. package/package.json +1 -1
  212. package/src/ARCHITECTURE.md +8 -4
  213. package/src/api/app/createNavigation.ts +3 -3
  214. package/src/api/commands/commands.ts +48 -7
  215. package/src/api/commands/presentation.ts +4 -3
  216. package/src/api/hooks/useSafeAreaInsets.ts +4 -3
  217. package/src/api/options/OptionsChain.ts +2 -2
  218. package/src/engine/commands.ts +30 -6
  219. package/src/engine/places.ts +9 -0
  220. package/src/engine/screens.tsx +29 -8
  221. package/src/interfaces/Options.ts +1 -0
  222. package/src/interfaces/options/animations.ts +31 -9
  223. package/src/interfaces/options/bottomTabs.ts +18 -5
  224. package/src/interfaces/options/buttons.ts +8 -8
  225. package/src/interfaces/options/stack.ts +9 -0
  226. package/src/interfaces/options/system.ts +8 -5
  227. package/src/interfaces/options/toolbar.ts +2 -2
  228. package/src/interfaces/options/topBar.ts +32 -29
  229. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
  230. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
  231. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
  232. package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
  233. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
  234. package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
  235. package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
@@ -1,6 +1,6 @@
1
1
  package dev.apexnative.navigation.viewcontrollers.component
2
2
 
3
- import dev.apexnative.navigation.react.ScreenInsets
3
+ import dev.apexnative.navigation.react.ScreenEnvironment.Insets
4
4
 
5
5
  /**
6
6
  * The arithmetic of a screen's room, safe area and background on Android, without views: the screen nodes read the
@@ -19,6 +19,7 @@ internal object ScreenSafeArea {
19
19
  const val LEFT = "left"
20
20
 
21
21
  private val ALL_EDGES = setOf(TOP, RIGHT, BOTTOM, LEFT)
22
+ private val DEFAULT_EDGES = setOf(TOP, RIGHT, LEFT)
22
23
  private const val OPAQUE = 0xFF
23
24
 
24
25
  /**
@@ -57,10 +58,10 @@ internal object ScreenSafeArea {
57
58
  headerBottom: Int?,
58
59
  tabsTop: Int?,
59
60
  density: Float,
60
- ): ScreenInsets.Insets {
61
+ ): Insets {
61
62
  val coveredAbove = maxOf(system.top, headerBottom ?: 0)
62
63
  val coveredBelow = minOf(windowHeight - system.bottom, tabsTop ?: windowHeight)
63
- return ScreenInsets.Insets(
64
+ return Insets(
64
65
  top = dp((coveredAbove - root.top).coerceAtLeast(0), density),
65
66
  right = dp((root.right - (windowWidth - system.right)).coerceAtLeast(0), density),
66
67
  bottom = dp((root.bottom - coveredBelow).coerceAtLeast(0), density),
@@ -69,26 +70,43 @@ internal object ScreenSafeArea {
69
70
  }
70
71
 
71
72
  /**
72
- * The edges the screen's safe area view keeps its content off: exactly [safeAreaEdges] when the options give them;
73
- * else all four, but the top when the stack's header hides on scroll or a large title shows ([topFree]) and the
74
- * bottom when the tabs hide on scroll ([bottomFree]).
73
+ * The edges the screen's safe area view keeps its content off: exactly [safeAreaEdges] when the options give them,
74
+ * but the top under a large title ([largeTitle]); else the top and the sides, but the top when the stack's header
75
+ * hides on scroll or a large title shows ([topFree]). The bottom is free by default: the content goes down to the
76
+ * bottom, and a list keeps its last rows clear of the navigation bar with `useSafeAreaInsets()`.
75
77
  */
76
- fun paddedEdges(safeAreaEdges: List<String>?, topFree: Boolean, bottomFree: Boolean): Set<String> {
77
- if (safeAreaEdges != null) return safeAreaEdges.filterTo(LinkedHashSet()) { it in ALL_EDGES }
78
- val edges = LinkedHashSet(ALL_EDGES)
78
+ fun paddedEdges(safeAreaEdges: List<String>?, topFree: Boolean, largeTitle: Boolean): Set<String> {
79
+ if (safeAreaEdges != null) {
80
+ val edges = safeAreaEdges.filterTo(LinkedHashSet()) { it in ALL_EDGES }
81
+ // Under a large title the list keeps its first rows clear of the bar itself; a padded top would follow the
82
+ // header as it collapses and move the list with it.
83
+ if (largeTitle) edges -= TOP
84
+ return edges
85
+ }
86
+ val edges = LinkedHashSet(DEFAULT_EDGES)
79
87
  if (topFree) edges -= TOP
80
- if (bottomFree) edges -= BOTTOM
81
88
  return edges
82
89
  }
83
90
 
84
91
  /** The padding of the safe area view: the [insets] on the [edges] it keeps, 0 on the others. */
85
- fun padding(insets: ScreenInsets.Insets, edges: Set<String>): ScreenInsets.Insets = ScreenInsets.Insets(
92
+ fun padding(insets: Insets, edges: Set<String>): Insets = Insets(
86
93
  top = if (TOP in edges) insets.top else 0.0,
87
94
  right = if (RIGHT in edges) insets.right else 0.0,
88
95
  bottom = if (BOTTOM in edges) insets.bottom else 0.0,
89
96
  left = if (LEFT in edges) insets.left else 0.0,
90
97
  )
91
98
 
99
+ /**
100
+ * The insets JavaScript gets (`useSafeAreaInsets`): what of the [insets] still covers the content once the safe
101
+ * area view keeps it off its [padding], 0 on a padded edge.
102
+ */
103
+ fun uncovered(insets: Insets, padding: Insets) = Insets(
104
+ top = (insets.top - padding.top).coerceAtLeast(0.0),
105
+ right = (insets.right - padding.right).coerceAtLeast(0.0),
106
+ bottom = (insets.bottom - padding.bottom).coerceAtLeast(0.0),
107
+ left = (insets.left - padding.left).coerceAtLeast(0.0),
108
+ )
109
+
92
110
  /**
93
111
  * The inner padding of a screen's view: the side system bars ([systemBars]) at the sides; at the bottom the larger
94
112
  * of the navigation bar (none when the screen draws under it) and the keyboard ([imeBottom]; none with
@@ -10,11 +10,9 @@ import dev.apexnative.navigation.layout.LayoutException
10
10
  import dev.apexnative.navigation.nativescreen.NativeScreenContext
11
11
  import dev.apexnative.navigation.options.DefaultOptions
12
12
  import dev.apexnative.navigation.options.Options
13
- import dev.apexnative.navigation.react.ArrangementStates
14
13
  import dev.apexnative.navigation.react.CommandScope
15
14
  import dev.apexnative.navigation.react.ErrorTexts
16
- import dev.apexnative.navigation.react.ScreenInsets
17
- import dev.apexnative.navigation.react.ScreenRegions
15
+ import dev.apexnative.navigation.react.ScreenEnvironment
18
16
  import dev.apexnative.navigation.react.events.AppearanceDispatcher
19
17
  import dev.apexnative.navigation.react.events.ComponentType
20
18
  import dev.apexnative.navigation.react.events.ScreenAppearance
@@ -170,9 +168,7 @@ class ExternalComponentViewController internal constructor(
170
168
  peekView()?.let { (it.parent as? ViewGroup)?.removeView(it) }
171
169
  setParentController(null)
172
170
  if (isIdTakenByAnother) return
173
- ScreenInsets.remove(id)
174
- ScreenRegions.remove(id)
175
- ArrangementStates.remove(id)
171
+ ScreenEnvironment.remove(id)
176
172
  ScreenReadiness.remove(id)
177
173
  if (!notMade) AppearanceDispatcher.nativeScreenReleased(this)
178
174
  }
@@ -158,6 +158,9 @@ internal class ModalCoordinator(
158
158
 
159
159
  /** The record's views lay out again (the activity resumed). */
160
160
  fun requestLayout(record: ModalRecord) = Unit
161
+
162
+ /** The record's sheet as back sees it; none before its show and for a modal that isn't a sheet. */
163
+ fun sheetBack(record: ModalRecord): SheetBack? = null
161
164
  }
162
165
 
163
166
  /**
@@ -397,11 +400,26 @@ internal class ModalCoordinator(
397
400
  }
398
401
 
399
402
  /**
400
- * The root of the modal that back goes to first; none without one. A back gesture previews only a pop in it: its
401
- * dismissal has a preview of its own nowhere yet.
403
+ * The root of the modal that back goes to first; none without one. A back gesture previews a pop in it, or the
404
+ * modal's dismissal when it is a sheet ([backTargetSheet]).
402
405
  */
403
406
  fun backTargetRoot(): ViewController<*>? = backTarget()?.root
404
407
 
408
+ /**
409
+ * The sheet back would dismiss now, as [handleBack] would, and as a drag down of the user would: the top modal is
410
+ * a sheet that shows, its content can't go back (else back goes there, and a pop in it has its own preview),
411
+ * `modal.swipeToDismiss` isn't false, `hardwareBackButton.dismissModalOnPress` doesn't hold back, it isn't the only
412
+ * modal over no root, and its sheet would hide, not collapse, and isn't closing already ([SheetBack.hidesOnBack]).
413
+ * None otherwise. The back gesture previews its hide on Android 14+, and its release closes it as the user does.
414
+ */
415
+ fun backTargetSheet(): SheetBack? {
416
+ val record = backTarget()?.takeIf { it.isSheet && it.state == State.Shown } ?: return null
417
+ if (record.root.canHandleBack() || holdsBack(record) || isLastWithoutRoot(record)) return null
418
+ val options = record.root.resolveShownOptions(DefaultOptions.current)
419
+ if (options.modal.swipeToDismiss.isFalse) return null
420
+ return views.sheetBack(record)?.takeIf { it.hidesOnBack }
421
+ }
422
+
405
423
  /** The mirror of [handleBack], without acting; null without a modal to ask. */
406
424
  fun canHandleBack(): Boolean? {
407
425
  val record = backTarget() ?: return null
@@ -15,6 +15,7 @@ import android.view.ViewOutlineProvider
15
15
  import android.view.ViewTreeObserver
16
16
  import android.widget.FrameLayout
17
17
  import android.widget.ScrollView
18
+ import androidx.activity.BackEventCompat
18
19
  import androidx.coordinatorlayout.widget.CoordinatorLayout
19
20
  import androidx.core.graphics.Insets
20
21
  import androidx.core.view.ViewCompat
@@ -34,13 +35,14 @@ import kotlin.math.roundToInt
34
35
  * the detents of its options — `medium` at half the height, `large` at the full height under the status bar, a number
35
36
  * at that height in dp — and the user drags it between them. In a window wider than [MAX_WIDTH_DP] it stands in the
36
37
  * middle at that width. A drag down past the smallest one closes it, unless `swipeToDismiss` is false: then the sheet
37
- * stays, and the try is told. The screen under it stays in place.
38
+ * stays, and the try is told. The screen under it stays in place. On Android 14+ the back gesture previews its hide
39
+ * ([SheetBack]): the Material behavior shrinks it with the progress and slides it down at the release.
38
40
  */
39
41
  internal class ModalSheet(
40
42
  private val layout: CoordinatorLayout,
41
43
  private val content: View,
42
44
  private val events: Events,
43
- ) {
45
+ ) : SheetBack {
44
46
  interface Events {
45
47
  /** The user dragged the sheet to another detent. */
46
48
  fun onDetentChanged(detent: SheetDetent)
@@ -66,6 +68,28 @@ internal class ModalSheet(
66
68
  private var dragged = false
67
69
  private var shown: (() -> Unit)? = null
68
70
  private var closing: (() -> Unit)? = null
71
+ // The back gesture whose preview runs now; none.
72
+ private var backPreview: BackPreview? = null
73
+ // Released: the behavior slides the sheet down from where the preview left it, and hides it at the end.
74
+ private var backFinishing = false
75
+ // The window's observer the scrim follows the preview's frames through, from its start until the sheet is back
76
+ // in place or gone; none otherwise.
77
+ private var backFramesObserver: ViewTreeObserver? = null
78
+
79
+ // The behavior tells no slide while back's preview scales the sheet and its release slides it down (they move it by
80
+ // its transform, not its offset): the scrim follows what is drawn, frame by frame, until the sheet stands again.
81
+ private val backFrames = ViewTreeObserver.OnPreDrawListener {
82
+ updateScrim()
83
+ if (backPreview == null && !backFinishing && container.scaleY == 1f && container.translationY == 0f) {
84
+ stopBackFrames()
85
+ }
86
+ true
87
+ }
88
+
89
+ private val sheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
90
+ override fun onStateChanged(sheet: View, newState: Int) = onState(newState)
91
+ override fun onSlide(sheet: View, slideOffset: Float) = updateScrim()
92
+ }
69
93
 
70
94
  // A list inside the sheet scrolls with it, as the Material behavior expects: up at half height the sheet goes up
71
95
  // first, down at the top of the list the sheet comes down. The behavior follows views that scroll nested.
@@ -89,6 +113,13 @@ internal class ModalSheet(
89
113
  }
90
114
  })
91
115
  scrim.setOnTouchListener { _, event -> taps.onTouchEvent(event) }
116
+ // The sheet keeps a touch that lands on it and no view in it takes (its bar, an empty part), as Material's
117
+ // BottomSheetDialog does: else the touch falls to the scrim under it, and a tap there closes a dimmed sheet;
118
+ // past an undimmed scrim it reaches the behavior, which takes the sheet into its drag state at the touch's
119
+ // down, before any move. The back gesture starts with such a touch, and a sheet in the drag state has no
120
+ // preview. A drag that starts there still moves the sheet: the behavior takes it past the touch slop, as from
121
+ // the views in it.
122
+ container.setOnTouchListener { _, _ -> true }
92
123
  container.clipToOutline = true
93
124
  container.outlineProvider = object : ViewOutlineProvider() {
94
125
  // The bottom corners are under the screen's bottom: only the top ones round.
@@ -116,10 +147,7 @@ internal class ModalSheet(
116
147
  behavior.isHideable = true
117
148
  behavior.state = BottomSheetBehavior.STATE_HIDDEN
118
149
  behavior.halfExpandedRatio = 0.5f
119
- behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
120
- override fun onStateChanged(sheet: View, newState: Int) = onState(newState)
121
- override fun onSlide(sheet: View, slideOffset: Float) = updateScrim()
122
- })
150
+ behavior.addBottomSheetCallback(sheetCallback)
123
151
  container.viewTreeObserver.addOnGlobalLayoutListener(scrollViews)
124
152
  }
125
153
 
@@ -154,10 +182,12 @@ internal class ModalSheet(
154
182
  behavior.dragAnywhere = sheetOptions.expandsOnScroll.get(true)
155
183
  container.invalidateOutline()
156
184
  configure()
157
- if (merge == null || closing != null) return
185
+ if (merge == null || closing != null || backFinishing) return
158
186
  val settled = detentOf(behavior.state)
159
187
  if (settled != null) behavior.isHideable = swipeToDismiss && settled == detents.first()
160
188
  val target = SheetStops.target(settled, detents, merge.selectedDetent, sheetOptions.selectedDetent)
189
+ // A merge that moves the sheet or keeps it from going down ends a preview of back on it.
190
+ if (target != null || !behavior.isHideable) stopBackPreview()
161
191
  if (target != null) {
162
192
  selected = target
163
193
  behavior.state = stateOf(target)
@@ -172,12 +202,19 @@ internal class ModalSheet(
172
202
  finishClosing()
173
203
  return
174
204
  }
205
+ // Released by back, it goes down already: it is closed at the end of that slide.
206
+ if (backFinishing) return
207
+ stopBackPreview()
175
208
  behavior.isHideable = true
176
209
  behavior.state = BottomSheetBehavior.STATE_HIDDEN
177
210
  }
178
211
 
179
212
  /** Takes the sheet's views away; the modal's view is the caller's. */
180
213
  fun remove() {
214
+ // A slide down of back's release that ends after this tells nothing: the modal went already.
215
+ behavior.removeBottomSheetCallback(sheetCallback)
216
+ stopBackPreview()
217
+ stopBackFrames()
181
218
  container.viewTreeObserver.removeOnGlobalLayoutListener(scrollViews)
182
219
  container.removeView(content)
183
220
  layout.removeView(container)
@@ -191,6 +228,69 @@ internal class ModalSheet(
191
228
  TestIds.apply(scrim, testId?.let { "$it.scrim" })
192
229
  }
193
230
 
231
+ override val hidesOnBack: Boolean
232
+ get() = closing == null && !backFinishing && behavior.isHideable && container.isLaidOut &&
233
+ detentOf(behavior.state) != null
234
+
235
+ override fun startBackPreview(event: BackEventCompat): SheetBack.Gesture? {
236
+ if (!hidesOnBack || backPreview != null) return null
237
+ val preview = BackPreview()
238
+ backPreview = preview
239
+ startBackFrames()
240
+ behavior.startBackProgress(event)
241
+ return preview
242
+ }
243
+
244
+ // A command closed or moved the sheet, or it goes, while back's preview runs: the sheet grows back where it stands
245
+ // (the behavior's cancel), and the gesture's release does the usual back.
246
+ private fun stopBackPreview() {
247
+ if (backPreview == null) return
248
+ backPreview = null
249
+ behavior.cancelBackProgress()
250
+ }
251
+
252
+ private fun startBackFrames() {
253
+ if (backFramesObserver != null) return
254
+ backFramesObserver = container.viewTreeObserver.also { it.addOnPreDrawListener(backFrames) }
255
+ }
256
+
257
+ // The listener leaves the observer it joined: a container out of the window has another one.
258
+ private fun stopBackFrames() {
259
+ val observer = backFramesObserver ?: return
260
+ backFramesObserver = null
261
+ if (observer.isAlive) observer.removeOnPreDrawListener(backFrames)
262
+ }
263
+
264
+ /** The back gesture over this sheet: the Material behavior previews its hide (`MaterialBackHandler`). */
265
+ private inner class BackPreview : SheetBack.Gesture {
266
+ override val sheet: SheetBack
267
+ get() = this@ModalSheet
268
+
269
+ override fun progress(event: BackEventCompat) {
270
+ if (backPreview !== this) return
271
+ behavior.updateBackProgress(event)
272
+ updateScrim()
273
+ }
274
+
275
+ override fun cancel() {
276
+ if (backPreview === this) stopBackPreview()
277
+ }
278
+
279
+ // The behavior slides the sheet down from the preview and sets its hidden state at the end: `onState` closes
280
+ // it there as a drag down does (`onClosedByUser`), so the modal goes through the user's path once.
281
+ override fun commit(): Boolean {
282
+ if (backPreview !== this) return false
283
+ backPreview = null
284
+ if (!hidesOnBack) {
285
+ behavior.cancelBackProgress()
286
+ return false
287
+ }
288
+ backFinishing = true
289
+ behavior.handleBackInvoked()
290
+ return true
291
+ }
292
+ }
293
+
194
294
  private fun closeByUser() {
195
295
  if (!swipeToDismiss) {
196
296
  events.onCloseAttempted()
@@ -243,6 +343,7 @@ internal class ModalSheet(
243
343
  when (state) {
244
344
  BottomSheetBehavior.STATE_DRAGGING -> dragged = true
245
345
  BottomSheetBehavior.STATE_HIDDEN -> {
346
+ backFinishing = false
246
347
  when {
247
348
  closing != null -> finishClosing()
248
349
  else -> {
@@ -280,13 +381,23 @@ internal class ModalSheet(
280
381
  // The screen under the sheet dims as the sheet goes up past `largestUndimmedDetent` (from the bottom without
281
382
  // one), to the dim of a Material modal sheet at the next detent; while it isn't dimmed, it takes touches.
282
383
  private fun updateScrim() {
283
- val height = layout.height - container.top
384
+ // Hidden at the end of back's slide, it still stands at its detent until the next layout.
385
+ if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) return setDim(0f)
386
+ val height = layout.height - sheetTop()
284
387
  val undimmed = options.largestUndimmedDetent?.takeIf { it in detents }
285
388
  val from = undimmed?.let { heightOf(it) } ?: 0
286
389
  val to = detents.firstOrNull { heightOf(it) > from }?.let { heightOf(it) } ?: return run { setDim(0f) }
287
390
  setDim(((height - from).toFloat() / (to - from)).coerceIn(0f, 1f))
288
391
  }
289
392
 
393
+ // The top of the sheet as drawn: its offset, and while back's preview moves it, its slide and its scale toward
394
+ // the bottom (the behavior's pivot).
395
+ private fun sheetTop(): Int {
396
+ if (backFramesObserver == null) return container.top
397
+ val scaled = container.pivotY * (1f - container.scaleY)
398
+ return (container.top + container.translationY + scaled).roundToInt()
399
+ }
400
+
290
401
  private fun setDim(fraction: Float) {
291
402
  scrim.alpha = fraction * SCRIM_ALPHA
292
403
  }
@@ -19,6 +19,7 @@ import dev.apexnative.navigation.options.Options
19
19
  import dev.apexnative.navigation.utils.TestIds
20
20
  import dev.apexnative.navigation.utils.dp
21
21
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
22
+ import dev.apexnative.navigation.views.animations.AnimationLayer
22
23
  import dev.apexnative.navigation.views.animations.AnimationRules
23
24
  import dev.apexnative.navigation.views.animations.SideAnimator
24
25
  import dev.apexnative.navigation.views.element.ElementTransition
@@ -263,6 +264,8 @@ internal class ModalViews(
263
264
  parts[record]?.container?.requestLayout()
264
265
  }
265
266
 
267
+ override fun sheetBack(record: ModalRecord): SheetBack? = parts[record]?.sheet
268
+
266
269
  // `modal.testID` on the modal's view; a sheet's on the sheet's container, `modal.sheet.testID` first, and its
267
270
  // grabber and scrim after it.
268
271
  private fun applyTestIds(record: ModalRecord, parts: Parts, options: Options) {
@@ -293,7 +296,11 @@ internal class ModalViews(
293
296
  val under = motion.underSide?.let { side -> underView?.let { SideAnimator.build(it, side, underBase) } }
294
297
  if (under != null && underView != null) {
295
298
  if (showing) moved[underView] = record else moved.remove(underView)
299
+ AnimationLayer.during(underView, under)
296
300
  }
301
+ // A fade of the modal or of the layer under it draws it through a hardware layer, given back at its end and at
302
+ // any stop (the set's cancel cancels it).
303
+ for (animator in listOfNotNull(side, fade)) AnimationLayer.during(container, animator)
297
304
  val fadeLength = motion.side?.takeIf(AnimationRules::isSingleAlpha)?.let(AnimationRules::lengthOf)
298
305
  val elements = motion.elements?.let { animation ->
299
306
  val content = modalsLayer.parent as? ViewGroup ?: return@let null
@@ -0,0 +1,42 @@
1
+ package dev.apexnative.navigation.viewcontrollers.modal
2
+
3
+ import androidx.activity.BackEventCompat
4
+
5
+ /**
6
+ * A modal's sheet as back sees it ([ModalSheet]): whether a release of back would hide it, and the preview of that
7
+ * hide during the back gesture (Android 14+). [ModalCoordinator.backTargetSheet] gives the one back would dismiss now.
8
+ * Main thread only.
9
+ */
10
+ internal interface SheetBack {
11
+ /**
12
+ * Back's release would hide the sheet, as a drag down does: it stands at a detent from which it goes down (its
13
+ * smallest, while it lets the user close it), laid out, and it isn't closing already. False at a larger detent:
14
+ * the Material behavior would only collapse it there.
15
+ */
16
+ val hidesOnBack: Boolean
17
+
18
+ /**
19
+ * Starts the preview of back on the sheet with the gesture's first [event]: the sheet shrinks a little toward the
20
+ * bottom with the progress, the scrim following it. None when [hidesOnBack] doesn't hold or a preview runs.
21
+ */
22
+ fun startBackPreview(event: BackEventCompat): Gesture?
23
+
24
+ /** A back gesture over the sheet ([startBackPreview]). */
25
+ interface Gesture {
26
+ /** The sheet the gesture previews. */
27
+ val sheet: SheetBack
28
+
29
+ /** The gesture moved. */
30
+ fun progress(event: BackEventCompat)
31
+
32
+ /** Cancelled, or back went elsewhere: the sheet grows back to where it stood, the scrim with it. */
33
+ fun cancel()
34
+
35
+ /**
36
+ * Released: the sheet slides down from where the preview left it and goes as one the user dragged down (its
37
+ * events, its dismissal). False when the preview stopped since it started (a command closed or moved the
38
+ * sheet): back goes the usual way.
39
+ */
40
+ fun commit(): Boolean
41
+ }
42
+ }
@@ -0,0 +1,50 @@
1
+ package dev.apexnative.navigation.viewcontrollers.navigator
2
+
3
+ import android.util.Log
4
+ import dev.apexnative.navigation.utils.NavigationLog
5
+
6
+ /**
7
+ * The check of [BackCallbackState]'s triggers in debug builds (`NavigationActivity` makes it only when
8
+ * `BuildConfig.DEBUG`): it compares an activity's back callback with what back does now
9
+ * ([BackCallbackState.mismatchOf]) and writes a mismatch to the log once per state, with the screen on top. It never
10
+ * changes the callback, so a missing trigger behaves as in a release build. A [sample] before a frame runs at most
11
+ * every [SAMPLE_MILLIS] and reports a mismatch only when the sample before saw the same one: a change whose trigger
12
+ * comes at the end of its motion (a pane that slides, a menu dragged open) isn't reported while it moves. A back event
13
+ * ([atBack]) reports at once. Main thread only.
14
+ */
15
+ internal class BackCallbackCheck(
16
+ private val log: (String) -> Unit = { Log.w(NavigationLog.TAG, it) },
17
+ ) {
18
+ private var lastSample: Long? = null
19
+ private var previous: String? = null
20
+ private val logged = HashSet<String>()
21
+
22
+ /** Before a frame of the activity of [navigator], at [now] (`SystemClock.uptimeMillis`). */
23
+ fun sample(navigator: Navigator, now: Long) {
24
+ val last = lastSample
25
+ if (last != null && now - last < SAMPLE_MILLIS) return
26
+ lastSample = now
27
+ val mismatch = BackCallbackState.mismatchOf(navigator)
28
+ if (mismatch != null && mismatch == previous) report(mismatch)
29
+ previous = mismatch
30
+ }
31
+
32
+ /** A back event reached the callback of the activity of [navigator], before it acts. */
33
+ fun atBack(navigator: Navigator) {
34
+ BackCallbackState.mismatchOf(navigator)?.let(::report)
35
+ }
36
+
37
+ // Once per state, and not without end.
38
+ private fun report(mismatch: String) {
39
+ if (logged.size >= MAX_LOGGED || !logged.add(mismatch)) return
40
+ log("[ApexNavigation] $mismatch: a change of what back does came without its trigger")
41
+ }
42
+
43
+ companion object {
44
+ /** The least time between two samples. */
45
+ const val SAMPLE_MILLIS = 500L
46
+
47
+ // The states logged at most: a debug check, not a second log.
48
+ private const val MAX_LOGGED = 32
49
+ }
50
+ }
@@ -2,21 +2,38 @@ package dev.apexnative.navigation.viewcontrollers.navigator
2
2
 
3
3
  import android.util.Log
4
4
  import androidx.activity.OnBackPressedCallback
5
+ import dev.apexnative.navigation.react.events.AppearanceDispatcher
5
6
  import dev.apexnative.navigation.utils.NavigationLog
6
7
  import java.lang.ref.WeakReference
7
8
 
8
9
  /**
9
10
  * Keeps the back callback of each navigation activity enabled only while back has something to do in the app: its
10
- * navigation can go back ([Navigator.canHandleBack]), or JavaScript listens to it (`BackHandler`). Otherwise the
11
- * callback is disabled and the system does its own back. The system plays its back animation (to the home screen, to
12
- * another task) only when no callback of the app is enabled as the gesture starts, and it decides that before the app
13
- * hears of the gesture.
11
+ * navigation can go back ([Navigator.canHandleBack]), the screen back acts on keeps it from the system
12
+ * (`hardwareBackButton.systemAnimation: false`, [Navigator.keepsBackFromSystem]), or JavaScript listens to it
13
+ * (`BackHandler`). Otherwise the callback is disabled and the system does its own back. The system plays its back
14
+ * animation (to the home screen, to another task) only when no callback of the app is enabled as the gesture starts,
15
+ * and it decides that before the app hears of the gesture.
14
16
  *
15
17
  * A stale "enabled" only loses that animation; a stale "disabled" would leave the app where back has to go back. So
16
- * the value is computed again whenever what back does can have changed: before each frame of the activity's window,
17
- * when a command is done, when JavaScript reports its listeners, and when a side menu, the panes of a split view or a
18
- * search open or close (the first two after their last frame). Each activity has its own navigator and callback; the
19
- * listeners of JavaScript count for all of them. Main thread only.
18
+ * the value is computed again on every event that can change what back does, never by walking the layouts before
19
+ * each frame:
20
+ * - the end of each command's main-thread task, `setDefaultOptions` and `mergeOptions` too (`CommandRunner`);
21
+ * - a stack's operation accepted, its current child changed, and the operation ended or rejected
22
+ * (`StackController`'s queue host), which covers a push or a `setStackRoot` that waited for its screen, a prepared
23
+ * push, back of the user and the back arrow;
24
+ * - a layout that changed the screens it shows (`Navigator.shownScreensChanged`: the tabs, a side menu, a split view,
25
+ * a pair) and the end of a tab's switch;
26
+ * - every change of the modals (`ModalCoordinator.Host.backChanged`): a show, a dismissal by a command, by back or by
27
+ * the user (a sheet dragged down, its scrim), a failure, a `setRoot` that takes them;
28
+ * - a root that changed or showed, and the layers that went (a reload: `Navigator.destroy`);
29
+ * - a side menu, the panes of a split view or a search that opened or closed (the first two after their last frame),
30
+ * and a split view's width that changed;
31
+ * - the release and the cancel of a back gesture, and each back press;
32
+ * - JavaScript's listeners ([setJsListeners]), and the activity resumed or focused, as a safety net.
33
+ *
34
+ * Overlays don't count: back never goes to them. Each activity has its own navigator and callback; the listeners of
35
+ * JavaScript count for all of them. Debug builds check the triggers ([mismatchOf], `BackCallbackCheck`). Main thread
36
+ * only.
20
37
  */
21
38
  internal object BackCallbackState {
22
39
  private class Entry(navigator: Navigator, callback: OnBackPressedCallback) {
@@ -56,22 +73,36 @@ internal object BackCallbackState {
56
73
  for (entry in entries.toList()) update(entry)
57
74
  }
58
75
 
59
- /** The callback of the activity of [navigator]: before its frames and at its back presses. */
76
+ /** The callback of the activity of [navigator]: after a change in its navigation. */
60
77
  fun update(navigator: Navigator) {
61
78
  entries.firstOrNull { it.navigator.get() === navigator }?.let(::update)
62
79
  }
63
80
 
81
+ /**
82
+ * Debug builds only: a callback of [navigator]'s activity that differs from what back does now — a change without
83
+ * its trigger —, described with the screen on top; null when it is right or not attached. It changes nothing.
84
+ */
85
+ fun mismatchOf(navigator: Navigator): String? {
86
+ val callback = entries.firstOrNull { it.navigator.get() === navigator }?.callback?.get() ?: return null
87
+ val expected = jsListeners || takesBack(navigator)
88
+ if (callback.isEnabled == expected) return null
89
+ val top = navigator.visibleLayout?.let { AppearanceDispatcher.visibleNode(it).id } ?: "none"
90
+ val state = if (callback.isEnabled) "enabled" else "disabled"
91
+ val back = if (expected) "back stays in the app" else "back leaves the app"
92
+ return "the back callback is $state where $back (on top: '$top')"
93
+ }
94
+
64
95
  // Enables or disables the callback when what back does changed.
65
96
  private fun update(entry: Entry) {
66
97
  val callback = entry.callback.get() ?: return
67
- val enabled = jsListeners || canHandleBack(entry.navigator.get())
98
+ val enabled = jsListeners || takesBack(entry.navigator.get())
68
99
  if (callback.isEnabled != enabled) callback.isEnabled = enabled
69
100
  }
70
101
 
71
- // A layout that fails to answer leaves back to the navigation, as before: this runs before each frame, and an
72
- // exception here would end the app.
73
- private fun canHandleBack(navigator: Navigator?): Boolean = try {
74
- navigator?.canHandleBack() == true
102
+ // A layout that fails to answer leaves back to the navigation, as before: this runs from the triggers of layouts
103
+ // and animators, and an exception here would end the app.
104
+ private fun takesBack(navigator: Navigator?): Boolean = try {
105
+ navigator != null && (navigator.canHandleBack() || navigator.keepsBackFromSystem())
75
106
  } catch (e: RuntimeException) {
76
107
  if (!failureLogged) {
77
108
  failureLogged = true