react-native-nitro-modal 0.1.0

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 (117) hide show
  1. package/LICENSE +20 -0
  2. package/NitroModal.podspec +29 -0
  3. package/README.md +39 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +124 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +284 -0
  9. package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +312 -0
  10. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +50 -0
  11. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +115 -0
  12. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +253 -0
  13. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +52 -0
  14. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalPackage.kt +28 -0
  15. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalViewManager.kt +67 -0
  16. package/android/src/main/java/com/margelo/nitro/nitromodal/PopupPresenter.kt +156 -0
  17. package/android/src/main/res/values/styles.xml +17 -0
  18. package/ios/BackdropView.swift +64 -0
  19. package/ios/HybridNitroModal.swift +329 -0
  20. package/ios/HybridNitroModalComponent+Children.mm +77 -0
  21. package/ios/ModalConfig.swift +73 -0
  22. package/ios/NitroModalHostView.swift +91 -0
  23. package/ios/PopupViewController.swift +223 -0
  24. package/ios/SheetViewController.swift +244 -0
  25. package/lib/module/NitroModal.js +188 -0
  26. package/lib/module/NitroModal.js.map +1 -0
  27. package/lib/module/NitroModal.nitro.js +4 -0
  28. package/lib/module/NitroModal.nitro.js.map +1 -0
  29. package/lib/module/NitroModalView.js +17 -0
  30. package/lib/module/NitroModalView.js.map +1 -0
  31. package/lib/module/NitroModalView.native.js +6 -0
  32. package/lib/module/NitroModalView.native.js.map +1 -0
  33. package/lib/module/index.js +4 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/typescript/package.json +1 -0
  37. package/lib/typescript/src/NitroModal.d.ts +59 -0
  38. package/lib/typescript/src/NitroModal.d.ts.map +1 -0
  39. package/lib/typescript/src/NitroModal.nitro.d.ts +78 -0
  40. package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -0
  41. package/lib/typescript/src/NitroModalView.d.ts +4 -0
  42. package/lib/typescript/src/NitroModalView.d.ts.map +1 -0
  43. package/lib/typescript/src/NitroModalView.native.d.ts +3 -0
  44. package/lib/typescript/src/NitroModalView.native.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +3 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/nitro.json +23 -0
  48. package/nitrogen/generated/android/c++/JDismissReason.hpp +64 -0
  49. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  50. package/nitrogen/generated/android/c++/JFunc_void_DismissReason.hpp +77 -0
  51. package/nitrogen/generated/android/c++/JFunc_void_ModalContentArea.hpp +77 -0
  52. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  53. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +344 -0
  54. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +106 -0
  55. package/nitrogen/generated/android/c++/JKeyboardBehavior.hpp +61 -0
  56. package/nitrogen/generated/android/c++/JModalContentArea.hpp +61 -0
  57. package/nitrogen/generated/android/c++/JModalMode.hpp +58 -0
  58. package/nitrogen/generated/android/c++/JPopupAnimation.hpp +61 -0
  59. package/nitrogen/generated/android/c++/JSheetDetent.hpp +64 -0
  60. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +174 -0
  61. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.hpp +54 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/DismissReason.kt +25 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void.kt +78 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_DismissReason.kt +78 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_ModalContentArea.kt +78 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/Func_void_double.kt +78 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +237 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/KeyboardBehavior.kt +24 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalContentArea.kt +56 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/ModalMode.kt +23 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/PopupAnimation.kt +24 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +25 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/nitromodalOnLoad.kt +35 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalManager.kt +96 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/views/HybridNitroModalStateUpdater.kt +23 -0
  76. package/nitrogen/generated/android/nitromodal+autolinking.cmake +83 -0
  77. package/nitrogen/generated/android/nitromodal+autolinking.gradle +27 -0
  78. package/nitrogen/generated/android/nitromodalOnLoad.cpp +64 -0
  79. package/nitrogen/generated/android/nitromodalOnLoad.hpp +34 -0
  80. package/nitrogen/generated/ios/NitroModal+autolinking.rb +62 -0
  81. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.cpp +65 -0
  82. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +237 -0
  83. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +64 -0
  84. package/nitrogen/generated/ios/NitroModalAutolinking.mm +33 -0
  85. package/nitrogen/generated/ios/NitroModalAutolinking.swift +26 -0
  86. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.cpp +11 -0
  87. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +247 -0
  88. package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +264 -0
  89. package/nitrogen/generated/ios/swift/DismissReason.swift +48 -0
  90. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  91. package/nitrogen/generated/ios/swift/Func_void_DismissReason.swift +46 -0
  92. package/nitrogen/generated/ios/swift/Func_void_ModalContentArea.swift +46 -0
  93. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  94. package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +77 -0
  95. package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +554 -0
  96. package/nitrogen/generated/ios/swift/KeyboardBehavior.swift +44 -0
  97. package/nitrogen/generated/ios/swift/ModalContentArea.swift +34 -0
  98. package/nitrogen/generated/ios/swift/ModalMode.swift +40 -0
  99. package/nitrogen/generated/ios/swift/PopupAnimation.swift +44 -0
  100. package/nitrogen/generated/ios/swift/SheetDetent.swift +48 -0
  101. package/nitrogen/generated/shared/c++/DismissReason.hpp +84 -0
  102. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +65 -0
  103. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +124 -0
  104. package/nitrogen/generated/shared/c++/KeyboardBehavior.hpp +80 -0
  105. package/nitrogen/generated/shared/c++/ModalContentArea.hpp +87 -0
  106. package/nitrogen/generated/shared/c++/ModalMode.hpp +76 -0
  107. package/nitrogen/generated/shared/c++/PopupAnimation.hpp +80 -0
  108. package/nitrogen/generated/shared/c++/SheetDetent.hpp +84 -0
  109. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +74 -0
  110. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +151 -0
  111. package/nitrogen/generated/shared/json/NitroModalConfig.json +30 -0
  112. package/package.json +183 -0
  113. package/src/NitroModal.nitro.ts +92 -0
  114. package/src/NitroModal.tsx +292 -0
  115. package/src/NitroModalView.native.tsx +8 -0
  116. package/src/NitroModalView.tsx +24 -0
  117. package/src/index.tsx +12 -0
