react-native-screens 5.0.0-alpha.2 → 5.0.0-alpha.3

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 (237) hide show
  1. package/Package.swift +87 -0
  2. package/README.md +0 -7
  3. package/android/src/main/cpp/NativeProxy.cpp +30 -6
  4. package/android/src/main/cpp/NativeProxy.h +7 -1
  5. package/android/src/main/java/com/swmansion/rnscreens/common/container/Container.kt +8 -0
  6. package/android/src/main/java/com/swmansion/rnscreens/common/container/ContainerItem.kt +13 -1
  7. package/android/src/main/java/com/swmansion/rnscreens/common/text/ReactTextAppearance.kt +62 -0
  8. package/android/src/main/java/com/swmansion/rnscreens/common/text/TextAppearance.kt +20 -0
  9. package/android/src/main/java/com/swmansion/rnscreens/common/text/TextAppearanceDefaults.kt +65 -0
  10. package/android/src/main/java/com/swmansion/rnscreens/helpers/PropIconResolver.kt +17 -23
  11. package/android/src/main/java/com/swmansion/rnscreens/legacy/Screen.kt +3 -1
  12. package/android/src/main/java/com/swmansion/rnscreens/legacy/ScreenStack.kt +2 -0
  13. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/coordinator/FormSheetBehaviorController.kt +49 -18
  14. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/coordinator/FormSheetDimensionsCoordinator.kt +65 -40
  15. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/coordinator/FormSheetKeyboardCoordinator.kt +101 -0
  16. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/coordinator/FormSheetNativeDismissCoordinator.kt +0 -6
  17. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/core/FormSheetAvailableHeightProvider.kt +44 -0
  18. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/core/FormSheetDialog.kt +35 -3
  19. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/core/FormSheetDialogManager.kt +33 -106
  20. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/interfaces/FormSheetDialogEventEmitter.kt +2 -0
  21. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/model/FormSheetDetents.kt +61 -15
  22. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/presentation/FormSheetAnimatorFactory.kt +8 -9
  23. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/presentation/FormSheetDimmingManager.kt +115 -0
  24. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/presentation/FormSheetDismissalOrigin.kt +22 -0
  25. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/presentation/FormSheetPresentation.kt +173 -0
  26. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/presentation/FormSheetPresentationManager.kt +141 -49
  27. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/presentation/FormSheetPresentationState.kt +1 -1
  28. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/native/presentation/FormSheetStackRegistry.kt +28 -0
  29. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/react/event/FormSheetDismissEvent.kt +29 -0
  30. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/react/host/FormSheetHostEventEmitter.kt +7 -0
  31. package/android/src/main/java/com/swmansion/rnscreens/modals/formsheet/react/host/FormSheetHostViewManager.kt +2 -0
  32. package/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt +369 -58
  33. package/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderBackPressHandler.kt +11 -0
  34. package/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderCoordinatorLayout.kt +143 -98
  35. package/android/src/main/java/com/swmansion/rnscreens/stack/header/appbar/StackHeaderAppBarLayout.kt +155 -23
  36. package/android/src/main/java/com/swmansion/rnscreens/stack/header/appbar/StackHeaderContentScrimDrawable.kt +36 -0
  37. package/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderCollapsedTitleGravityMode.kt +6 -0
  38. package/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfig.kt +170 -167
  39. package/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigViewManager.kt +385 -12
  40. package/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigurationObserver.kt +0 -4
  41. package/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigurationProviding.kt +32 -3
  42. package/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderDelegate.kt +0 -7
  43. package/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderInvalidationFlags.kt +7 -2
  44. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/StackHeaderToolbarMenuApplicator.kt +27 -280
  45. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/StackHeaderToolbarMenuController.kt +448 -0
  46. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/StackHeaderToolbarMenuDelegate.kt +10 -0
  47. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/StackHeaderToolbarMenuMapper.kt +28 -20
  48. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/model/StackHeaderToolbarMenuConfig.kt +3 -55
  49. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/model/StackHeaderToolbarMenuGroupMetadata.kt +1 -10
  50. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/model/StackHeaderToolbarMenuItemConfig.kt +1 -3
  51. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/model/StackHeaderToolbarMenuModel.kt +180 -0
  52. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/update/StackHeaderToolbarMenuElementOptions.kt +57 -6
  53. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/update/StackHeaderToolbarMenuElementRawUpdate.kt +3 -1
  54. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/update/StackHeaderToolbarMenuElementUpdate.kt +1 -0
  55. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/update/StackHeaderToolbarMenuIconResolver.kt +4 -3
  56. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/update/StackHeaderToolbarMenuUpdateQueue.kt +23 -9
  57. package/android/src/main/java/com/swmansion/rnscreens/stack/host/StackContainer.kt +103 -3
  58. package/android/src/main/java/com/swmansion/rnscreens/stack/host/StackHost.kt +13 -0
  59. package/android/src/main/java/com/swmansion/rnscreens/stack/host/StackHostViewManager.kt +14 -0
  60. package/android/src/main/java/com/swmansion/rnscreens/stack/screen/StackScreen.kt +4 -0
  61. package/android/src/main/java/com/swmansion/rnscreens/stack/screen/StackScreenFragment.kt +20 -1
  62. package/android/src/main/java/com/swmansion/rnscreens/stack/screen/StackScreenFragmentDelegate.kt +12 -0
  63. package/android/src/main/java/com/swmansion/rnscreens/tabs/appearance/TabsAppearanceCoordinator.kt +2 -4
  64. package/android/src/main/java/com/swmansion/rnscreens/tabs/container/MenuHelpers.kt +5 -23
  65. package/android/src/main/java/com/swmansion/rnscreens/tabs/container/TabsContainer.kt +46 -25
  66. package/android/src/main/java/com/swmansion/rnscreens/tabs/host/TabsHost.kt +10 -0
  67. package/android/src/main/java/com/swmansion/rnscreens/tabs/host/TabsHostA11yCoordinator.kt +23 -2
  68. package/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreen.kt +17 -0
  69. package/android/src/main/java/com/swmansion/rnscreens/tabs/screen/TabsScreenFragment.kt +3 -0
  70. package/android/src/main/java/com/swmansion/rnscreens/utils/DimensionUtils.kt +7 -0
  71. package/android/src/main/java/com/swmansion/rnscreens/utils/StyleUtils.kt +22 -0
  72. package/android/src/main/res/base/layout/rns_collapsing_toolbar_large_available_space.xml +8 -0
  73. package/android/src/main/res/base/layout/rns_collapsing_toolbar_large_entire_space.xml +8 -0
  74. package/android/src/main/res/base/layout/rns_collapsing_toolbar_medium_available_space.xml +8 -0
  75. package/android/src/main/res/base/layout/rns_collapsing_toolbar_medium_entire_space.xml +8 -0
  76. package/cpp/legacy/RNSScreenRemovalListener.cpp +31 -1
  77. package/cpp/legacy/RNSScreenRemovalListener.h +18 -3
  78. package/ios/RNSEnums.h +2 -0
  79. package/ios/conversion/RNSConversions.h +7 -108
  80. package/ios/legacy/RNSFullWindowOverlay.mm +1 -1
  81. package/ios/legacy/RNSModalScreen.mm +1 -1
  82. package/ios/legacy/RNSScreen.mm +8 -19
  83. package/ios/legacy/RNSScreenStackHeaderConfig.mm +3 -1
  84. package/ios/legacy/RNSScreenStackHeaderSubview.mm +1 -1
  85. package/ios/legacy/RNSScreenWindowTraits.mm +5 -13
  86. package/ios/modals/form-sheet/RNSFormSheetConfigurationApplicator.h +1 -3
  87. package/ios/modals/form-sheet/RNSFormSheetConfigurationApplicator.mm +11 -22
  88. package/ios/modals/form-sheet/RNSFormSheetContentController.h +1 -1
  89. package/ios/modals/form-sheet/RNSFormSheetContentController.mm +7 -18
  90. package/ios/modals/form-sheet/RNSFormSheetHostComponentView.mm +4 -4
  91. package/ios/modals/form-sheet/RNSFormSheetHostShadowStateProxy.mm +2 -2
  92. package/ios/modals/form-sheet/RNSFormSheetUpdateFlags.h +3 -0
  93. package/ios/safe-area/RNSSafeAreaViewComponentView.mm +2 -2
  94. package/ios/{conversion → scroll-view-marker/conversion}/RNSConversions-ScrollViewMarker.mm +2 -3
  95. package/ios/split/RNSSplitAppearanceApplicator.h +4 -4
  96. package/ios/split/RNSSplitAppearanceApplicator.mm +53 -56
  97. package/ios/split/RNSSplitHostComponentEventEmitter.h +1 -0
  98. package/ios/split/RNSSplitHostComponentEventEmitter.mm +1 -1
  99. package/ios/split/RNSSplitHostComponentView.h +5 -15
  100. package/ios/split/RNSSplitHostComponentView.mm +55 -6
  101. package/ios/split/RNSSplitHostController.h +25 -6
  102. package/ios/split/RNSSplitHostController.mm +24 -92
  103. package/ios/split/RNSSplitHostProviders.h +67 -0
  104. package/ios/split/RNSSplitScreenComponentView.h +0 -39
  105. package/ios/split/RNSSplitScreenComponentView.mm +27 -10
  106. package/ios/split/RNSSplitScreenController.h +26 -3
  107. package/ios/split/RNSSplitScreenController.mm +13 -30
  108. package/ios/split/RNSSplitScreenShadowStateProxy.h +1 -40
  109. package/ios/split/RNSSplitScreenShadowStateProxy.mm +1 -26
  110. package/ios/split/conversion/RNSConversions-SplitView.h +56 -0
  111. package/ios/{conversion → split/conversion}/RNSConversions-SplitView.mm +4 -1
  112. package/ios/stack/conversion/RNSConversions-Stack.h +32 -0
  113. package/ios/stack/conversion/RNSConversions-Stack.mm +55 -0
  114. package/ios/stack/header/RNSStackHeaderAppearanceMapper.h +26 -0
  115. package/ios/stack/header/RNSStackHeaderAppearanceMapper.mm +88 -0
  116. package/ios/stack/header/RNSStackHeaderConfigComponentView.h +10 -1
  117. package/ios/stack/header/RNSStackHeaderConfigComponentView.mm +67 -35
  118. package/ios/stack/header/RNSStackHeaderConfigDataProviding.h +6 -0
  119. package/ios/stack/header/RNSStackHeaderConfigShadowStateProxy.mm +1 -1
  120. package/ios/stack/header/RNSStackHeaderIconResolver.mm +7 -0
  121. package/ios/stack/header/RNSStackHeaderItemComponentView.h +3 -1
  122. package/ios/stack/header/RNSStackHeaderItemComponentView.mm +15 -2
  123. package/ios/stack/header/RNSStackHeaderItemDataProviding.h +2 -0
  124. package/ios/stack/header/RNSStackHeaderItemShadowStateProxy.mm +1 -1
  125. package/ios/stack/header/RNSStackHeaderItemSpacerComponentView.mm +2 -1
  126. package/ios/stack/header/RNSStackHeaderMenuMapper.mm +4 -0
  127. package/ios/stack/host/RNSStackNavigationBar.h +25 -0
  128. package/ios/stack/host/RNSStackNavigationBar.mm +80 -0
  129. package/ios/stack/host/RNSStackNavigationBarCoordinator.h +8 -0
  130. package/ios/stack/host/RNSStackNavigationBarCoordinator.mm +11 -0
  131. package/ios/stack/host/RNSStackNavigationController.mm +13 -5
  132. package/ios/stack/host/RNSStackOperation.h +0 -2
  133. package/ios/stack/screen/RNSStackScreenComponentView.mm +9 -2
  134. package/ios/stack/screen/RNSStackScreenController.mm +12 -0
  135. package/ios/stack/screen/RNSStackScreenHeaderCoordinator.h +8 -0
  136. package/ios/stack/screen/RNSStackScreenHeaderCoordinator.mm +98 -16
  137. package/ios/tabs/RNSTabBarAppearanceCoordinator.mm +40 -9
  138. package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryComponentView.h +1 -1
  139. package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryComponentView.mm +1 -1
  140. package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryContentComponentView.mm +1 -1
  141. package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryEventEmitter.mm +1 -1
  142. package/ios/tabs/bottom-accessory/RNSTabsBottomAccessoryShadowStateProxy.mm +1 -1
  143. package/ios/tabs/conversion/RNSConversions-Tabs.h +78 -0
  144. package/ios/{conversion → tabs/conversion}/RNSConversions-Tabs.mm +8 -71
  145. package/ios/tabs/extensions/RNSTabsHostComponentView+RNSImageLoader.h +1 -1
  146. package/ios/tabs/host/RNSTabsHostComponentView.h +2 -0
  147. package/ios/tabs/host/RNSTabsHostComponentView.mm +10 -3
  148. package/ios/tabs/host/RNSTabsHostEventEmitter.mm +1 -1
  149. package/ios/tabs/screen/RNSTabsScreenComponentView.mm +20 -5
  150. package/ios/utils/RNSDefines.h +3 -0
  151. package/lib/commonjs/components/stack/header/StackHeaderConfig.android.js +34 -19
  152. package/lib/commonjs/components/stack/header/StackHeaderConfig.android.js.map +1 -1
  153. package/lib/commonjs/components/stack/header/StackHeaderConfig.ios.js +33 -1
  154. package/lib/commonjs/components/stack/header/StackHeaderConfig.ios.js.map +1 -1
  155. package/lib/commonjs/components/stack/host/StackHost.js +9 -6
  156. package/lib/commonjs/components/stack/host/StackHost.js.map +1 -1
  157. package/lib/commonjs/components/stack/index.js.map +1 -1
  158. package/lib/commonjs/components/tabs/host/TabsHost.ios.js +1 -0
  159. package/lib/commonjs/components/tabs/host/TabsHost.ios.js.map +1 -1
  160. package/lib/commonjs/fabric/stack/StackHeaderConfigAndroidNativeComponent.js.map +1 -1
  161. package/lib/commonjs/fabric/stack/StackHeaderConfigIOSNativeComponent.js.map +1 -1
  162. package/lib/commonjs/fabric/stack/StackHeaderItemIOSNativeComponent.js.map +1 -1
  163. package/lib/commonjs/fabric/stack/StackHostNativeComponent.js.map +1 -1
  164. package/lib/commonjs/fabric/tabs/TabsHostIOSNativeComponent.js.map +1 -1
  165. package/lib/module/components/stack/header/StackHeaderConfig.android.js +35 -20
  166. package/lib/module/components/stack/header/StackHeaderConfig.android.js.map +1 -1
  167. package/lib/module/components/stack/header/StackHeaderConfig.ios.js +34 -2
  168. package/lib/module/components/stack/header/StackHeaderConfig.ios.js.map +1 -1
  169. package/lib/module/components/stack/host/StackHost.js +9 -6
  170. package/lib/module/components/stack/host/StackHost.js.map +1 -1
  171. package/lib/module/components/stack/index.js.map +1 -1
  172. package/lib/module/components/tabs/host/TabsHost.ios.js +1 -0
  173. package/lib/module/components/tabs/host/TabsHost.ios.js.map +1 -1
  174. package/lib/module/fabric/stack/StackHeaderConfigAndroidNativeComponent.js.map +1 -1
  175. package/lib/module/fabric/stack/StackHeaderConfigIOSNativeComponent.js.map +1 -1
  176. package/lib/module/fabric/stack/StackHeaderItemIOSNativeComponent.js.map +1 -1
  177. package/lib/module/fabric/stack/StackHostNativeComponent.js.map +1 -1
  178. package/lib/module/fabric/tabs/TabsHostIOSNativeComponent.js.map +1 -1
  179. package/lib/typescript/components/modals/form-sheet/FormSheet.types.d.ts +1 -1
  180. package/lib/typescript/components/stack/header/StackHeaderConfig.android.d.ts.map +1 -1
  181. package/lib/typescript/components/stack/header/StackHeaderConfig.android.types.d.ts +616 -21
  182. package/lib/typescript/components/stack/header/StackHeaderConfig.android.types.d.ts.map +1 -1
  183. package/lib/typescript/components/stack/header/StackHeaderConfig.ios.d.ts.map +1 -1
  184. package/lib/typescript/components/stack/header/StackHeaderConfig.ios.types.d.ts +226 -2
  185. package/lib/typescript/components/stack/header/StackHeaderConfig.ios.types.d.ts.map +1 -1
  186. package/lib/typescript/components/stack/header/StackHeaderConfig.types.d.ts +2 -2
  187. package/lib/typescript/components/stack/header/ios/StackHeaderItem.ios.types.d.ts +2 -0
  188. package/lib/typescript/components/stack/header/ios/StackHeaderItem.ios.types.d.ts.map +1 -1
  189. package/lib/typescript/components/stack/host/StackHost.d.ts +1 -1
  190. package/lib/typescript/components/stack/host/StackHost.d.ts.map +1 -1
  191. package/lib/typescript/components/stack/host/StackHost.types.d.ts +24 -0
  192. package/lib/typescript/components/stack/host/StackHost.types.d.ts.map +1 -1
  193. package/lib/typescript/components/stack/index.d.ts +2 -2
  194. package/lib/typescript/components/stack/index.d.ts.map +1 -1
  195. package/lib/typescript/components/tabs/host/TabsHost.ios.d.ts.map +1 -1
  196. package/lib/typescript/components/tabs/host/TabsHost.ios.types.d.ts +16 -0
  197. package/lib/typescript/components/tabs/host/TabsHost.ios.types.d.ts.map +1 -1
  198. package/lib/typescript/components/tabs/host/TabsHost.types.d.ts +3 -0
  199. package/lib/typescript/components/tabs/host/TabsHost.types.d.ts.map +1 -1
  200. package/lib/typescript/fabric/stack/StackHeaderConfigAndroidNativeComponent.d.ts +52 -0
  201. package/lib/typescript/fabric/stack/StackHeaderConfigAndroidNativeComponent.d.ts.map +1 -1
  202. package/lib/typescript/fabric/stack/StackHeaderConfigIOSNativeComponent.d.ts +25 -1
  203. package/lib/typescript/fabric/stack/StackHeaderConfigIOSNativeComponent.d.ts.map +1 -1
  204. package/lib/typescript/fabric/stack/StackHeaderItemIOSNativeComponent.d.ts +2 -0
  205. package/lib/typescript/fabric/stack/StackHeaderItemIOSNativeComponent.d.ts.map +1 -1
  206. package/lib/typescript/fabric/stack/StackHostNativeComponent.d.ts +3 -1
  207. package/lib/typescript/fabric/stack/StackHostNativeComponent.d.ts.map +1 -1
  208. package/lib/typescript/fabric/tabs/TabsHostIOSNativeComponent.d.ts +1 -0
  209. package/lib/typescript/fabric/tabs/TabsHostIOSNativeComponent.d.ts.map +1 -1
  210. package/package.json +8 -7
  211. package/react-native.config.js +3 -0
  212. package/src/components/modals/form-sheet/FormSheet.types.ts +1 -1
  213. package/src/components/stack/header/StackHeaderConfig.android.tsx +48 -23
  214. package/src/components/stack/header/StackHeaderConfig.android.types.ts +637 -21
  215. package/src/components/stack/header/StackHeaderConfig.ios.tsx +45 -1
  216. package/src/components/stack/header/StackHeaderConfig.ios.types.ts +238 -2
  217. package/src/components/stack/header/StackHeaderConfig.types.ts +2 -2
  218. package/src/components/stack/header/ios/StackHeaderItem.ios.types.ts +2 -0
  219. package/src/components/stack/host/StackHost.tsx +4 -2
  220. package/src/components/stack/host/StackHost.types.ts +26 -0
  221. package/src/components/stack/index.ts +6 -1
  222. package/src/components/tabs/host/TabsHost.ios.tsx +1 -0
  223. package/src/components/tabs/host/TabsHost.ios.types.ts +16 -0
  224. package/src/components/tabs/host/TabsHost.types.ts +3 -0
  225. package/src/fabric/stack/StackHeaderConfigAndroidNativeComponent.ts +78 -0
  226. package/src/fabric/stack/StackHeaderConfigIOSNativeComponent.ts +32 -0
  227. package/src/fabric/stack/StackHeaderItemIOSNativeComponent.ts +2 -0
  228. package/src/fabric/stack/StackHostNativeComponent.ts +11 -2
  229. package/src/fabric/tabs/TabsHostIOSNativeComponent.ts +1 -0
  230. package/android/src/main/java/com/swmansion/rnscreens/modals/dimmingview/DimmingView.kt +0 -83
  231. package/android/src/main/java/com/swmansion/rnscreens/modals/dimmingview/DimmingViewManager.kt +0 -89
  232. package/android/src/main/java/com/swmansion/rnscreens/modals/dimmingview/DimmingViewPointerEvents.kt +0 -18
  233. package/android/src/main/java/com/swmansion/rnscreens/stack/header/toolbar/StackHeaderToolbarMenuSelectionController.kt +0 -83
  234. package/ios/conversion/RNSConversions-Stack.h +0 -32
  235. package/ios/conversion/RNSConversions-Stack.mm +0 -41
  236. package/ios/conversion/always_false.h +0 -31
  237. /package/ios/{conversion → scroll-view-marker/conversion}/RNSConversions-ScrollViewMarker.h +0 -0
