@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
@@ -0,0 +1,262 @@
1
+ package dev.apexnative.navigation.utils
2
+
3
+ import android.content.ComponentCallbacks2
4
+ import android.content.Context
5
+ import android.content.res.Configuration
6
+ import android.content.res.Resources
7
+ import android.graphics.BitmapFactory
8
+ import android.graphics.drawable.BitmapDrawable
9
+ import android.graphics.drawable.Drawable
10
+ import android.net.Uri
11
+ import android.os.StrictMode
12
+ import android.util.LruCache
13
+ import androidx.core.content.ContextCompat
14
+ import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper
15
+ import java.io.FileInputStream
16
+ import java.io.FileNotFoundException
17
+ import java.io.IOException
18
+ import java.io.InputStream
19
+ import java.net.URL
20
+ import java.util.Collections
21
+ import java.util.concurrent.ExecutorService
22
+ import java.util.concurrent.Executors
23
+
24
+ /**
25
+ * The images and icons of the options, every one of them (bar buttons and their menus, the back button, search
26
+ * suggestions, tab icons, the FAB): one cache for the app, by [ImageKey], sized by the bytes of its images and let go
27
+ * when the system is low on memory with the app in the background ([clearsOnTrim]), not each time the app's UI hides;
28
+ * each caller gets its own mutable copy. The images don't depend on the JavaScript, so a reload keeps them.
29
+ *
30
+ * A `file:` address is a file; anything else first a drawable of the app by name (as React Native's images by resource
31
+ * name: release builds' assets, `{ android }` icons). Those two load on the caller's thread ([load]) or on a
32
+ * background thread of their own ([loadLater]). Not found while the app is in development: the address is read as a
33
+ * stream on the main thread, Metro's asset over the network (network on the main thread allowed for that read only)
34
+ * or through the content resolver. A remote address that is not Metro's asset is never loaded. A failure (a missing
35
+ * file or drawable, data that doesn't decode, an image too large for the memory) never ends the app: no image, and one
36
+ * warning per address in the log (and LogBox in development).
37
+ *
38
+ * A theme attribute inside a drawable of the app is resolved against the theme of the context that loaded it first.
39
+ * Main thread, unless a member says otherwise.
40
+ */
41
+ internal object OptionImages {
42
+ private val cache = object : LruCache<ImageKey, Entry>(maxBytes()) {
43
+ override fun sizeOf(key: ImageKey, value: Entry) = value.bytes
44
+ }
45
+
46
+ // The addresses already named in a warning: one each.
47
+ private val warned: MutableSet<String> = Collections.synchronizedSet(HashSet())
48
+
49
+ // The loads off the main thread under way, with who waits for each.
50
+ private val loading = HashMap<ImageKey, MutableList<Waiter>>()
51
+
52
+ private val background: ExecutorService by lazy {
53
+ Executors.newSingleThreadExecutor { task -> Thread(task, THREAD_NAME).apply { isDaemon = true } }
54
+ }
55
+
56
+ @Volatile
57
+ private var listening = false
58
+
59
+ /** The cache and the loads for [context]: the source of an [ImageRequests]. */
60
+ fun source(context: Context): ImageSource = object : ImageSource {
61
+ override fun key(uri: String) = ImageKey.of(context, uri)
62
+
63
+ override fun cached(key: ImageKey) = cached(context, key)
64
+
65
+ override fun now(key: ImageKey) = load(context, key)
66
+
67
+ override fun loadsLater(uri: String) = loadsLater(context, uri)
68
+
69
+ override fun later(key: ImageKey, done: (Drawable?) -> Unit) = loadLater(context, key, done)
70
+ }
71
+
72
+ /** A copy of the image of [key] when the cache has it. Any thread. */
73
+ fun cached(context: Context, key: ImageKey): Drawable? = cache.get(key)?.copy(context)
74
+
75
+ /** A copy of the image of [uri], from the cache or loaded on the caller's thread; none on a failure. */
76
+ fun load(context: Context, uri: String): Drawable? = load(context, ImageKey.of(context, uri))
77
+
78
+ /**
79
+ * A copy of the image of [key] (built in the configuration of [context]), from the cache or loaded on the caller's
80
+ * thread; none on a failure.
81
+ */
82
+ fun load(context: Context, key: ImageKey): Drawable? {
83
+ listen(context)
84
+ cache.get(key)?.let { return it.copy(context) }
85
+ val loaded = decodeOrWarn(context, key.uri) ?: return null
86
+ return keep(key, loaded)?.copy(context) ?: loaded
87
+ }
88
+
89
+ /** Whether [uri] loads off the main thread: a file, or a drawable the app has by that name. Any thread. */
90
+ fun loadsLater(context: Context, uri: String): Boolean =
91
+ uri.startsWith(FILE_SCHEME) || ResourceDrawableIdHelper.getResourceDrawableId(context, uri) > 0
92
+
93
+ /**
94
+ * Loads [key] (built in the configuration of [context]) on the background thread and gives [done] a copy of it, or
95
+ * none on a failure, on the main thread; one load for all who ask for the same image meanwhile. Only for what
96
+ * [loadsLater].
97
+ */
98
+ fun loadLater(context: Context, key: ImageKey, done: (Drawable?) -> Unit) {
99
+ listen(context)
100
+ val waiters = loading[key]
101
+ if (waiters != null) {
102
+ waiters.add(Waiter(context, done))
103
+ return
104
+ }
105
+ loading[key] = mutableListOf(Waiter(context, done))
106
+ background.execute {
107
+ val loaded = decodeOrWarn(context, key.uri)
108
+ val entry = loaded?.let { keep(key, it) }
109
+ UiThread.post { deliver(key, loaded, entry) }
110
+ }
111
+ }
112
+
113
+ /** Lets every image of the cache go. Any thread. */
114
+ fun clear() {
115
+ cache.evictAll()
116
+ }
117
+
118
+ /**
119
+ * Whether the cache lets its images go at a trim of [level]: when the system is low on memory with the app in the
120
+ * background (`TRIM_MEMORY_BACKGROUND` and above), not at `TRIM_MEMORY_UI_HIDDEN`, which comes each time the app
121
+ * goes to the background: its icons are there when it comes back.
122
+ */
123
+ fun clearsOnTrim(level: Int): Boolean = level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND
124
+
125
+ // Each waiter gets a copy of its own; an image the cache can't keep (no constant state) goes to the first only,
126
+ // the others load it again.
127
+ private fun deliver(key: ImageKey, loaded: Drawable?, entry: Entry?) {
128
+ val waiters = loading.remove(key) ?: return
129
+ waiters.forEachIndexed { index, waiter ->
130
+ val image = when {
131
+ loaded == null -> null
132
+ entry != null -> entry.copy(waiter.context)
133
+ index == 0 -> loaded
134
+ else -> load(waiter.context, key)
135
+ }
136
+ waiter.done(image)
137
+ }
138
+ }
139
+
140
+ // Into the cache, when the image can be copied; the entry it keeps. Any thread.
141
+ private fun keep(key: ImageKey, image: Drawable): Entry? {
142
+ val state = image.constantState ?: return null
143
+ return Entry(state, bytesOf(image)).also { cache.put(key, it) }
144
+ }
145
+
146
+ private fun bytesOf(image: Drawable): Int {
147
+ if (image is BitmapDrawable) image.bitmap?.let { return it.allocationByteCount }
148
+ // A drawable that draws itself (a vector) keeps a picture of its own size when it is drawn.
149
+ return image.intrinsicWidth.coerceAtLeast(1) * image.intrinsicHeight.coerceAtLeast(1) * BYTES_PER_PIXEL
150
+ }
151
+
152
+ // The image of [uri], or none and one warning per address. Any thread.
153
+ private fun decodeOrWarn(context: Context, uri: String): Drawable? = try {
154
+ decode(context, uri)
155
+ } catch (e: Exception) {
156
+ warnOnce(uri, (e as? LoadFailure)?.why ?: ": ${e.message ?: e.javaClass.simpleName}")
157
+ null
158
+ } catch (e: OutOfMemoryError) {
159
+ warnOnce(uri, ": it is too large to decode")
160
+ null
161
+ }
162
+
163
+ private fun warnOnce(uri: String, why: String) {
164
+ if (warned.add(uri)) DevelopmentLog.warn("Could not load image $uri$why")
165
+ }
166
+
167
+ private fun decode(context: Context, uri: String): Drawable {
168
+ if (uri.startsWith(FILE_SCHEME)) return decodeStream(context) { FileInputStream(Uri.parse(uri).path) }
169
+ resourceDrawable(context, uri)?.let { return it }
170
+ if (!DevelopmentLog.isDevelopment) throw LoadFailure("")
171
+ val address = Uri.parse(uri)
172
+ return when (address.scheme) {
173
+ "http", "https" -> {
174
+ if (!isMetroAsset(address)) throw LoadFailure(": remote images are not supported in options")
175
+ withNetworkOnMainThread { decodeStream(context) { URL(uri).openStream() } }
176
+ }
177
+ else -> decodeStream(context) {
178
+ context.contentResolver.openInputStream(address) ?: throw FileNotFoundException(uri)
179
+ }
180
+ }
181
+ }
182
+
183
+ // A drawable of the app by name; none when the app has none by that name.
184
+ private fun resourceDrawable(context: Context, name: String): Drawable? {
185
+ val id = ResourceDrawableIdHelper.getResourceDrawableId(context, name)
186
+ if (id <= 0) return null
187
+ return try {
188
+ ContextCompat.getDrawable(context, id)
189
+ } catch (e: Resources.NotFoundException) {
190
+ null
191
+ }
192
+ }
193
+
194
+ // The image of the stream [open] gives, closed after the read, also on an error; one it can't decode fails, and so
195
+ // does one too large for the memory.
196
+ private fun decodeStream(context: Context, open: () -> InputStream): Drawable {
197
+ val decoded = try {
198
+ open().use { BitmapFactory.decodeStream(it) }
199
+ } catch (e: OutOfMemoryError) {
200
+ throw LoadFailure(": it is too large to decode")
201
+ }
202
+ val bitmap = decoded ?: throw LoadFailure(": it can't be decoded")
203
+ return BitmapDrawable(context.resources, bitmap)
204
+ }
205
+
206
+ // Metro's asset: an address of the server the JavaScript bundle came from, under `/assets/`.
207
+ private fun isMetroAsset(address: Uri): Boolean {
208
+ val bundle = DevelopmentLog.bundleUrl?.let(Uri::parse) ?: return false
209
+ return address.host == bundle.host && address.port == bundle.port &&
210
+ address.path.orEmpty().startsWith(ASSETS_PATH)
211
+ }
212
+
213
+ // Reading from the network on the main thread for [read] only; the thread's policy comes back, also on an error.
214
+ private fun <T> withNetworkOnMainThread(read: () -> T): T {
215
+ val policy = StrictMode.getThreadPolicy()
216
+ StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder(policy).permitNetwork().build())
217
+ try {
218
+ return read()
219
+ } finally {
220
+ StrictMode.setThreadPolicy(policy)
221
+ }
222
+ }
223
+
224
+ // The cache lets its images go when the system is low on memory ([clearsOnTrim]): they load again when asked.
225
+ private fun listen(context: Context) {
226
+ if (listening) return
227
+ synchronized(this) {
228
+ if (listening) return
229
+ listening = true
230
+ }
231
+ context.applicationContext.registerComponentCallbacks(object : ComponentCallbacks2 {
232
+ override fun onTrimMemory(level: Int) {
233
+ if (clearsOnTrim(level)) clear()
234
+ }
235
+
236
+ override fun onConfigurationChanged(newConfig: Configuration) = Unit
237
+
238
+ @Deprecated("Android 14 deprecates it; onTrimMemory comes with it")
239
+ override fun onLowMemory() = clear()
240
+ })
241
+ }
242
+
243
+ // A sixteenth of the app's heap, at most 16 MB: the options' icons are small.
244
+ private fun maxBytes(): Int = (Runtime.getRuntime().maxMemory() / HEAP_PART).coerceAtMost(MAX_BYTES).toInt()
245
+
246
+ // An image the cache keeps: what makes its copies, and its size.
247
+ private class Entry(val state: Drawable.ConstantState, val bytes: Int) {
248
+ fun copy(context: Context): Drawable = state.newDrawable(context.resources).mutate()
249
+ }
250
+
251
+ private class Waiter(val context: Context, val done: (Drawable?) -> Unit)
252
+
253
+ // A failure the loader names itself: `Could not load image <source>`, then [why].
254
+ private class LoadFailure(val why: String) : IOException("Could not load image$why")
255
+
256
+ private const val FILE_SCHEME = "file:"
257
+ private const val ASSETS_PATH = "/assets/"
258
+ private const val THREAD_NAME = "ApexNavigation images"
259
+ private const val BYTES_PER_PIXEL = 4
260
+ private const val HEAP_PART = 16L
261
+ private const val MAX_BYTES = 16L * 1024 * 1024
262
+ }
@@ -59,7 +59,9 @@ private fun ViewController<*>.hostIsShown(): Boolean {
59
59
  * Runs [mounted] as the content of this screen's slot mounts into its container, and [start] on the pre-draw of that
60
60
  * frame: after React Native applied the whole mount, before the frame is drawn. At once if the content has mounted,
61
61
  * after [DRAW_WAIT_LIMIT] if it never does; while [waiting] holds. A screen prepared for its push was rendered at its
62
- * size and insets before: in that frame it is whole.
62
+ * size and insets before, so it is whole from that frame on; its push doesn't start in that frame: [start] hands it to
63
+ * the stack's queue, which starts the transition on a later turn, behind the calls JavaScript made before
64
+ * (`PendingCalls`), so the `mergeOptions` of the mount's commit has applied.
63
65
  */
64
66
  fun ViewController<*>.whenContentMounted(waiting: () -> Boolean, mounted: () -> Unit, start: () -> Unit) {
65
67
  var done = false
@@ -0,0 +1,49 @@
1
+ package dev.apexnative.navigation.viewcontrollers.bottomtabs
2
+
3
+ import dev.apexnative.navigation.utils.ImageKey
4
+
5
+ /**
6
+ * The last call of each kind for each item of the bar, by which [BottomTabsPresenter.run] skips a call equal to it: an
7
+ * icon isn't loaded and set again as a screen appears. The badge and the font always go (their defaults are read at
8
+ * each set). An icon whose load gave no image goes again ([failed]); a `file:` icon compares by its [ImageKey] (the
9
+ * file's modification time and size, read by [fileKey]), so a file written again at the same path shows. Main thread.
10
+ */
11
+ internal class AppliedTabCalls(private val fileKey: (String) -> ImageKey) {
12
+ /** A call that runs: [isChange], the item had a call of its kind before; [image], the key of a `file:` icon. */
13
+ class Next(val isChange: Boolean, val image: ImageKey?)
14
+
15
+ private class Applied(val call: TabItems.Call, val image: ImageKey?, var failed: Boolean = false)
16
+
17
+ private val last = HashMap<Pair<Int, Class<*>>, Applied>()
18
+
19
+ /** How [call] runs, and it is the last of its kind for its item from now on; none when it is skipped. */
20
+ fun next(call: TabItems.Call): Next? {
21
+ val key = call.index to call.javaClass
22
+ val before = last[key]
23
+ val image = uriOf(call)?.takeIf { it.startsWith(FILE_SCHEME) }?.let(fileKey)
24
+ val always = call is TabItems.Call.Badge || call is TabItems.Call.Font
25
+ if (!always && before != null && before.call == call && !before.failed && before.image == image) return null
26
+ last[key] = Applied(call, image)
27
+ return Next(isChange = before != null, image = image)
28
+ }
29
+
30
+ /** The icon of [call] didn't load: the same call goes again. */
31
+ fun failed(call: TabItems.Call) {
32
+ last[call.index to call.javaClass]?.takeIf { it.call == call }?.failed = true
33
+ }
34
+
35
+ /** The bar goes: every call goes again. */
36
+ fun clear() {
37
+ last.clear()
38
+ }
39
+
40
+ private fun uriOf(call: TabItems.Call): String? = when (call) {
41
+ is TabItems.Call.Icon -> call.uri
42
+ is TabItems.Call.SelectedIcon -> call.uri
43
+ else -> null
44
+ }
45
+
46
+ private companion object {
47
+ const val FILE_SCHEME = "file:"
48
+ }
49
+ }
@@ -74,6 +74,10 @@ class BottomTabsController(
74
74
  private var pendingSelected: (() -> Unit)? = null
75
75
  // Ends the fade through of a switch still running, with its events.
76
76
  private var endSwitch: (() -> Unit)? = null
77
+
78
+ // The made tabs that didn't show at the last repaint (new default options, a light/dark switch): each is repainted
79
+ // as it is selected, not while hidden.
80
+ private val staleTabs = mutableSetOf<ViewController<*>>()
77
81
  // The bar goes with a stack's transition: the options of the new top don't move it themselves.
78
82
  private var riding = false
79
83
  private var restAttached = false
@@ -247,7 +251,7 @@ class BottomTabsController(
247
251
  // `mergeOptions` with the tabs' id: their own options, then what they show, then the layouts around without
248
252
  // `bottomTabs`; the one-shot options act once, also while the view isn't shown.
249
253
  override fun mergeOptions(options: Options) {
250
- this.options = this.options.copy().mergeWith(options)
254
+ mergeOwnOptions(options)
251
255
  applyMerge(options, null)
252
256
  performOnParentController { it.mergeChildOptions(withoutBottomTabs(options), this) }
253
257
  }
@@ -311,8 +315,10 @@ class BottomTabsController(
311
315
  private enum class How { Press, Code, Back }
312
316
 
313
317
  /**
314
- * A tap on the tab at [index], on the bar or on its cell of the custom row: its `ANN.BottomTabPressed` first, then
315
- * the selection, the repeat, or nothing, as its resolved options say.
318
+ * A tap on the tab at [index], on the bar or on its cell of the custom row, the one path of both: a switch still
319
+ * running ends first, with its `ANN.BottomTabSelected`; then this press's `ANN.BottomTabPressed`, then the
320
+ * selection (`ANN.BottomTabSelected` after the new screen's DidAppear), the repeat (the pop's own events), or
321
+ * nothing, as its resolved options say. The same order as `-[ANNBottomTabsController pressTabAtIndex:source:]`.
316
322
  */
317
323
  internal fun onTabPressed(index: Int) {
318
324
  if (isDestroyed || index !in tabs.indices) return
@@ -363,6 +369,9 @@ class BottomTabsController(
363
369
  }
364
370
  selectedIndex = index
365
371
  attach(index)
372
+ // A tab hidden at a repaint takes it now, before its screens appear: its bar's parts, its search's and its
373
+ // FAB's colors follow the theme and the defaults only through a repaint.
374
+ if (staleTabs.remove(shown)) Navigator.repaintLayer(shown)
366
375
  peekView()?.bar?.let {
367
376
  it.setCurrentItem(index, false)
368
377
  it.onCustomItemViewSelectionChanged(index)
@@ -592,8 +601,8 @@ class BottomTabsController(
592
601
  }
593
602
  }
594
603
 
595
- // The repaint: the bar, the items of every tab, the row, then the made tabs that don't show, which the pass over
596
- // what shows doesn't reach.
604
+ // The repaint: the bar, the items of every tab, the row. Only what shows is repainted (the pass over what shows
605
+ // reaches the selected tab); a made tab that doesn't show is repainted when it is selected ([staleTabs]).
597
606
  internal fun repaint(options: Options) {
598
607
  if (isDestroyed) return
599
608
  ContainerBackground.apply(this, options)
@@ -603,7 +612,7 @@ class BottomTabsController(
603
612
  if (usesRow) layout.bar.customRowOptions = options.bottomTabs.customRow.copy()
604
613
  }
605
614
  cells.forEachIndexed { index, cell -> cell.update(cellState(index)) }
606
- tabs.forEach { if (it !== currentChild && it.isViewCreated) Navigator.repaintLayer(it) }
615
+ tabs.forEach { if (it !== currentChild && it.isViewCreated) staleTabs += it }
607
616
  }
608
617
 
609
618
  // The custom row: the cells first into the bar's external host, then the row holds them; its look follows the tabs'
@@ -681,6 +690,7 @@ class BottomTabsController(
681
690
  override fun destroy() {
682
691
  if (isDestroyed) return
683
692
  pendingSelected = null
693
+ staleTabs.clear()
684
694
  super.destroy()
685
695
  // A switch still running ends with its views at rest, without events: the tabs are gone.
686
696
  endSwitch?.invoke()
@@ -6,7 +6,6 @@ import android.animation.AnimatorSet
6
6
  import android.animation.ObjectAnimator
7
7
  import android.app.Activity
8
8
  import android.graphics.Color
9
- import android.graphics.drawable.Drawable
10
9
  import android.view.View
11
10
  import android.view.animation.LinearInterpolator
12
11
  import android.view.animation.PathInterpolator
@@ -20,8 +19,8 @@ import dev.apexnative.navigation.options.params.LayoutDirection
20
19
  import dev.apexnative.navigation.options.params.ThemeColour
21
20
  import dev.apexnative.navigation.options.parsers.TypefaceLoader
22
21
  import dev.apexnative.navigation.system.SystemUiOwner
23
- import dev.apexnative.navigation.utils.ImageLoader
24
- import dev.apexnative.navigation.utils.ImageLoadingListenerAdapter
22
+ import dev.apexnative.navigation.utils.ImageRequests
23
+ import dev.apexnative.navigation.utils.OptionImages
25
24
  import dev.apexnative.navigation.utils.TestIds
26
25
  import dev.apexnative.navigation.utils.UiUtils
27
26
  import dev.apexnative.navigation.viewcontrollers.viewcontroller.Presenter
@@ -67,8 +66,9 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
67
66
 
68
67
  private var layout: BottomTabsLayout? = null
69
68
  private val typefaces = TypefaceLoader(activity)
70
- private val icons = HashMap<String, Drawable>()
71
- private val applied = HashMap<Pair<Int, Class<*>>, TabItems.Call>()
69
+ // The icons of the items by item and kind (selected or not): those of the cache at once, the others as below.
70
+ private val icons = ImageRequests<Pair<Int, Boolean>>(OptionImages.source(activity))
71
+ private val applied = AppliedTabCalls(icons::key)
72
72
  private var motion: Animator? = null
73
73
  private var anyIcon = false
74
74
  private var testId: String? = null
@@ -100,25 +100,24 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
100
100
 
101
101
  /**
102
102
  * Runs [calls] on the bar's items; a call equal to the last of its kind for its item is skipped (an icon isn't
103
- * loaded and set again as a screen appears). The badge always goes: its default colors are read at each set.
103
+ * loaded and set again as a screen appears), except the badge and the font, an icon that didn't load and a `file:`
104
+ * icon whose file changed ([AppliedTabCalls]).
104
105
  */
105
106
  open fun run(calls: List<TabItems.Call>) {
106
107
  val bar = layout?.bar ?: return
107
108
  for (call in calls) {
108
109
  if (call is TabItems.Call.TestId && !itemTestIds) continue
109
- val key = call.index to call.javaClass
110
- if (call !is TabItems.Call.Badge && call !is TabItems.Call.Font && applied[key] == call) continue
111
- applied[key] = call
112
- run(bar, call)
110
+ val next = applied.next(call) ?: continue
111
+ run(bar, call, next)
113
112
  }
114
113
  }
115
114
 
116
- private fun run(bar: BottomTabs, call: TabItems.Call) {
115
+ private fun run(bar: BottomTabs, call: TabItems.Call, next: AppliedTabCalls.Next) {
117
116
  val index = call.index
118
117
  when (call) {
119
118
  is TabItems.Call.Title -> bar.setText(index, call.text)
120
- is TabItems.Call.Icon -> iconOf(call.uri)?.let { bar.setIcon(index, it) }
121
- is TabItems.Call.SelectedIcon -> iconOf(call.uri)?.let { bar.setSelectedIcon(index, it) }
119
+ is TabItems.Call.Icon -> icon(bar, call, call.uri, false, next)
120
+ is TabItems.Call.SelectedIcon -> icon(bar, call, call.uri, true, next)
122
121
  is TabItems.Call.Colors -> {
123
122
  bar.setIconActiveColor(index, call.iconActive)
124
123
  bar.setIconInactiveColor(index, call.iconInactive)
@@ -344,17 +343,19 @@ internal open class BottomTabsPresenter(activity: Activity) : Presenter(activity
344
343
 
345
344
  private fun colorOf(colour: ThemeColour): Int? = colour.get(activity, null)
346
345
 
347
- // Icons load as they apply and never throw: an icon that doesn't load leaves the item as it was.
348
- private fun iconOf(uri: String): Drawable? {
349
- icons[uri]?.let { return it.constantState?.newDrawable()?.mutate() ?: it }
350
- var loaded: Drawable? = null
351
- ImageLoader().loadIcon(activity, uri, object : ImageLoadingListenerAdapter() {
352
- override fun onComplete(drawable: Drawable) {
353
- loaded = drawable
354
- }
355
- })
356
- loaded?.let { icons[uri] = it }
357
- return loaded
346
+ // Icons never throw: one that doesn't load leaves the item without it (without its selected icon, the item shows
347
+ // its icon when selected). An item's first icon loads at once, its tab's first frame shows it; a change of it on
348
+ // the bar that shows loads off the main thread when it can, and the item takes it when ready, unless a newer icon
349
+ // of the item came first.
350
+ // The key of a `file:` icon was built for the comparison: the request takes it, its file read once.
351
+ private fun icon(bar: BottomTabs, call: TabItems.Call, uri: String, selected: Boolean, next: AppliedTabCalls.Next) {
352
+ val index = call.index
353
+ val key = next.image ?: icons.key(uri)
354
+ icons.request(index to selected, key, mayWait = next.isChange && bar.isAttachedToWindow) { icon ->
355
+ if (icon == null) applied.failed(call)
356
+ val shown = layout?.bar ?: return@request
357
+ if (selected) shown.setSelectedIcon(index, icon) else shown.setIcon(index, icon)
358
+ }
358
359
  }
359
360
 
360
361
  private companion object {
@@ -4,7 +4,7 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
4
4
 
5
5
  /**
6
6
  * Whether a screen's content rendered, which a command waits for before the screen comes in: a new root (`setRoot`),
7
- * a push with `waitForRender: true`, the new top of a `setStackRoot`. A wait never listens to the readiness store
7
+ * a push (unless `waitForRender: false`), the new top of a `setStackRoot`. A wait never listens to the readiness store
8
8
  * itself: the content keeps the one listener of its id and tells every waiter. Main thread only.
9
9
  */
10
10
  internal interface Readiness {
@@ -16,8 +16,10 @@ import dev.apexnative.navigation.viewcontrollers.viewcontroller.ViewController
16
16
  * without a limit.
17
17
  *
18
18
  * What differs between its users are its values, never its steps: the readiness and the clock (fakes in the unit
19
- * tests), [countsUnknownContent], how a readiness signal is delivered ([deliver]: at once, or posted to the next turn
20
- * of the main loop), and [step], the guard the user runs every later step in (a timer, a signal). Main thread only.
19
+ * tests), [countsUnknownContent], how a readiness signal is delivered ([deliver]: at once, posted to the next turn of
20
+ * the main loop, or behind the calls of JavaScript), whether content ready at once is delivered too
21
+ * ([deliversReadyContent]), and [step], the guard the user runs every later step in (a timer, a signal). Main thread
22
+ * only.
21
23
  */
22
24
  internal class RenderWait(
23
25
  private val readiness: Readiness,
@@ -32,6 +34,12 @@ internal class RenderWait(
32
34
  private val countsUnknownContent: Boolean,
33
35
  /** How a readiness signal reaches the wait: at once by default; posted, when a signal may come inside a layout. */
34
36
  private val deliver: (() -> Unit) -> Unit = { it() },
37
+ /**
38
+ * Content ready when the wait starts (or retakes) still reaches its end through [deliver], within the limit that
39
+ * runs: what [deliver] waits behind (a push's: the calls JavaScript made in the commit of that content) comes
40
+ * first. Off: such content ends the wait at once.
41
+ */
42
+ private val deliversReadyContent: Boolean = false,
35
43
  /** Runs a later step of the wait (a timer, a signal): the user's guards and protection; as it is by default. */
36
44
  private val step: (() -> Unit) -> Unit = { it() },
37
45
  private val onEnd: () -> Unit,
@@ -70,7 +78,8 @@ internal class RenderWait(
70
78
 
71
79
  /**
72
80
  * Starts waiting for [screens], and for the content of [unseen] within its first [UNSEEN_LIMIT_MILLIS]; with all
73
- * of them ready (and their bars' components, when they count within the limit) it ends at once, before it returns.
81
+ * of them ready (and their bars' components, when they count within the limit) it ends at once, before it returns,
82
+ * or through [deliver] with [deliversReadyContent].
74
83
  */
75
84
  fun start(screens: List<ViewController<*>>, unseen: List<ViewController<*>> = emptyList()) {
76
85
  this.screens = screens
@@ -78,15 +87,18 @@ internal class RenderWait(
78
87
  unseenDone = unseen.isEmpty()
79
88
  waiting = true
80
89
  token = Any()
81
- // Content ready at once ends the wait, unless the bar's components count within a limit: it runs for them.
82
- if (contentReady() && !(bar == Bar.WithinLimit && limitMillis != null)) return contentDone()
90
+ // Content ready at once ends the wait, unless the bar's components count within a limit (it runs for them) or
91
+ // the content goes through [deliver] too.
92
+ val ready = contentReady()
93
+ if (ready && !deliversReadyContent && !(bar == Bar.WithinLimit && limitMillis != null)) return contentDone()
94
+ // The timers first: a [deliver] that runs at once may end the wait before this returns.
83
95
  cancelContentTimer = when {
84
96
  limitMillis != null -> scheduler.schedule(limitMillis) { step { atLimit() } }
85
97
  countsUnknownContent -> scheduler.schedule(UNKNOWN_CONTENT_MILLIS) { step { atUnknownContentLimit() } }
86
98
  else -> null
87
99
  }
88
100
  startUnseenLimit()
89
- await()
101
+ if (ready && deliversReadyContent) deliverReady() else await()
90
102
  }
91
103
 
92
104
  /**
@@ -98,7 +110,7 @@ internal class RenderWait(
98
110
  this.screens = screens
99
111
  this.unseen = unseen
100
112
  token = Any()
101
- await()
113
+ if (deliversReadyContent && contentReady()) deliverReady() else await()
102
114
  }
103
115
 
104
116
  /** Ends the wait without [onEnd]: its timers stop, and the signals still to come do nothing. */
@@ -129,6 +141,14 @@ internal class RenderWait(
129
141
  if (contentReady() && cancelBarPhase == null) contentDone()
130
142
  }
131
143
 
144
+ // The content is ready already: it goes once through [deliver] as a signal would, then on as [await] says.
145
+ private fun deliverReady() {
146
+ val token = token
147
+ deliver {
148
+ step { if (waiting && this.token === token) await() }
149
+ }
150
+ }
151
+
132
152
  // The content of every screen ready: on to the bars; else the signals of those not ready.
133
153
  private fun await() {
134
154
  if (contentReady()) return contentDone()
@@ -731,7 +731,7 @@ internal class ModalCoordinator(
731
731
  // without the events not sent yet; what waits for it rejects, the layer it took out comes back, and the modals that
732
732
  // wait go on.
733
733
  private fun dismissFailed(record: ModalRecord, e: Throwable) {
734
- val text = ErrorTexts.couldNot("dismiss the modal of the screen '${record.rootId}'", ErrorTexts.reasonOf(e))
734
+ val text = ErrorTexts.couldNot("dismiss the modal '${record.rootId}'", ErrorTexts.reasonOf(e))
735
735
  val listed = record in records
736
736
  val under = layerUnder(record)
737
737
  runCatching { record.stopAnimation?.invoke() }
@@ -835,8 +835,8 @@ internal class ModalCoordinator(
835
835
  State.Presenting -> record.waitingDismissals += dismissal
836
836
  State.Shown -> if (isTop(record)) beginDismissal(record, dismissal) else closeBelow(record, dismissal)
837
837
  State.Dismissing -> dismissal.answer?.let(record.answers::add)
838
- State.ClosedBelow -> dismissal.answer?.resolve(record.rootId)
839
- State.WithOldRoot, State.Gone -> dismissal.answer?.reject(ErrorTexts.screenNotShown(record.rootId))
838
+ // It went already, or goes with the old root: the dismissal just completes, as on iOS.
839
+ State.ClosedBelow, State.WithOldRoot, State.Gone -> dismissal.answer?.resolve(record.rootId)
840
840
  }
841
841
  }
842
842
 
@@ -850,7 +850,7 @@ internal class ModalCoordinator(
850
850
  record.state = State.Gone
851
851
  records.remove(record)
852
852
  views.remove(record)
853
- val screen = AppearanceDispatcher.visibleNode(record.root)
853
+ val screen = AppearanceDispatcher.modalScreen(record.root)
854
854
  record.showAnswer?.resolve(record.rootId)
855
855
  record.showAnswer = null
856
856
  record.chain?.let {
@@ -1012,7 +1012,7 @@ internal class ModalCoordinator(
1012
1012
  val wasOut = isOut(revealed)
1013
1013
  val counted = members.filter { it.state != State.ClosedBelow && !it.reported }
1014
1014
  batch.count = counted.size
1015
- batch.screen = counted.lastOrNull()?.let { AppearanceDispatcher.visibleNode(it.root) }
1015
+ batch.screen = counted.lastOrNull()?.let { AppearanceDispatcher.modalScreen(it.root) }
1016
1016
  // The top that leaves with its animation: one already leaving keeps its own.
1017
1017
  val top = members.lastOrNull { it.state in ANIMATED_IN_BATCH }
1018
1018
  batch.top = top
@@ -439,9 +439,10 @@ class Navigator(
439
439
  }
440
440
 
441
441
  /**
442
- * The activity is destroyed: every layer goes, with the events of a destroy; a `setRoot` on its way resolves with
443
- * `''` (JavaScript sets its root again in the next activity), a modal's or an overlay's show and the commands that
444
- * wait for a modal with their roots' ids. Main thread only.
442
+ * The activity is destroyed: every layer goes, with the events of a destroy; a `setRoot` whose root still waited
443
+ * resolves with `''` (JavaScript sets its root again in the next activity), one whose root showed resolves with
444
+ * its id, and a modal's or an overlay's show and the commands that wait for a modal resolve with their roots' ids.
445
+ * Main thread only.
445
446
  */
446
447
  internal fun destroyWithActivity() {
447
448
  destroyLayers(answerPending = true)