@@ -0,0 +1,312 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.view.WindowManager
6
+ import androidx.core.graphics.Insets
7
+ import androidx.core.view.ViewCompat
8
+ import androidx.core.view.WindowInsetsCompat
9
+ import com.facebook.proguard.annotations.DoNotStrip
10
+ import com.facebook.react.bridge.LifecycleEventListener
11
+ import com.facebook.react.uimanager.ThemedReactContext
12
+
13
+ /**
14
+ * The Nitro Hybrid View. Props arrive on the UI thread (Fabric `updateState`);
15
+ * methods arrive on the JS thread and hop to the UI thread.
16
+ *
17
+ * Presentation is a small state machine so that rapid `isOpen` toggles,
18
+ * native dismissals and unmounts always settle on a consistent state, and
19
+ * every accepted present request ends with exactly one `onDismiss`.
20
+ */
21
+ @DoNotStrip
22
+ class HybridNitroModal(private val reactContext: ThemedReactContext) :
23
+ HybridNitroModalSpec(), LifecycleEventListener, ModalPresenter.Listener {
24
+
25
+ override val view = NitroModalHostView(reactContext)
26
+
27
+ // region Props
28
+
29
+ override var isOpen: Boolean = false
30
+ set(value) {
31
+ if (field == value) return
32
+ field = value
33
+ if (value) requestPresent() else requestDismiss(DismissReason.PROGRAMMATIC)
34
+ }
35
+
36
+ override var mode: ModalMode = ModalMode.BOTTOMSHEET
37
+ override var detents: Array<SheetDetent> = arrayOf(SheetDetent.FITCONTENT)
38
+ override var initialDetentIndex: Double = 0.0
39
+ override var backdropColor: Double = 0xFF000000.toDouble()
40
+ override var backdropOpacity: Double = 0.4
41
+ override var backdropBlurRadius: Double = 0.0
42
+ override var dismissOnBackdropPress: Boolean = true
43
+ override var dismissOnSwipe: Boolean = true
44
+ override var dismissOnBackButton: Boolean = true
45
+ override var grabberVisible: Boolean = false
46
+ override var cornerRadius: Double = -1.0
47
+ override var contentBackgroundColor: Double? = null
48
+ override var keyboardBehavior: KeyboardBehavior = KeyboardBehavior.PAN
49
+ override var popupAnimation: PopupAnimation = PopupAnimation.SCALE
50
+
51
+ override var onPresent: (() -> Unit)? = null
52
+ override var onDismiss: ((reason: DismissReason) -> Unit)? = null
53
+ override var onDetentChange: ((index: Double) -> Unit)? = null
54
+ override var onBackdropPress: (() -> Unit)? = null
55
+ override var onBackButtonPress: (() -> Unit)? = null
56
+ override var onContentAreaChange: ((area: ModalContentArea) -> Unit)? = null
57
+
58
+ // endregion
59
+
60
+ private enum class Phase { IDLE, PRESENTING, PRESENTED, DISMISSING }
61
+
62
+ private val mainHandler = Handler(Looper.getMainLooper())
63
+ private var config = ModalConfig()
64
+ private var phase = Phase.IDLE
65
+
66
+ /** What React (or an imperative call) asked for. */
67
+ private var wantsOpen = false
68
+
69
+ /** A present request was accepted and its `onDismiss` is still owed. */
70
+ private var sessionActive = false
71
+ private var dismissReason = DismissReason.PROGRAMMATIC
72
+ private var reconcileScheduled = false
73
+ private var isDropped = false
74
+ private var presenter: ModalPresenter? = null
75
+ private var lastContentArea: ModalContentArea? = null
76
+
77
+ private val reconcileRunnable = Runnable {
78
+ reconcileScheduled = false
79
+ reconcile()
80
+ }
81
+
82
+ init {
83
+ reactContext.addLifecycleEventListener(this)
84
+ view.onAttachedChange = { attached ->
85
+ if (attached) {
86
+ if (presenter == null) reportEstimatedContentArea()
87
+ scheduleReconcile()
88
+ }
89
+ }
90
+ view.contentRoot.onContentSizeChange = { presenter?.onContentSizeChanged() }
91
+ }
92
+
93
+ // region HybridView
94
+
95
+ override fun afterUpdate() {
96
+ val next = makeConfig()
97
+ if (next == config) return
98
+ config = next
99
+ presenter?.update(next) ?: reportEstimatedContentArea()
100
+ }
101
+
102
+ override fun onDropView() {
103
+ isDropped = true
104
+ wantsOpen = false
105
+ sessionActive = false
106
+ mainHandler.removeCallbacksAndMessages(null)
107
+ reactContext.removeLifecycleEventListener(this)
108
+ teardownPresenter()
109
+ }
110
+
111
+ // endregion
112
+
113
+ // region Methods (JS thread)
114
+
115
+ override fun present() {
116
+ mainHandler.post { requestPresent() }
117
+ }
118
+
119
+ override fun dismiss() {
120
+ mainHandler.post { requestDismiss(DismissReason.PROGRAMMATIC) }
121
+ }
122
+
123
+ override fun snapToDetent(index: Double) {
124
+ if (index.isNaN()) return
125
+ mainHandler.post { presenter?.snapToDetent(index.toInt()) }
126
+ }
127
+
128
+ // endregion
129
+
130
+ // region State machine
131
+
132
+ private fun requestPresent() {
133
+ if (isDropped) return
134
+ wantsOpen = true
135
+ sessionActive = true
136
+ scheduleReconcile()
137
+ }
138
+
139
+ private fun requestDismiss(reason: DismissReason) {
140
+ if (wantsOpen) dismissReason = reason
141
+ wantsOpen = false
142
+ scheduleReconcile()
143
+ }
144
+
145
+ /** Posted so children and layout from the same Fabric batch are mounted first. */
146
+ private fun scheduleReconcile() {
147
+ if (reconcileScheduled) return
148
+ reconcileScheduled = true
149
+ mainHandler.post(reconcileRunnable)
150
+ }
151
+
152
+ private fun reconcile() {
153
+ when (phase) {
154
+ Phase.PRESENTING, Phase.DISMISSING -> Unit // The transition reconciles when done.
155
+ Phase.PRESENTED -> if (!wantsOpen) startDismiss()
156
+ Phase.IDLE -> when {
157
+ wantsOpen -> startPresent()
158
+ sessionActive -> endSession() // Cancelled before anything appeared.
159
+ }
160
+ }
161
+ }
162
+
163
+ private fun startPresent() {
164
+ // Without an attached host or activity there is nothing to present over;
165
+ // attaching / resuming retries.
166
+ if (!view.isAttachedToWindow) return
167
+ val activity = reactContext.currentActivity ?: return
168
+ if (activity.isFinishing || activity.isDestroyed) return
169
+
170
+ val next = when (config.mode) {
171
+ ModalMode.POPUP -> PopupPresenter(activity, reactContext, view.contentRoot, config, this)
172
+ ModalMode.BOTTOMSHEET -> BottomSheetPresenter(activity, reactContext, view.contentRoot, config, this)
173
+ }
174
+ presenter = next
175
+ phase = Phase.PRESENTING
176
+ try {
177
+ next.show()
178
+ } catch (e: WindowManager.BadTokenException) {
179
+ // The activity window isn't ready; retry on the next resume.
180
+ next.dismissImmediately()
181
+ presenter = null
182
+ phase = Phase.IDLE
183
+ }
184
+ }
185
+
186
+ private fun startDismiss() {
187
+ val current = presenter ?: return finishDismiss()
188
+ phase = Phase.DISMISSING
189
+ current.dismiss(dismissReason)
190
+ }
191
+
192
+ private fun finishDismiss() {
193
+ phase = Phase.IDLE
194
+ presenter = null
195
+ endSession()
196
+ reconcile()
197
+ }
198
+
199
+ private fun endSession() {
200
+ if (!sessionActive) return
201
+ val reason = dismissReason
202
+ dismissReason = DismissReason.PROGRAMMATIC
203
+ // A present requested while we were dismissing starts a new session.
204
+ sessionActive = wantsOpen
205
+ if (!isDropped) onDismiss?.invoke(reason)
206
+ }
207
+
208
+ private fun teardownPresenter() {
209
+ presenter?.dismissImmediately()
210
+ presenter = null
211
+ phase = Phase.IDLE
212
+ }
213
+
214
+ // endregion
215
+
216
+ // region ModalPresenter.Listener
217
+
218
+ override fun onPresented() {
219
+ if (phase != Phase.PRESENTING) return
220
+ phase = Phase.PRESENTED
221
+ if (!isDropped) onPresent?.invoke()
222
+ reconcile()
223
+ }
224
+
225
+ override fun onDismissed(reason: DismissReason) {
226
+ if (phase != Phase.DISMISSING) {
227
+ // Dismissed natively (swipe) rather than by us.
228
+ wantsOpen = false
229
+ dismissReason = reason
230
+ }
231
+ finishDismiss()
232
+ }
233
+
234
+ override fun onBackdropPress() {
235
+ onBackdropPress?.invoke()
236
+ if (!config.dismissOnBackdropPress) return
237
+ requestDismiss(DismissReason.BACKDROP)
238
+ reconcile()
239
+ }
240
+
241
+ override fun onBackPress() {
242
+ onBackButtonPress?.invoke()
243
+ if (!config.dismissOnBackButton) return
244
+ requestDismiss(DismissReason.BACKBUTTON)
245
+ reconcile()
246
+ }
247
+
248
+ override fun onDetentChange(index: Int) {
249
+ onDetentChange?.invoke(index.toDouble())
250
+ }
251
+
252
+ override fun onContentAreaChange(width: Int, height: Int) {
253
+ val area = ModalContentArea(
254
+ kotlin.math.floor(reactContext.pxToDp(width)),
255
+ kotlin.math.floor(reactContext.pxToDp(height)),
256
+ )
257
+ if (area == lastContentArea || area.width <= 0 || area.height <= 0) return
258
+ lastContentArea = area
259
+ onContentAreaChange?.invoke(area)
260
+ }
261
+
262
+ // endregion
263
+
264
+ // region LifecycleEventListener
265
+
266
+ override fun onHostResume() = scheduleReconcile()
267
+
268
+ override fun onHostPause() = Unit
269
+
270
+ override fun onHostDestroy() {
271
+ // The activity is going away; its windows go with it.
272
+ teardownPresenter()
273
+ if (sessionActive) {
274
+ wantsOpen = false
275
+ endSession()
276
+ }
277
+ }
278
+
279
+ // endregion
280
+
281
+ private fun makeConfig() = ModalConfig(
282
+ mode = mode,
283
+ detents = detents.toList(),
284
+ initialDetentIndex = if (initialDetentIndex.isNaN()) 0 else initialDetentIndex.toInt(),
285
+ backdropColor = backdropColor.toProcessedColor(),
286
+ backdropOpacity = backdropOpacity.toFloat(),
287
+ backdropBlurRadius = backdropBlurRadius.toFloat(),
288
+ dismissOnBackdropPress = dismissOnBackdropPress,
289
+ dismissOnSwipe = dismissOnSwipe,
290
+ dismissOnBackButton = dismissOnBackButton,
291
+ grabberVisible = grabberVisible,
292
+ cornerRadius = cornerRadius.takeIf { it >= 0 }?.toFloat(),
293
+ contentBackgroundColor = contentBackgroundColor?.toProcessedColor(),
294
+ keyboardBehavior = keyboardBehavior,
295
+ popupAnimation = popupAnimation,
296
+ )
297
+
298
+ /** Lets React size the content before the first presentation. */
299
+ private fun reportEstimatedContentArea() {
300
+ val activity = reactContext.currentActivity ?: return
301
+ val decor = activity.window?.decorView ?: return
302
+ if (decor.width == 0 || decor.height == 0) return
303
+ val insets = ViewCompat.getRootWindowInsets(decor)
304
+ ?.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
305
+ ?: Insets.NONE
306
+ val (width, height) = when (config.mode) {
307
+ ModalMode.POPUP -> PopupPresenter.estimateContentArea(activity, decor.width, decor.height, insets)
308
+ ModalMode.BOTTOMSHEET -> BottomSheetPresenter.estimateContentArea(activity, config.detents, decor.width, decor.height, insets)
309
+ }
310
+ onContentAreaChange(width, height)
311
+ }
312
+ }
@@ -0,0 +1,50 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import android.graphics.Color
6
+
7
+ /** Snapshot of the React props that shape a presentation. */
8
+ internal data class ModalConfig(
9
+ val mode: ModalMode = ModalMode.BOTTOMSHEET,
10
+ val detents: List<SheetDetent> = listOf(SheetDetent.FITCONTENT),
11
+ val initialDetentIndex: Int = 0,
12
+ val backdropColor: Int = Color.BLACK,
13
+ val backdropOpacity: Float = 0.4f,
14
+ /** dp, 0 disables the blur. */
15
+ val backdropBlurRadius: Float = 0f,
16
+ val dismissOnBackdropPress: Boolean = true,
17
+ val dismissOnSwipe: Boolean = true,
18
+ val dismissOnBackButton: Boolean = true,
19
+ val grabberVisible: Boolean = false,
20
+ /** dp, `null` uses the platform default. */
21
+ val cornerRadius: Float? = null,
22
+ /** `null` uses the system surface color. */
23
+ val contentBackgroundColor: Int? = null,
24
+ val keyboardBehavior: KeyboardBehavior = KeyboardBehavior.PAN,
25
+ val popupAnimation: PopupAnimation = PopupAnimation.SCALE,
26
+ ) {
27
+ val clampedInitialDetentIndex: Int
28
+ get() = initialDetentIndex.coerceIn(0, (detents.size - 1).coerceAtLeast(0))
29
+
30
+ /** Backdrop color with `backdropOpacity` folded into its alpha. */
31
+ val effectiveBackdropColor: Int
32
+ get() {
33
+ val alpha = (Color.alpha(backdropColor) * backdropOpacity.coerceIn(0f, 1f)).toInt()
34
+ return Color.argb(alpha, Color.red(backdropColor), Color.green(backdropColor), Color.blue(backdropColor))
35
+ }
36
+
37
+ fun surfaceColor(context: Context): Int {
38
+ contentBackgroundColor?.let { return it }
39
+ val night = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
40
+ Configuration.UI_MODE_NIGHT_YES
41
+ return if (night) Color.rgb(0x1C, 0x1B, 0x1F) else Color.WHITE
42
+ }
43
+ }
44
+
45
+ /** Decodes a color produced by React Native's `processColor` (ARGB int). */
46
+ internal fun Double.toProcessedColor(): Int = toLong().toInt()
47
+
48
+ internal fun Context.dpToPx(dp: Float): Float = dp * resources.displayMetrics.density
49
+
50
+ internal fun Context.pxToDp(px: Int): Double = (px / resources.displayMetrics.density).toDouble()
@@ -0,0 +1,115 @@
1
+ package com.margelo.nitro.nitromodal
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.view.MotionEvent
5
+ import android.view.View
6
+ import com.facebook.react.config.ReactFeatureFlags
7
+ import com.facebook.react.uimanager.JSPointerDispatcher
8
+ import com.facebook.react.uimanager.JSTouchDispatcher
9
+ import com.facebook.react.uimanager.RootView
10
+ import com.facebook.react.uimanager.ThemedReactContext
11
+ import com.facebook.react.uimanager.events.EventDispatcher
12
+ import com.facebook.react.views.view.ReactViewGroup
13
+
14
+ /**
15
+ * Holds the React children inside the modal window.
16
+ *
17
+ * The dialog is a separate window, so touches there never reach the React
18
+ * root view. Like React Native's own `Modal`, this view is a [RootView] that
19
+ * forwards touch and pointer events to JS itself. Children are laid out by
20
+ * Fabric; [ReactViewGroup.onLayout] is a no-op.
21
+ */
22
+ @SuppressLint("ViewConstructor")
23
+ internal class ModalContentRoot(private val reactContext: ThemedReactContext) :
24
+ ReactViewGroup(reactContext), RootView {
25
+
26
+ var eventDispatcher: EventDispatcher? = null
27
+
28
+ /** Called when a child's laid-out bounds change. */
29
+ var onContentSizeChange: (() -> Unit)? = null
30
+
31
+ private val touchDispatcher = JSTouchDispatcher(this)
32
+ private val pointerDispatcher: JSPointerDispatcher? =
33
+ if (ReactFeatureFlags.dispatchPointerEvents) JSPointerDispatcher(this) else null
34
+
35
+ private val childLayoutListener =
36
+ OnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
37
+ if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
38
+ onContentSizeChange?.invoke()
39
+ }
40
+ }
41
+
42
+ /** Bounding size of the React children, in px. */
43
+ val contentWidth: Int
44
+ get() = (0 until childCount).maxOfOrNull { getChildAt(it).right } ?: 0
45
+
46
+ val contentHeight: Int
47
+ get() = (0 until childCount).maxOfOrNull { getChildAt(it).bottom } ?: 0
48
+
49
+ override fun onViewAdded(child: View) {
50
+ super.onViewAdded(child)
51
+ child.addOnLayoutChangeListener(childLayoutListener)
52
+ onContentSizeChange?.invoke()
53
+ }
54
+
55
+ override fun onViewRemoved(child: View) {
56
+ super.onViewRemoved(child)
57
+ child.removeOnLayoutChangeListener(childLayoutListener)
58
+ onContentSizeChange?.invoke()
59
+ }
60
+
61
+ // region Touch forwarding (mirrors ReactModalHostView.DialogRootViewGroup)
62
+
63
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
64
+ eventDispatcher?.let {
65
+ touchDispatcher.handleTouchEvent(event, it, reactContext)
66
+ pointerDispatcher?.handleMotionEvent(event, it, true)
67
+ }
68
+ return super.onInterceptTouchEvent(event)
69
+ }
70
+
71
+ @SuppressLint("ClickableViewAccessibility")
72
+ override fun onTouchEvent(event: MotionEvent): Boolean {
73
+ eventDispatcher?.let {
74
+ touchDispatcher.handleTouchEvent(event, it, reactContext)
75
+ pointerDispatcher?.handleMotionEvent(event, it, false)
76
+ }
77
+ super.onTouchEvent(event)
78
+ // Keep receiving the gesture even if no child is interested in it.
79
+ return true
80
+ }
81
+
82
+ override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
83
+ eventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, true) }
84
+ return super.onInterceptHoverEvent(event)
85
+ }
86
+
87
+ override fun onHoverEvent(event: MotionEvent): Boolean {
88
+ eventDispatcher?.let { pointerDispatcher?.handleMotionEvent(event, it, false) }
89
+ return super.onHoverEvent(event)
90
+ }
91
+
92
+ override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
93
+ eventDispatcher?.let {
94
+ touchDispatcher.onChildStartedNativeGesture(ev, it, reactContext)
95
+ pointerDispatcher?.onChildStartedNativeGesture(childView, ev, it)
96
+ }
97
+ }
98
+
99
+ override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
100
+ eventDispatcher?.let { touchDispatcher.onChildEndedNativeGesture(ev, it) }
101
+ pointerDispatcher?.onChildEndedNativeGesture()
102
+ }
103
+
104
+ override fun handleException(t: Throwable) {
105
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
106
+ }
107
+
108
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
109
+ // Keep seeing events in onInterceptTouchEvent, but still let a nested
110
+ // scroller stop the sheet's drag behavior from stealing its gesture.
111
+ parent?.requestDisallowInterceptTouchEvent(disallowIntercept)
112
+ }
113
+
114
+ // endregion
115
+ }