@@ -0,0 +1,448 @@
1
+ package com.swmansion.rnscreens.stack.header.toolbar
2
+
3
+ import android.graphics.drawable.Drawable
4
+ import android.util.Log
5
+ import android.view.MenuItem
6
+ import com.google.android.material.appbar.MaterialToolbar
7
+ import com.swmansion.rnscreens.helpers.IconResolution
8
+ import com.swmansion.rnscreens.helpers.PropIconResolver
9
+ import com.swmansion.rnscreens.stack.header.toolbar.model.StackHeaderToolbarMenuConfig
10
+ import com.swmansion.rnscreens.stack.header.toolbar.model.StackHeaderToolbarMenuElementConfig
11
+ import com.swmansion.rnscreens.stack.header.toolbar.model.StackHeaderToolbarMenuItemIconSource
12
+ import com.swmansion.rnscreens.stack.header.toolbar.model.StackHeaderToolbarMenuModel
13
+ import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarFieldUpdate
14
+ import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarMenuElementOptions
15
+ import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarMenuElementRawUpdate
16
+ import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarMenuElementUpdate
17
+ import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarMenuIconResolver
18
+ import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarMenuUpdateQueue
19
+ import com.swmansion.rnscreens.stack.header.toolbar.update.toOptions
20
+ import java.lang.ref.WeakReference
21
+
22
+ /**
23
+ * Owns the toolbar menu: the parsed model and the runtime state accumulated on
24
+ * top of it (imperative element updates, group selections, resolved prop
25
+ * icons). The state is the source of truth and lives as long as this
26
+ * controller; an attached toolbar is merely a projection of it, rebuilt from
27
+ * scratch at every [attach]. Checked-state semantics run on the state, so
28
+ * updates behave identically whether a toolbar is attached or not.
29
+ */
30
+ internal class StackHeaderToolbarMenuController(
31
+ private val iconResolver: StackHeaderToolbarMenuIconResolver,
32
+ ) {
33
+ internal var delegate: WeakReference<StackHeaderToolbarMenuDelegate>? = null
34
+
35
+ // region Model
36
+
37
+ private var model = StackHeaderToolbarMenuModel.EMPTY
38
+ private var groupDividerEnabled = false
39
+
40
+ // Set by a menu change and cleared by [attach]: until then a live toolbar
41
+ // still shows the previous menu (with previous int ids), so in-place
42
+ // application is suspended and updates are recorded only.
43
+ private var modelDirty = false
44
+
45
+ /**
46
+ * Replaces the menu configuration. Returns `true` when it actually
47
+ * changed; icon sources are part of the config, so an icon-source-only
48
+ * prop change counts. A real change validates the new menu, resets all
49
+ * runtime state, re-initializes group selections from
50
+ * `initialToggleState` and drops pending command batches. Application to
51
+ * the toolbar is the caller's responsibility (via [attach]).
52
+ */
53
+ internal fun setMenu(menu: StackHeaderToolbarMenuConfig): Boolean {
54
+ if (menu == model.config) {
55
+ return false
56
+ }
57
+
58
+ // Throws on an invalid menu before any mutation, so the previous,
59
+ // valid model stays intact.
60
+ model = StackHeaderToolbarMenuModel.from(menu)
61
+
62
+ commandOverrides.clear()
63
+ propIcons.keys.retainAll(model.elementById.keys)
64
+ propIconResolvers.keys.retainAll(model.elementById.keys)
65
+ groupSelections =
66
+ model.groupMetadata.groupMemberItems
67
+ .mapValues { (_, members) ->
68
+ members.filterTo(mutableSetOf()) {
69
+ model.elementById
70
+ .getValue(it)
71
+ .item.initialToggleState
72
+ }
73
+ }.toMutableMap()
74
+
75
+ // Commands sent against the previous menu must not touch the new one,
76
+ // even those still waiting for an icon.
77
+ updateQueue.clearPending()
78
+
79
+ modelDirty = true
80
+ resolvePropIcons()
81
+ return true
82
+ }
83
+
84
+ /** Returns `true` when the value changed and the menu needs a rebuild. */
85
+ internal fun setGroupDividerEnabled(enabled: Boolean): Boolean {
86
+ if (groupDividerEnabled == enabled) {
87
+ return false
88
+ }
89
+ groupDividerEnabled = enabled
90
+ return true
91
+ }
92
+
93
+ // endregion
94
+
95
+ // region State
96
+
97
+ // Resolved icons of prop-declared items, and the stateful per-item
98
+ // resolvers behind them; both pruned in [setMenu].
99
+ private val propIcons = mutableMapOf<String, Drawable>()
100
+ private val propIconResolvers = mutableMapOf<String, PropIconResolver>()
101
+
102
+ // Accumulated imperative element updates, merged field-wise (newer
103
+ // non-null wins). `checked` never reaches here — it travels on the update
104
+ // envelope and group selections are its single home.
105
+ private val commandOverrides = mutableMapOf<String, StackHeaderToolbarMenuElementOptions>()
106
+
107
+ // Fully materialized selection per group. Initialized from
108
+ // `initialToggleState` at [setMenu], mutated by taps and command `checked`.
109
+ private var groupSelections = mutableMapOf<String, MutableSet<String>>()
110
+
111
+ // endregion
112
+
113
+ // region Imperative updates
114
+
115
+ // Serial, batch-atomic command ingestion; owned here so a menu change can
116
+ // drop stale batches locally (see setMenu).
117
+ private val updateQueue =
118
+ StackHeaderToolbarMenuUpdateQueue(iconResolver) { applyElementUpdates(it) }
119
+
120
+ /**
121
+ * Enqueues an `updateToolbarMenuElements` batch. Batches apply serially
122
+ * and atomically, once every icon in the batch has resolved — see
123
+ * [StackHeaderToolbarMenuUpdateQueue].
124
+ */
125
+ internal fun enqueueElementUpdates(batch: List<StackHeaderToolbarMenuElementRawUpdate>) {
126
+ updateQueue.enqueue(batch)
127
+ }
128
+
129
+ /**
130
+ * Applies a fully resolved `updateToolbarMenuElements` batch: records it,
131
+ * runs checked-state transitions on the state, projects onto the live
132
+ * toolbar when one is attached, and emits one coalesced selection event per
133
+ * changed group — identically whether attached or not.
134
+ */
135
+ private fun applyElementUpdates(updates: List<StackHeaderToolbarMenuElementUpdate>) {
136
+ val changedGroups = LinkedHashSet<String>()
137
+ val toolbar = liveToolbar()
138
+
139
+ for (update in updates) {
140
+ val id = update.id
141
+ if (id !in model.elementById) {
142
+ Log.w(TAG, "[RNScreens] Ignoring toolbar menu update for unknown item id '$id'.")
143
+ continue
144
+ }
145
+
146
+ val options = applicableOptions(id, update.options)
147
+
148
+ if (!options.isEmpty) {
149
+ commandOverrides[id] = commandOverrides[id]?.mergedWith(options) ?: options
150
+ }
151
+
152
+ update.checked?.let { checked ->
153
+ applyCheckedInState(id, checked)?.let(changedGroups::add)
154
+ }
155
+
156
+ if (toolbar != null && !options.isEmpty) {
157
+ StackHeaderToolbarMenuApplicator.updateToolbarMenuElement(
158
+ toolbar,
159
+ model.forwardIdMap,
160
+ id,
161
+ widenWithCoupledFields(id, options),
162
+ )
163
+ }
164
+ }
165
+
166
+ if (toolbar != null) {
167
+ changedGroups.forEach { applySelectionToToolbar(toolbar, it) }
168
+ }
169
+ changedGroups.forEach(::emitGroupSelection)
170
+ }
171
+
172
+ /**
173
+ * Drops options the target element cannot accept. They would otherwise be
174
+ * recorded in [commandOverrides], which outlive every rebuild, and be
175
+ * re-applied — and re-warned about — by each of them.
176
+ */
177
+ private fun applicableOptions(
178
+ id: String,
179
+ options: StackHeaderToolbarMenuElementOptions,
180
+ ): StackHeaderToolbarMenuElementOptions {
181
+ val targetsSubmenu = model.elementById[id] is StackHeaderToolbarMenuElementConfig.Submenu
182
+ if (options.menuTitle == null || targetsSubmenu) {
183
+ return options
184
+ }
185
+ Log.w(TAG, "[RNScreens] Ignoring menuTitle of '$id': the target is not a submenu.")
186
+ return options.copy(menuTitle = null)
187
+ }
188
+
189
+ // endregion
190
+
191
+ // region Prop icons
192
+
193
+ /**
194
+ * Re-resolves every prop-declared icon source; called on a menu change —
195
+ * the only time sources can change. Results flow through [setItemIcon]:
196
+ * while the menu is dirty (until the next [attach]) they land in state
197
+ * only, later async ones also apply to the live toolbar in place.
198
+ */
199
+ private fun resolvePropIcons() {
200
+ model.elementById.forEach { (id, element) ->
201
+ val source = element.item.iconSource
202
+ // An escaping exception would leave the menu dirty forever, with
203
+ // in-place application suspended until the next [attach].
204
+ try {
205
+ propIconResolvers
206
+ .getOrPut(id) {
207
+ PropIconResolver { name, uri, onComplete ->
208
+ iconResolver.resolve(
209
+ StackHeaderToolbarMenuItemIconSource(name, uri),
210
+ onComplete,
211
+ )
212
+ }
213
+ }.resolve(source.drawableIconResourceName, source.imageIconUri) { result ->
214
+ when (result) {
215
+ IconResolution.Unchanged -> Unit
216
+ is IconResolution.Resolved -> setItemIcon(id, source, result.drawable)
217
+ }
218
+ }
219
+ } catch (e: Exception) {
220
+ Log.e(TAG, "[RNScreens] Failed to resolve the icon of toolbar menu item '$id'.", e)
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Sets or clears the resolved icon of a prop-declared item, in place —
227
+ * never rebuilds the menu and never touches command state. An icon set via
228
+ * a view command takes precedence until the next real menu change.
229
+ */
230
+ private fun setItemIcon(
231
+ id: String,
232
+ source: StackHeaderToolbarMenuItemIconSource,
233
+ icon: Drawable?,
234
+ ) {
235
+ // Pruning a resolver in [setMenu] does not cancel its in-flight load,
236
+ // and the orphan's latest-wins guard still matches the source it was
237
+ // asked for, so a result must be matched against the current model.
238
+ if (model.elementById[id]?.item?.iconSource != source) {
239
+ return
240
+ }
241
+ val previous = if (icon != null) propIcons.put(id, icon) else propIcons.remove(id)
242
+ if (previous === icon || commandOverrides[id]?.icon != null) {
243
+ return
244
+ }
245
+ val toolbar = liveToolbar() ?: return
246
+ val effective = effectiveOptions(id)
247
+ StackHeaderToolbarMenuApplicator.updateToolbarMenuElement(
248
+ toolbar,
249
+ model.forwardIdMap,
250
+ id,
251
+ StackHeaderToolbarMenuElementOptions(
252
+ // Action-item classification runs when showAsAction is set and
253
+ // setting an icon alone does not re-run it, so it must
254
+ // accompany the icon.
255
+ showAsAction = effective.showAsAction,
256
+ icon = effective.icon,
257
+ iconTintColorNormal = effective.iconTintColorNormal,
258
+ iconTintColorPressed = effective.iconTintColorPressed,
259
+ iconTintColorFocused = effective.iconTintColorFocused,
260
+ iconTintColorDisabled = effective.iconTintColorDisabled,
261
+ ),
262
+ )
263
+ }
264
+
265
+ // endregion
266
+
267
+ // region Attachment & projection
268
+
269
+ private var attachedToolbar: WeakReference<MaterialToolbar>? = null
270
+
271
+ /**
272
+ * Projects the menu (model ⊕ prop icons ⊕ command overrides ⊕ selections)
273
+ * onto [toolbar] and starts applying subsequent updates to it in place.
274
+ * Emits no events — the projection is semantically a no-change.
275
+ */
276
+ internal fun attach(toolbar: MaterialToolbar) {
277
+ attachedToolbar = WeakReference(toolbar)
278
+ modelDirty = false
279
+ StackHeaderToolbarMenuApplicator.rebuildToolbarMenu(
280
+ toolbar,
281
+ model,
282
+ groupDividerEnabled,
283
+ optionsForItem = ::effectiveOptions,
284
+ onItemClicked = ::handleItemClick,
285
+ )
286
+ model.groupMetadata.groupMemberItems.keys
287
+ .forEach { applySelectionToToolbar(toolbar, it) }
288
+ }
289
+
290
+ internal fun detach() {
291
+ attachedToolbar = null
292
+ }
293
+
294
+ internal fun tearDown() {
295
+ updateQueue.tearDown()
296
+ detach()
297
+ }
298
+
299
+ private fun liveToolbar(): MaterialToolbar? = if (modelDirty) null else attachedToolbar?.get()
300
+
301
+ private fun applySelectionToToolbar(
302
+ toolbar: MaterialToolbar,
303
+ groupId: String,
304
+ ) {
305
+ val selected = groupSelections[groupId].orEmpty()
306
+
307
+ // setChecked(false) on an item of an exclusive group checks it instead
308
+ // of clearing it, so only the selected member is written and the native
309
+ // sibling-uncheck does the rest. Such a group is never cleared.
310
+ if (model.groupMetadata.groupSingleSelection[groupId] == true) {
311
+ val selectedId = selected.singleOrNull() ?: return
312
+ menuItemFor(toolbar, selectedId)?.isChecked = true
313
+ return
314
+ }
315
+
316
+ for (memberId in model.groupMetadata.groupMemberItems[groupId].orEmpty()) {
317
+ val item = menuItemFor(toolbar, memberId) ?: continue
318
+ val shouldBeChecked = memberId in selected
319
+ if (item.isChecked != shouldBeChecked) {
320
+ item.isChecked = shouldBeChecked
321
+ }
322
+ }
323
+ }
324
+
325
+ private fun menuItemFor(
326
+ toolbar: MaterialToolbar,
327
+ id: String,
328
+ ): MenuItem? = model.forwardIdMap[id]?.let { toolbar.menu.findItem(it) }
329
+
330
+ // endregion
331
+
332
+ // region Checked-state semantics
333
+
334
+ /**
335
+ * Mutates the selection of [itemId]'s group and returns the group id when
336
+ * it actually changed. [explicitCheckedValue] `null` means a user toggle.
337
+ */
338
+ private fun applyCheckedInState(
339
+ itemId: String,
340
+ explicitCheckedValue: Boolean?,
341
+ ): String? {
342
+ val groupId = model.groupMetadata.itemGroupMap[itemId] ?: return null
343
+ val singleSelection = model.groupMetadata.groupSingleSelection[groupId] ?: return null
344
+ val selection = groupSelections.getOrPut(groupId) { mutableSetOf() }
345
+
346
+ if (singleSelection) {
347
+ if (explicitCheckedValue == false) {
348
+ Log.w(
349
+ TAG,
350
+ "[RNScreens] Cannot uncheck item '$itemId' in single-selection group '$groupId'. " +
351
+ "Check a different item instead.",
352
+ )
353
+ return null
354
+ }
355
+ if (selection.singleOrNull() == itemId) {
356
+ return null
357
+ }
358
+ selection.clear()
359
+ selection.add(itemId)
360
+ return groupId
361
+ }
362
+
363
+ val newChecked = explicitCheckedValue ?: (itemId !in selection)
364
+ val changed = if (newChecked) selection.add(itemId) else selection.remove(itemId)
365
+ return groupId.takeIf { changed }
366
+ }
367
+
368
+ private fun handleItemClick(id: String) {
369
+ if (!isCheckable(id)) {
370
+ delegate?.get()?.onMenuItemClicked(id)
371
+ return
372
+ }
373
+ val changedGroup = applyCheckedInState(id, explicitCheckedValue = null) ?: return
374
+ liveToolbar()?.let { applySelectionToToolbar(it, changedGroup) }
375
+ emitGroupSelection(changedGroup)
376
+ }
377
+
378
+ private fun emitGroupSelection(groupId: String) {
379
+ val selection = groupSelections[groupId].orEmpty()
380
+ val selectedIds =
381
+ model.groupMetadata.groupMemberItems[groupId]
382
+ .orEmpty()
383
+ .filter { it in selection }
384
+ delegate?.get()?.onGroupSelectionChanged(groupId, selectedIds)
385
+ }
386
+
387
+ // Validation guarantees only plain menu items carry a groupId, so
388
+ // checkability reduces to group membership.
389
+ private fun isCheckable(id: String): Boolean = model.elementById[id]?.item?.groupId != null
390
+
391
+ // endregion
392
+
393
+ // region Effective values
394
+
395
+ private fun effectiveOptions(id: String): StackHeaderToolbarMenuElementOptions {
396
+ val base = model.elementById.getValue(id).toOptions()
397
+ val withPropIcon =
398
+ propIcons[id]?.let { base.copy(icon = StackHeaderToolbarFieldUpdate.Set(it)) } ?: base
399
+ return commandOverrides[id]?.let(withPropIcon::mergedWith) ?: withPropIcon
400
+ }
401
+
402
+ /**
403
+ * The live item already reflects effective state, so applying the delta
404
+ * keeps it there. Three fields need widening: tint colors resolve as one
405
+ * complete ColorStateList, so touching any tint slot (or the icon, whose
406
+ * application re-tints) requires all four; `showAsAction` must accompany a
407
+ * new icon, because action-item classification runs when it is set and
408
+ * setting an icon alone does not re-run it; and a submenu header must be
409
+ * re-asserted on every title change, because `MenuItem.setTitle`
410
+ * propagates into it and would leave it following the title until the
411
+ * next rebuild projected the effective `menuTitle` back.
412
+ */
413
+ private fun widenWithCoupledFields(
414
+ id: String,
415
+ delta: StackHeaderToolbarMenuElementOptions,
416
+ ): StackHeaderToolbarMenuElementOptions {
417
+ val effective = effectiveOptions(id)
418
+ var resolved = delta
419
+
420
+ if (delta.icon != null || delta.requiresIconTintColorUpdate) {
421
+ resolved =
422
+ resolved.copy(
423
+ iconTintColorNormal = effective.iconTintColorNormal,
424
+ iconTintColorPressed = effective.iconTintColorPressed,
425
+ iconTintColorFocused = effective.iconTintColorFocused,
426
+ iconTintColorDisabled = effective.iconTintColorDisabled,
427
+ )
428
+ if (delta.icon != null) {
429
+ resolved = resolved.copy(showAsAction = effective.showAsAction)
430
+ }
431
+ }
432
+
433
+ if (delta.title != null &&
434
+ delta.menuTitle == null &&
435
+ model.elementById[id] is StackHeaderToolbarMenuElementConfig.Submenu
436
+ ) {
437
+ resolved = resolved.copy(menuTitle = effective.menuTitle)
438
+ }
439
+
440
+ return resolved
441
+ }
442
+
443
+ // endregion
444
+
445
+ companion object {
446
+ private const val TAG = "StackHeaderToolbarMenuController"
447
+ }
448
+ }
@@ -0,0 +1,10 @@
1
+ package com.swmansion.rnscreens.stack.header.toolbar
2
+
3
+ internal interface StackHeaderToolbarMenuDelegate {
4
+ fun onMenuItemClicked(id: String)
5
+
6
+ fun onGroupSelectionChanged(
7
+ groupId: String,
8
+ selectedIds: List<String>,
9
+ )
10
+ }
@@ -21,6 +21,7 @@ import com.swmansion.rnscreens.stack.header.toolbar.model.StackHeaderToolbarMenu
21
21
  import com.swmansion.rnscreens.stack.header.toolbar.model.StackHeaderToolbarMenuItemType
22
22
  import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarFieldUpdate
23
23
  import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarMenuElementOptions
24
+ import com.swmansion.rnscreens.stack.header.toolbar.update.StackHeaderToolbarMenuElementRawUpdate
24
25
 
25
26
  /**
26
27
  * Parses React toolbar-menu props (Dynamic / ReadableMap) into the native menu
@@ -32,23 +33,37 @@ internal object StackHeaderToolbarMenuMapper {
32
33
  internal fun parseMenu(
33
34
  context: Context,
34
35
  value: Dynamic,
35
- ): Pair<StackHeaderToolbarMenuConfig, Map<String, StackHeaderToolbarMenuItemIconSource>> {
36
- if (value.isNull) return Pair(StackHeaderToolbarMenuConfig(emptyList(), emptyList()), emptyMap())
36
+ ): StackHeaderToolbarMenuConfig {
37
+ if (value.isNull) return StackHeaderToolbarMenuConfig(emptyList(), emptyList())
37
38
  val map =
38
39
  value.asMapOrNull()
39
40
  ?: throw JSApplicationIllegalArgumentException(
40
41
  "[RNScreens] toolbarMenu must be an object.",
41
42
  )
42
- val iconSources = mutableMapOf<String, StackHeaderToolbarMenuItemIconSource>()
43
- val config = StackHeaderToolbarMenuConfig(parseGroups(map), parseChildren(context, map, iconSources))
44
- return Pair(config, iconSources)
43
+ return StackHeaderToolbarMenuConfig(parseGroups(map), parseChildren(context, map))
45
44
  }
46
45
 
47
46
  // endregion
48
47
 
49
48
  // region Menu element command parsing
50
49
 
51
- internal fun parseMenuElementOptions(
50
+ internal fun parseElementRawUpdate(
51
+ context: Context,
52
+ id: String,
53
+ map: ReadableMap,
54
+ ): StackHeaderToolbarMenuElementRawUpdate =
55
+ StackHeaderToolbarMenuElementRawUpdate(
56
+ id = id,
57
+ options = parseMenuElementOptions(context, map),
58
+ iconSource = parseCommandIconSource(map),
59
+ checked =
60
+ map.readNullableBooleanUpdate(
61
+ "checked",
62
+ StackHeaderToolbarMenuItemDefaults.INITIAL_TOGGLE_STATE,
63
+ ),
64
+ )
65
+
66
+ private fun parseMenuElementOptions(
52
67
  context: Context,
53
68
  map: ReadableMap,
54
69
  ): StackHeaderToolbarMenuElementOptions =
@@ -64,20 +79,15 @@ internal object StackHeaderToolbarMenuMapper {
64
79
  "showAsAction",
65
80
  StackHeaderToolbarMenuItemDefaults.SHOW_AS_ACTION,
66
81
  ),
67
- icon = null,
68
82
  iconTintColorNormal = map.readNullableColorUpdate(context, "iconTintColorNormal"),
69
83
  iconTintColorPressed = map.readNullableColorUpdate(context, "iconTintColorPressed"),
70
84
  iconTintColorFocused = map.readNullableColorUpdate(context, "iconTintColorFocused"),
71
85
  iconTintColorDisabled = map.readNullableColorUpdate(context, "iconTintColorDisabled"),
72
- checked =
73
- map.readNullableBooleanUpdate(
74
- "checked",
75
- StackHeaderToolbarMenuItemDefaults.INITIAL_TOGGLE_STATE,
76
- ),
77
86
  menuTitle = map.readNullableStringUpdate("menuTitle"),
78
87
  )
79
88
 
80
- internal fun parseMenuElementIconSource(map: ReadableMap): StackHeaderToolbarMenuItemIconSource? {
89
+ // Command icons are optional deltas: null when neither key is present.
90
+ private fun parseCommandIconSource(map: ReadableMap): StackHeaderToolbarMenuItemIconSource? {
81
91
  if (!map.hasKey("drawableIconResourceName") && !map.hasKey("imageIconResource")) {
82
92
  return null
83
93
  }
@@ -112,7 +122,6 @@ internal object StackHeaderToolbarMenuMapper {
112
122
  private fun parseChildren(
113
123
  context: Context,
114
124
  map: ReadableMap,
115
- iconSources: MutableMap<String, StackHeaderToolbarMenuItemIconSource>,
116
125
  ): List<StackHeaderToolbarMenuElementConfig> {
117
126
  val array = map.getArray("children") ?: return emptyList()
118
127
  return (0 until array.size()).map { i ->
@@ -120,23 +129,21 @@ internal object StackHeaderToolbarMenuMapper {
120
129
  requireNotNull(array.getMap(i)) {
121
130
  "[RNScreens] Menu children array must contain valid menu element specification objects."
122
131
  }
123
- parseElement(context, child, iconSources)
132
+ parseElement(context, child)
124
133
  }
125
134
  }
126
135
 
127
136
  private fun parseElement(
128
137
  context: Context,
129
138
  map: ReadableMap,
130
- iconSources: MutableMap<String, StackHeaderToolbarMenuItemIconSource>,
131
139
  ): StackHeaderToolbarMenuElementConfig {
132
140
  val item = parseItemConfig(context, map)
133
- iconSources[item.id] = parseItemIconSource(map)
134
141
  return when (val type = map.readOptionalString("type")) {
135
142
  "menuItem" -> StackHeaderToolbarMenuElementConfig.MenuItem(item = item)
136
143
  "menu" ->
137
144
  StackHeaderToolbarMenuElementConfig.Submenu(
138
145
  item = item,
139
- menu = StackHeaderToolbarMenuConfig(parseGroups(map), parseChildren(context, map, iconSources)),
146
+ menu = StackHeaderToolbarMenuConfig(parseGroups(map), parseChildren(context, map)),
140
147
  menuTitle = map.readOptionalString("menuTitle"),
141
148
  )
142
149
 
@@ -147,7 +154,8 @@ internal object StackHeaderToolbarMenuMapper {
147
154
  }
148
155
  }
149
156
 
150
- private fun parseItemIconSource(map: ReadableMap): StackHeaderToolbarMenuItemIconSource =
157
+ // Prop icons are absolute per-item state: absent keys parse to defaults.
158
+ private fun parsePropIconSource(map: ReadableMap): StackHeaderToolbarMenuItemIconSource =
151
159
  StackHeaderToolbarMenuItemIconSource(
152
160
  drawableIconResourceName =
153
161
  map.getString("drawableIconResourceName")
@@ -172,7 +180,6 @@ internal object StackHeaderToolbarMenuMapper {
172
180
  hidden = map.readBoolean("hidden", StackHeaderToolbarMenuItemDefaults.HIDDEN),
173
181
  disabled = map.readBoolean("disabled", StackHeaderToolbarMenuItemDefaults.DISABLED),
174
182
  showAsAction = map.readShowAsActionEnum("showAsAction", StackHeaderToolbarMenuItemDefaults.SHOW_AS_ACTION),
175
- icon = null,
176
183
  iconTintColorNormal =
177
184
  map.readColor(
178
185
  context,
@@ -197,6 +204,7 @@ internal object StackHeaderToolbarMenuMapper {
197
204
  "iconTintColorDisabled",
198
205
  StackHeaderToolbarMenuItemDefaults.ICON_TINT_COLOR_DISABLED,
199
206
  ),
207
+ iconSource = parsePropIconSource(map),
200
208
  groupId = map.readOptionalString("groupId"),
201
209
  itemType = map.readItemTypeEnum("itemType", StackHeaderToolbarMenuItemDefaults.ITEM_TYPE),
202
210
  initialToggleState =
@@ -1,60 +1,8 @@
1
1
  package com.swmansion.rnscreens.stack.header.toolbar.model
2
2
 
3
- import android.graphics.drawable.Drawable
4
-
5
- // Immutable tree of data classes. Updates walk the tree recursively and use
6
- // reference comparison (!==) to detect actual changes — only the path from
7
- // the changed leaf to the root is copied; unchanged subtrees keep their
8
- // original instances.
3
+ // Immutable, Drawable-free tree of data classes parsed from the `toolbarMenu`
4
+ // prop, so structural equality is sound (used to detect real prop changes).
9
5
  internal data class StackHeaderToolbarMenuConfig(
10
6
  val groups: List<StackHeaderToolbarMenuGroupConfig>,
11
7
  val children: List<StackHeaderToolbarMenuElementConfig>,
12
- ) {
13
- internal fun updateItemIcon(
14
- id: String,
15
- icon: Drawable?,
16
- ): StackHeaderToolbarMenuConfig {
17
- val updated = updateChildrenIcon(children, id, icon)
18
- return if (updated !== children) copy(children = updated) else this
19
- }
20
-
21
- private fun updateChildrenIcon(
22
- children: List<StackHeaderToolbarMenuElementConfig>,
23
- id: String,
24
- icon: Drawable?,
25
- ): List<StackHeaderToolbarMenuElementConfig> {
26
- for ((index, element) in children.withIndex()) {
27
- val updated = updateElementIcon(element, id, icon)
28
- if (updated !== element) {
29
- return children.toMutableList().apply { set(index, updated) }
30
- }
31
- }
32
- return children
33
- }
34
-
35
- private fun updateElementIcon(
36
- element: StackHeaderToolbarMenuElementConfig,
37
- id: String,
38
- icon: Drawable?,
39
- ): StackHeaderToolbarMenuElementConfig {
40
- if (element.item.id == id) {
41
- if (icon !== element.item.icon) {
42
- val newItem = element.item.copy(icon = icon)
43
- return when (element) {
44
- is StackHeaderToolbarMenuElementConfig.MenuItem -> element.copy(item = newItem)
45
- is StackHeaderToolbarMenuElementConfig.Submenu -> element.copy(item = newItem)
46
- }
47
- }
48
- return element
49
- }
50
-
51
- if (element is StackHeaderToolbarMenuElementConfig.Submenu) {
52
- val updatedChildren = updateChildrenIcon(element.menu.children, id, icon)
53
- if (updatedChildren !== element.menu.children) {
54
- return element.copy(menu = element.menu.copy(children = updatedChildren))
55
- }
56
- }
57
-
58
- return element
59
- }
60
- }
8
+ )
@@ -8,13 +8,4 @@ internal data class StackHeaderToolbarMenuGroupMetadata(
8
8
  val itemGroupMap: Map<String, String>,
9
9
  val groupSingleSelection: Map<String, Boolean>,
10
10
  val groupMemberItems: Map<String, List<String>>,
11
- ) {
12
- companion object {
13
- val EMPTY =
14
- StackHeaderToolbarMenuGroupMetadata(
15
- emptyMap(),
16
- emptyMap(),
17
- emptyMap(),
18
- )
19
- }
20
- }
11
+ )