react-native-theme-reveal 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +734 -2
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/baseline-prof.txt +4 -0
- package/android/src/main/java/expo/modules/themereveal/CapturePlan.kt +199 -0
- package/android/src/main/java/expo/modules/themereveal/CaptureRules.kt +50 -0
- package/android/src/main/java/expo/modules/themereveal/GpuSnapshot.kt +367 -0
- package/android/src/main/java/expo/modules/themereveal/KeptBitmaps.kt +102 -0
- package/android/src/main/java/expo/modules/themereveal/KeptTarget.kt +103 -0
- package/android/src/main/java/expo/modules/themereveal/Motion.kt +79 -0
- package/android/src/main/java/expo/modules/themereveal/Once.kt +13 -0
- package/android/src/main/java/expo/modules/themereveal/RenderThreadReveal.kt +107 -0
- package/android/src/main/java/expo/modules/themereveal/RevealOverlayView.kt +192 -0
- package/android/src/main/java/expo/modules/themereveal/SecondSnapshotWait.kt +112 -0
- package/android/src/main/java/expo/modules/themereveal/ShapePath.kt +47 -0
- package/android/src/main/java/expo/modules/themereveal/ThemeRevealModule.kt +904 -0
- package/android/src/main/java/expo/modules/themereveal/TransitionArgs.kt +50 -0
- package/android/src/main/java/expo/modules/themereveal/WindowSnapshot.kt +536 -0
- package/build/ThemeRevealModule.d.ts +11 -0
- package/build/ThemeRevealModule.d.ts.map +1 -0
- package/build/ThemeRevealModule.js +11 -0
- package/build/ThemeRevealModule.js.map +1 -0
- package/build/createThemeReveal.d.ts +39 -0
- package/build/createThemeReveal.d.ts.map +1 -0
- package/build/createThemeReveal.js +186 -0
- package/build/createThemeReveal.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/originFromPress.d.ts +12 -0
- package/build/originFromPress.d.ts.map +1 -0
- package/build/originFromPress.js +26 -0
- package/build/originFromPress.js.map +1 -0
- package/build/resolveTransition.d.ts +13 -0
- package/build/resolveTransition.d.ts.map +1 -0
- package/build/resolveTransition.js +43 -0
- package/build/resolveTransition.js.map +1 -0
- package/build/store.d.ts +8 -0
- package/build/store.d.ts.map +1 -0
- package/build/store.js +23 -0
- package/build/store.js.map +1 -0
- package/build/types.d.ts +31 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Motion.swift +171 -0
- package/ios/RevealOverlay.swift +115 -0
- package/ios/SVGPath.swift +373 -0
- package/ios/ThemeReveal.podspec +26 -0
- package/ios/ThemeRevealModule.swift +264 -0
- package/mocks/ThemeReveal.js +10 -0
- package/package.json +67 -4
- package/src/ThemeRevealModule.ts +24 -0
- package/src/createThemeReveal.ts +262 -0
- package/src/index.ts +14 -0
- package/src/originFromPress.ts +40 -0
- package/src/resolveTransition.ts +63 -0
- package/src/store.ts +30 -0
- package/src/types.ts +17 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package expo.modules.themereveal
|
|
2
|
+
|
|
3
|
+
// The start argument, read from the JS object by hand. Expo would convert a Record with Kotlin reflection, and its
|
|
4
|
+
// first conversion blocked the main thread on the first start (85-102 ms on a Mi 11 Lite, 18-27 ms on a Pixel 8
|
|
5
|
+
// Pro). A missing, null or wrong-typed field gets the default the Records had, so start takes the same path for it.
|
|
6
|
+
// A null or undefined inside origin or shape never gets here, because Expo's conversion of the nested object
|
|
7
|
+
// rejects start before this runs. resolveTransition in JS keeps such values out of start.
|
|
8
|
+
internal data class PointArgs(val x: Double, val y: Double) {
|
|
9
|
+
companion object {
|
|
10
|
+
fun from(value: Any?): PointArgs? {
|
|
11
|
+
val map = value as? Map<*, *> ?: return null
|
|
12
|
+
return PointArgs(map.number("x"), map.number("y"))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
internal data class ShapeArgs(val path: String, val anchor: PointArgs, val inscribedRadius: Double) {
|
|
18
|
+
companion object {
|
|
19
|
+
fun from(value: Any?): ShapeArgs? {
|
|
20
|
+
val map = value as? Map<*, *> ?: return null
|
|
21
|
+
return ShapeArgs(
|
|
22
|
+
map.string("path") ?: "",
|
|
23
|
+
PointArgs.from(map["anchor"]) ?: PointArgs(0.0, 0.0),
|
|
24
|
+
map.number("inscribedRadius"),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
internal data class TransitionArgs(
|
|
31
|
+
val type: String,
|
|
32
|
+
val durationMs: Double,
|
|
33
|
+
val origin: PointArgs?,
|
|
34
|
+
val shape: ShapeArgs?,
|
|
35
|
+
val direction: String?,
|
|
36
|
+
) {
|
|
37
|
+
companion object {
|
|
38
|
+
fun from(map: Map<*, *>): TransitionArgs = TransitionArgs(
|
|
39
|
+
type = map.string("type") ?: "",
|
|
40
|
+
durationMs = map.number("durationMs"),
|
|
41
|
+
origin = PointArgs.from(map["origin"]),
|
|
42
|
+
shape = ShapeArgs.from(map["shape"]),
|
|
43
|
+
direction = map.string("direction"),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun Map<*, *>.number(key: String): Double = (this[key] as? Number)?.toDouble() ?: 0.0
|
|
49
|
+
|
|
50
|
+
private fun Map<*, *>.string(key: String): String? = this[key] as? String
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
package expo.modules.themereveal
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.pm.ActivityInfo
|
|
5
|
+
import android.graphics.Bitmap
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.graphics.Rect
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.HandlerThread
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import android.os.Process
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import android.view.PixelCopy
|
|
16
|
+
import android.view.View
|
|
17
|
+
import android.view.ViewGroup
|
|
18
|
+
import android.view.Window
|
|
19
|
+
import android.view.WindowManager
|
|
20
|
+
import com.facebook.react.uimanager.RootView
|
|
21
|
+
import com.facebook.react.views.modal.ReactModalHostView
|
|
22
|
+
|
|
23
|
+
// Overlay goes into host's overlay at (hostX, hostY), bitmap-sized. The bitmap covers cover (the activity decor
|
|
24
|
+
// view), whose top-left corner is at (screenX, screenY) on screen.
|
|
25
|
+
internal class Snapshot(
|
|
26
|
+
val bitmap: Bitmap,
|
|
27
|
+
val host: ViewGroup,
|
|
28
|
+
val cover: View,
|
|
29
|
+
val hostX: Int,
|
|
30
|
+
val hostY: Int,
|
|
31
|
+
val screenX: Int,
|
|
32
|
+
val screenY: Int,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
internal object WindowSnapshot {
|
|
36
|
+
private const val TAG = "ThemeReveal"
|
|
37
|
+
|
|
38
|
+
// Activity-window bitmaps for the next PixelCopy capture. A GPU snapshot is a hardware bitmap, and those are
|
|
39
|
+
// immutable: PixelCopy can't copy into it, so it isn't kept. Main thread only.
|
|
40
|
+
val kept = KeptBitmaps<Bitmap>(1, { it.width }, { it.height }, { it.isRecycled }, { it.isMutable }) { it.recycle() }
|
|
41
|
+
|
|
42
|
+
fun capture(activity: Activity, root: View?, handler: Handler, onResult: (Snapshot?) -> Unit) =
|
|
43
|
+
capture(activity, root, handler, null, null, onResult)
|
|
44
|
+
|
|
45
|
+
// The RenderThread reveal's second snapshot, API 29+: GPU capture only, without hide (the overlay), into prepared
|
|
46
|
+
// if it's not null. Takes prepared and frees it, even when it doesn't use it. No PixelCopy fallback, since a
|
|
47
|
+
// PixelCopy would show the overlay. A failure gives null and doesn't turn off the GPU path: the next switch's first
|
|
48
|
+
// snapshot tests that path under its own rules.
|
|
49
|
+
fun captureSecond(
|
|
50
|
+
activity: Activity,
|
|
51
|
+
root: View?,
|
|
52
|
+
handler: Handler,
|
|
53
|
+
hide: View,
|
|
54
|
+
prepared: GpuOutput?,
|
|
55
|
+
onResult: (Snapshot?) -> Unit,
|
|
56
|
+
) = capture(activity, root, handler, hide, prepared, onResult)
|
|
57
|
+
|
|
58
|
+
// hide: see captureWindows.
|
|
59
|
+
private fun capture(
|
|
60
|
+
activity: Activity,
|
|
61
|
+
root: View?,
|
|
62
|
+
handler: Handler,
|
|
63
|
+
hide: View?,
|
|
64
|
+
prepared: GpuOutput?,
|
|
65
|
+
onResult: (Snapshot?) -> Unit,
|
|
66
|
+
) {
|
|
67
|
+
val window = activity.window
|
|
68
|
+
val decor = window?.decorView as? ViewGroup
|
|
69
|
+
if (window == null || decor == null) {
|
|
70
|
+
releaseOutput(prepared)
|
|
71
|
+
onResult(null)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
val screen = locationOf(decor)
|
|
75
|
+
if (root == null || root === decor) {
|
|
76
|
+
val pixelCopy = { done: (Bitmap?) -> Unit -> capture(window, decor.width, decor.height, handler, true, done) }
|
|
77
|
+
val draw = { canvas: Canvas -> GpuSnapshot.drawWindow(canvas, decor) }
|
|
78
|
+
captureWindows(activity, decor, listOf(window), handler, draw, pixelCopy, hide, prepared) {
|
|
79
|
+
onResult(it?.let { bitmap -> Snapshot(bitmap, decor, decor, 0, 0, screen[0], screen[1]) })
|
|
80
|
+
}
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
val dialog = findModalWindow(decor) { it.peekDecorView() === root }
|
|
84
|
+
val dialogDecor = dialog?.peekDecorView() as? ViewGroup
|
|
85
|
+
if (dialog == null || dialogDecor == null) {
|
|
86
|
+
releaseOutput(prepared)
|
|
87
|
+
onResult(null)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
val at = locationOf(dialogDecor)
|
|
91
|
+
val dialogX = at[0] - screen[0]
|
|
92
|
+
val dialogY = at[1] - screen[1]
|
|
93
|
+
val frame = Motion.dialogFrame(dialogX, dialogY, dialogDecor.width, dialogDecor.height, decor.width, decor.height)
|
|
94
|
+
val attributes = dialog.attributes
|
|
95
|
+
val dim = if (attributes.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND != 0) {
|
|
96
|
+
Motion.dimAlpha(attributes.dimAmount)
|
|
97
|
+
} else {
|
|
98
|
+
0
|
|
99
|
+
}
|
|
100
|
+
// The GPU draws both windows and the dim into one picture, like compose does with the two PixelCopy bitmaps.
|
|
101
|
+
val draw = { canvas: Canvas ->
|
|
102
|
+
GpuSnapshot.drawWindow(canvas, decor)
|
|
103
|
+
if (dim > 0) canvas.drawColor(Color.argb(dim, 0, 0, 0))
|
|
104
|
+
if (!frame.isEmpty) {
|
|
105
|
+
val count = canvas.save()
|
|
106
|
+
canvas.clipRect(frame.left, frame.top, frame.right, frame.bottom)
|
|
107
|
+
canvas.translate(dialogX.toFloat(), dialogY.toFloat())
|
|
108
|
+
GpuSnapshot.drawWindow(canvas, dialogDecor)
|
|
109
|
+
canvas.restoreToCount(count)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
val pixelCopy = { done: (Bitmap?) -> Unit ->
|
|
113
|
+
capturePair(window, decor.width, decor.height, dialog, dialogDecor.width, dialogDecor.height, handler) { back, front ->
|
|
114
|
+
if (back == null || front == null) {
|
|
115
|
+
back?.let(kept::give)
|
|
116
|
+
front?.recycle()
|
|
117
|
+
done(null)
|
|
118
|
+
return@capturePair
|
|
119
|
+
}
|
|
120
|
+
val composed = try {
|
|
121
|
+
compose(back, front, frame, dialogX, dialogY, dim)
|
|
122
|
+
true
|
|
123
|
+
} catch (_: RuntimeException) {
|
|
124
|
+
false
|
|
125
|
+
} finally {
|
|
126
|
+
front.recycle()
|
|
127
|
+
}
|
|
128
|
+
if (!composed) {
|
|
129
|
+
kept.give(back)
|
|
130
|
+
done(null)
|
|
131
|
+
return@capturePair
|
|
132
|
+
}
|
|
133
|
+
done(back)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
captureWindows(activity, decor, listOf(window, dialog), handler, draw, pixelCopy, hide, prepared) {
|
|
137
|
+
onResult(it?.let { bitmap -> Snapshot(bitmap, dialogDecor, decor, -dialogX, -dialogY, screen[0], screen[1]) })
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
142
|
+
|
|
143
|
+
// Capture target kept for the activity window's next first snapshot (keepTarget). Main thread only. A target a
|
|
144
|
+
// capture can't use is freed in a later main message, off that capture's path.
|
|
145
|
+
val target = KeptTarget<GpuOutput>(
|
|
146
|
+
{ releaseOutput(it, KEPT_TARGET) },
|
|
147
|
+
{ output -> mainHandler.post { releaseOutput(output, KEPT_TARGET) } },
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// Set when a GPU capture timed out or threw: from then on the process captures with PixelCopy. Main thread only.
|
|
151
|
+
var gpuOff = false
|
|
152
|
+
private set
|
|
153
|
+
|
|
154
|
+
// Snapshot of windows, decor-sized, drawn on the GPU on API 29+. Read back with PixelCopy on API 26-28, for a wide
|
|
155
|
+
// color window, and when the GPU capture fails. With hide set (the second snapshot): the draw leaves that view
|
|
156
|
+
// out, null instead of PixelCopy, and a failure leaves gpuOff alone. prepared: see captureSecond.
|
|
157
|
+
private fun captureWindows(
|
|
158
|
+
activity: Activity,
|
|
159
|
+
decor: View,
|
|
160
|
+
windows: List<Window>,
|
|
161
|
+
handler: Handler,
|
|
162
|
+
draw: (Canvas) -> Unit,
|
|
163
|
+
pixelCopy: ((Bitmap?) -> Unit) -> Unit,
|
|
164
|
+
hide: View?,
|
|
165
|
+
prepared: GpuOutput?,
|
|
166
|
+
onResult: (Bitmap?) -> Unit,
|
|
167
|
+
) {
|
|
168
|
+
val gpuOnly = hide != null
|
|
169
|
+
val noCopy = { done: (Bitmap?) -> Unit -> done(null) }
|
|
170
|
+
val fallback = if (gpuOnly) noCopy else pixelCopy
|
|
171
|
+
val api = Build.VERSION.SDK_INT
|
|
172
|
+
if (api >= Build.VERSION_CODES.Q) {
|
|
173
|
+
val decors = windows.mapNotNull { it.peekDecorView() }
|
|
174
|
+
val defaultColor = windows.all { it.colorMode == ActivityInfo.COLOR_MODE_DEFAULT }
|
|
175
|
+
val accelerated = decors.all { it.isHardwareAccelerated }
|
|
176
|
+
// A decor is dirty while a view change waits for the window's next frame. Read it before the draw clears it.
|
|
177
|
+
val dirty = decors.filter { it.isDirty }
|
|
178
|
+
val pending = dirty.isNotEmpty()
|
|
179
|
+
val forceDark = pending && GpuSnapshot.forceDarkCanApply(activity)
|
|
180
|
+
val light = if (CapturePlan.path(api, defaultColor, accelerated, forceDark, gpuOff) == CapturePath.GPU) {
|
|
181
|
+
GpuSnapshot.lightOf(activity, decor)
|
|
182
|
+
} else {
|
|
183
|
+
null
|
|
184
|
+
}
|
|
185
|
+
if (light != null && decor.width > 0 && decor.height > 0 && !preCreates.skipsGpu()) {
|
|
186
|
+
if (pending && Log.isLoggable(TAG, Log.DEBUG)) {
|
|
187
|
+
Log.d(TAG, "a view change waits for its frame in ${dirty.size} window(s)")
|
|
188
|
+
}
|
|
189
|
+
// Image arrives on the copy thread, where a warm-up pre-create can hold it back past the timeout.
|
|
190
|
+
val busyAtStart = preCreates.busy
|
|
191
|
+
val countAtStart = preCreates.count
|
|
192
|
+
// The view gets hidden inside the draw, after the dirty read above: hiding it marks its window dirty, which
|
|
193
|
+
// would read as a view change under force dark.
|
|
194
|
+
val record = if (hide == null) draw else drawWithout(hide, draw)
|
|
195
|
+
// A first snapshot draws into the kept target if it has the decor's size.
|
|
196
|
+
val output = prepared ?: if (gpuOnly) null else target.take(decor.width, decor.height)
|
|
197
|
+
if (output == null && !gpuOnly && target.preparing && Log.isLoggable(TAG, Log.DEBUG)) {
|
|
198
|
+
Log.d(TAG, "a snapshot starts while the kept target is made: its image waits behind that prepare")
|
|
199
|
+
}
|
|
200
|
+
GpuSnapshot.capture(decor.width, decor.height, light, dirty, handler, copyHandler, record, output) {
|
|
201
|
+
bitmap, failure ->
|
|
202
|
+
if (bitmap != null) {
|
|
203
|
+
onResult(bitmap)
|
|
204
|
+
return@capture
|
|
205
|
+
}
|
|
206
|
+
if (!gpuOnly && failure != null && preCreates.turnsGpuOff(failure, busyAtStart, countAtStart)) {
|
|
207
|
+
gpuOff = true
|
|
208
|
+
target.clear()
|
|
209
|
+
}
|
|
210
|
+
fallback(onResult)
|
|
211
|
+
}
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
releaseOutput(prepared)
|
|
216
|
+
fallback(onResult)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// draw with view hidden, on the UI thread. The window re-records the view in its next frame.
|
|
220
|
+
private fun drawWithout(view: View, draw: (Canvas) -> Unit): (Canvas) -> Unit = { canvas ->
|
|
221
|
+
view.visibility = View.INVISIBLE
|
|
222
|
+
try {
|
|
223
|
+
draw(canvas)
|
|
224
|
+
} finally {
|
|
225
|
+
view.visibility = View.VISIBLE
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Gets a GPU capture target of width x height ready on the copy thread (GpuSnapshot.prepareOutput). onReady gets it
|
|
230
|
+
// on handler, with the ms it took on the copy thread, or null if it failed. The caller frees it with releaseOutput or
|
|
231
|
+
// hands it to captureSecond. Main thread.
|
|
232
|
+
fun prepareOutput(width: Int, height: Int, handler: Handler, onReady: (GpuOutput?, Double) -> Unit) {
|
|
233
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || width <= 0 || height <= 0) {
|
|
234
|
+
onReady(null, 0.0)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
// Holds the copy thread like a warm-up pre-create.
|
|
238
|
+
preCreates.started()
|
|
239
|
+
copyHandler.post {
|
|
240
|
+
val start = System.nanoTime()
|
|
241
|
+
val output = try {
|
|
242
|
+
GpuSnapshot.prepareOutput(width, height, copyHandler)
|
|
243
|
+
} catch (_: RuntimeException) {
|
|
244
|
+
null
|
|
245
|
+
} catch (_: OutOfMemoryError) {
|
|
246
|
+
// HardwareRenderer throws it when HWUI can't make its native proxy.
|
|
247
|
+
null
|
|
248
|
+
}
|
|
249
|
+
val ms = (System.nanoTime() - start) / 1e6
|
|
250
|
+
handler.post {
|
|
251
|
+
preCreates.finished()
|
|
252
|
+
onReady(output, ms)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Main thread. Prepares the target the activity window's next first snapshot draws into, if KeptTarget wants one:
|
|
258
|
+
// a window of known size whose capture takes the GPU path, with none kept or in flight.
|
|
259
|
+
fun keepTarget(activity: Activity?, handler: Handler) {
|
|
260
|
+
val decor = activity?.window?.peekDecorView() ?: return
|
|
261
|
+
val width = decor.width
|
|
262
|
+
val height = decor.height
|
|
263
|
+
val gpuOn = warmUpOf(activity) == WarmUp.GPU
|
|
264
|
+
// A target of the old size, or of a window that left the GPU path, goes now and a new one replaces it.
|
|
265
|
+
target.drop(width, height, gpuOn)
|
|
266
|
+
if (!target.wants(width, height, gpuOn)) return
|
|
267
|
+
val ticket = target.started()
|
|
268
|
+
prepareOutput(width, height, handler) { output, ms ->
|
|
269
|
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
|
270
|
+
val made = output != null
|
|
271
|
+
Log.d(TAG, "kept a capture target of ${width}x$height in %.1f ms on the copy thread: %b".format(ms, made))
|
|
272
|
+
}
|
|
273
|
+
target.ready(ticket, output, width, height)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Frees a target from prepareOutput. Main thread.
|
|
278
|
+
fun releaseOutput(prepared: GpuOutput?, what: String = "an unused second-snapshot target") {
|
|
279
|
+
if (prepared != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) prepared.release(what)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private const val KEPT_TARGET = "the kept target"
|
|
283
|
+
|
|
284
|
+
// Pre-imports a held GPU snapshot's texture on the copy thread (GpuSnapshot.preImport). Failures are ignored: the
|
|
285
|
+
// overlay's first frame imports it as before. Main thread: handler is the main thread's.
|
|
286
|
+
fun preImport(bitmap: Bitmap, handler: Handler) {
|
|
287
|
+
val api = Build.VERSION.SDK_INT
|
|
288
|
+
// Same API check again, in a form lint understands.
|
|
289
|
+
if (api < Build.VERSION_CODES.Q || bitmap.isRecycled || !RevealPlan.preImports(api, pathOf(bitmap))) return
|
|
290
|
+
val node = try {
|
|
291
|
+
GpuSnapshot.preImportNode(bitmap)
|
|
292
|
+
} catch (_: RuntimeException) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
// Holds the copy thread like a warm-up pre-create.
|
|
296
|
+
preCreates.started()
|
|
297
|
+
copyHandler.post {
|
|
298
|
+
val start = System.nanoTime()
|
|
299
|
+
try {
|
|
300
|
+
GpuSnapshot.preImport(node)
|
|
301
|
+
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
|
302
|
+
Log.d(TAG, "pre-imported the held snapshot in %.1f ms".format((System.nanoTime() - start) / 1e6))
|
|
303
|
+
}
|
|
304
|
+
} catch (_: RuntimeException) {
|
|
305
|
+
// Ignored.
|
|
306
|
+
} catch (_: OutOfMemoryError) {
|
|
307
|
+
// HardwareRenderer throws it when HWUI can't make its native proxy.
|
|
308
|
+
} finally {
|
|
309
|
+
handler.post { preCreates.finished() }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// The path that took a snapshot: a GPU capture gives a hardware bitmap.
|
|
315
|
+
fun pathOf(bitmap: Bitmap): CapturePath =
|
|
316
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && bitmap.config == Bitmap.Config.HARDWARE) {
|
|
317
|
+
CapturePath.GPU
|
|
318
|
+
} else {
|
|
319
|
+
CapturePath.PIXEL_COPY
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Focused window root: the activity decor view, or a React Native modal's decor view.
|
|
323
|
+
fun focusedRoot(activity: Activity): View? {
|
|
324
|
+
val decor = activity.window?.decorView as? ViewGroup ?: return null
|
|
325
|
+
if (decor.hasWindowFocus()) return decor
|
|
326
|
+
return findModalWindow(decor) { it.peekDecorView()?.hasWindowFocus() == true }?.peekDecorView() ?: decor
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// React root that JS measures pageX and pageY against.
|
|
330
|
+
fun reactRootOf(view: View): View {
|
|
331
|
+
var current: View? = view
|
|
332
|
+
while (current != null) {
|
|
333
|
+
if (current is RootView) return current
|
|
334
|
+
current = current.parent as? View
|
|
335
|
+
}
|
|
336
|
+
return view.rootView
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fun locationOf(view: View): IntArray = IntArray(2).also { view.getLocationOnScreen(it) }
|
|
340
|
+
|
|
341
|
+
// pooled: the bitmap comes from kept and goes back there (the activity window). A dialog window's bitmap doesn't.
|
|
342
|
+
fun capture(window: Window, width: Int, height: Int, handler: Handler, pooled: Boolean, onResult: (Bitmap?) -> Unit) {
|
|
343
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || width <= 0 || height <= 0) {
|
|
344
|
+
onResult(null)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
val bitmap = (if (pooled) kept.take(width, height) else null) ?: try {
|
|
348
|
+
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
349
|
+
} catch (_: OutOfMemoryError) {
|
|
350
|
+
onResult(null)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
copyInto(window, bitmap, handler, { if (pooled) kept.give(bitmap) else bitmap.recycle() }, onResult)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Copies window into bitmap. On failure calls drop, then onResult(null).
|
|
357
|
+
private fun copyInto(window: Window, bitmap: Bitmap, handler: Handler, drop: () -> Unit, onResult: (Bitmap?) -> Unit) {
|
|
358
|
+
val listener = PixelCopy.OnPixelCopyFinishedListener { result ->
|
|
359
|
+
if (result == PixelCopy.SUCCESS) {
|
|
360
|
+
onResult(bitmap)
|
|
361
|
+
} else {
|
|
362
|
+
drop()
|
|
363
|
+
onResult(null)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
367
|
+
try {
|
|
368
|
+
PixelCopy.request(window, bitmap, listener, handler)
|
|
369
|
+
} catch (_: RuntimeException) {
|
|
370
|
+
drop()
|
|
371
|
+
onResult(null)
|
|
372
|
+
}
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
// API 26-33: PixelCopy.request blocks the calling thread until the RenderThread has read the window back (40-47
|
|
376
|
+
// ms for 1080x2400 on a Mi 11 Lite). So the request runs on copyHandler and the main thread stays free. The
|
|
377
|
+
// listener and all failures still run on handler. The listener is posted to the front of handler's queue: the
|
|
378
|
+
// readback often finishes right as a vsync arrives, and this way the frame queued for that vsync runs after the
|
|
379
|
+
// listener, with the overlay, instead of before it (one frame earlier on a Mi 11 Lite). The result can also
|
|
380
|
+
// jump ahead of a queued cancel, prepare or onPause. Harmless: JS drops a result whose switch is no longer the
|
|
381
|
+
// latest, and the next removeOverlay cleans up its overlay.
|
|
382
|
+
val toFront = PixelCopy.OnPixelCopyFinishedListener { result ->
|
|
383
|
+
handler.postAtFrontOfQueue { listener.onPixelCopyFinished(result) }
|
|
384
|
+
}
|
|
385
|
+
// Off main, PixelCopy.request(window) reads peekDecorView().getViewRootImpl(), its mSurface (final field, Surface
|
|
386
|
+
// locks internally), mWindowAttributes.surfaceInsets and mWidth/mHeight. A stale read means either a failed copy
|
|
387
|
+
// (invalid surface throws, caught below, or the listener gets an error) or a size mismatch (Motion.sameSize in
|
|
388
|
+
// start drops the overlay), and both go down the existing failure path. postAtFrontOfQueue jumps the main queue
|
|
389
|
+
// with one short message per copy (two for a modal). The last one attaches the overlay and resolves.
|
|
390
|
+
copyHandler.post {
|
|
391
|
+
try {
|
|
392
|
+
PixelCopy.request(window, bitmap, toFront, copyHandler)
|
|
393
|
+
} catch (_: RuntimeException) {
|
|
394
|
+
handler.post {
|
|
395
|
+
drop()
|
|
396
|
+
onResult(null)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Off main. Starts the copy thread so the first switch doesn't have to.
|
|
403
|
+
fun warmUp() {
|
|
404
|
+
copyHandler
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Main thread, after warmUp. Prepares the path the activity window's first switch takes: on API 29+, a GPU
|
|
408
|
+
// capture of nothing on the copy thread with no bitmap kept. Otherwise (a window that takes PixelCopy) keepOne.
|
|
409
|
+
fun prepareFirst(activity: Activity?, handler: Handler) {
|
|
410
|
+
if (warmUpOf(activity) == WarmUp.KEPT_BITMAP) {
|
|
411
|
+
keepOne(activity, handler)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
preCreates.started()
|
|
415
|
+
copyHandler.post {
|
|
416
|
+
try {
|
|
417
|
+
GpuSnapshot.preCreate()
|
|
418
|
+
} catch (thrown: Exception) {
|
|
419
|
+
// Then the first switch pays for it.
|
|
420
|
+
Log.w(TAG, "Cannot prepare the GPU capture", thrown)
|
|
421
|
+
} catch (thrown: OutOfMemoryError) {
|
|
422
|
+
Log.w(TAG, "Cannot prepare the GPU capture", thrown)
|
|
423
|
+
} finally {
|
|
424
|
+
handler.post { preCreates.finished() }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// The activity window's inputs to the capture path, read the same way captureWindows reads them. Without a
|
|
430
|
+
// window, the GPU capture is the likely path from API 29.
|
|
431
|
+
private fun warmUpOf(activity: Activity?): WarmUp {
|
|
432
|
+
val api = Build.VERSION.SDK_INT
|
|
433
|
+
if (api < Build.VERSION_CODES.Q) return WarmUp.KEPT_BITMAP
|
|
434
|
+
val window = activity?.window
|
|
435
|
+
val decor = window?.peekDecorView()
|
|
436
|
+
if (activity == null || window == null || decor == null) return CapturePlan.warmUp(api, true, true, true, gpuOff)
|
|
437
|
+
return CapturePlan.warmUp(
|
|
438
|
+
api,
|
|
439
|
+
window.colorMode == ActivityInfo.COLOR_MODE_DEFAULT,
|
|
440
|
+
decor.isHardwareAccelerated,
|
|
441
|
+
GpuSnapshot.lightOf(activity, decor) != null,
|
|
442
|
+
gpuOff,
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private val preCreates = PreCreates()
|
|
447
|
+
|
|
448
|
+
// Main thread. If kept has room, allocates a bitmap of the activity window's size on the copy thread, copies the
|
|
449
|
+
// window into it once and keeps it. The first copy into a bitmap is the slow one (roughly twice as long as later
|
|
450
|
+
// ones into the same bitmap on a Pixel 8 Pro), so the first switch gets a warm one.
|
|
451
|
+
fun keepOne(activity: Activity?, handler: Handler) {
|
|
452
|
+
val window = activity?.window ?: return
|
|
453
|
+
val decor = window.decorView
|
|
454
|
+
val width = decor.width
|
|
455
|
+
val height = decor.height
|
|
456
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || keeping || width <= 0 || height <= 0 ||
|
|
457
|
+
!kept.hasRoom(width, height)
|
|
458
|
+
) {
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
keeping = true
|
|
462
|
+
copyHandler.post {
|
|
463
|
+
val bitmap = try {
|
|
464
|
+
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
465
|
+
} catch (_: OutOfMemoryError) {
|
|
466
|
+
null
|
|
467
|
+
}
|
|
468
|
+
handler.post {
|
|
469
|
+
if (bitmap == null) {
|
|
470
|
+
keeping = false
|
|
471
|
+
return@post
|
|
472
|
+
}
|
|
473
|
+
val keep = {
|
|
474
|
+
keeping = false
|
|
475
|
+
kept.fill(bitmap)
|
|
476
|
+
}
|
|
477
|
+
copyInto(window, bitmap, handler, { keep() }) { if (it != null) keep() }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// A keepOne is in flight. Main thread only.
|
|
483
|
+
private var keeping = false
|
|
484
|
+
|
|
485
|
+
// Never quit the copy thread. Without it PixelCopy never calls back, prepare never resolves and the switch never
|
|
486
|
+
// applies the theme. The GPU capture takes its images on it too.
|
|
487
|
+
private val copyHandler: Handler by lazy {
|
|
488
|
+
val thread = HandlerThread("ThemeRevealCopy", Process.THREAD_PRIORITY_DISPLAY)
|
|
489
|
+
thread.start()
|
|
490
|
+
Handler(thread.looper)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Both copies run concurrently. onResult fires once, after the second one finishes.
|
|
494
|
+
private fun capturePair(
|
|
495
|
+
first: Window,
|
|
496
|
+
firstW: Int,
|
|
497
|
+
firstH: Int,
|
|
498
|
+
second: Window,
|
|
499
|
+
secondW: Int,
|
|
500
|
+
secondH: Int,
|
|
501
|
+
handler: Handler,
|
|
502
|
+
onResult: (Bitmap?, Bitmap?) -> Unit,
|
|
503
|
+
) {
|
|
504
|
+
val results = arrayOfNulls<Bitmap>(2)
|
|
505
|
+
var pending = 2
|
|
506
|
+
val done = { index: Int, bitmap: Bitmap? ->
|
|
507
|
+
results[index] = bitmap
|
|
508
|
+
pending--
|
|
509
|
+
if (pending == 0) onResult(results[0], results[1])
|
|
510
|
+
}
|
|
511
|
+
capture(first, firstW, firstH, handler, true) { done(0, it) }
|
|
512
|
+
capture(second, secondW, secondH, handler, false) { done(1, it) }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// PixelCopy doesn't capture the dim behind a dialog: the compositor draws it.
|
|
516
|
+
private fun compose(back: Bitmap, front: Bitmap, frame: Frame, frontX: Int, frontY: Int, dimAlpha: Int) {
|
|
517
|
+
val canvas = Canvas(back)
|
|
518
|
+
if (dimAlpha > 0) canvas.drawColor(Color.argb(dimAlpha, 0, 0, 0))
|
|
519
|
+
if (frame.isEmpty) return
|
|
520
|
+
val source = Rect(frame.left - frontX, frame.top - frontY, frame.right - frontX, frame.bottom - frontY)
|
|
521
|
+
canvas.drawBitmap(front, source, Rect(frame.left, frame.top, frame.right, frame.bottom), null)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private fun findModalWindow(view: View, match: (Window) -> Boolean): Window? {
|
|
525
|
+
if (view is ReactModalHostView) {
|
|
526
|
+
val window = view.dialog?.takeIf { it.isShowing }?.window
|
|
527
|
+
if (window != null && match(window)) return window
|
|
528
|
+
}
|
|
529
|
+
if (view !is ViewGroup) return null
|
|
530
|
+
for (index in 0 until view.childCount) {
|
|
531
|
+
val found = findModalWindow(view.getChildAt(index) ?: continue, match)
|
|
532
|
+
if (found != null) return found
|
|
533
|
+
}
|
|
534
|
+
return null
|
|
535
|
+
}
|
|
536
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CapturePath, Transition } from './types';
|
|
2
|
+
declare const _default: {
|
|
3
|
+
prepare: (viewTag?: number) => Promise<CapturePath | null>;
|
|
4
|
+
start: (transition: Transition) => Promise<void>;
|
|
5
|
+
cancel: () => Promise<void>;
|
|
6
|
+
warmUp: () => Promise<void>;
|
|
7
|
+
capture: (viewTag?: number) => Promise<boolean>;
|
|
8
|
+
dropCapture: () => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export default _default;
|
|
11
|
+
//# sourceMappingURL=ThemeRevealModule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeRevealModule.d.ts","sourceRoot":"","sources":["../src/ThemeRevealModule.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;;wBAehC,MAAM,KAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;wBACpC,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC;kBAClC,OAAO,CAAC,IAAI,CAAC;kBACb,OAAO,CAAC,IAAI,CAAC;wBACL,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC;uBAC5B,OAAO,CAAC,IAAI,CAAC;;AANhC,wBAOC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo';
|
|
2
|
+
const native = requireNativeModule('ThemeReveal');
|
|
3
|
+
export default {
|
|
4
|
+
prepare: (viewTag) => native.prepare(viewTag),
|
|
5
|
+
start: (transition) => native.start(transition),
|
|
6
|
+
cancel: () => native.cancel(),
|
|
7
|
+
warmUp: () => native.warmUp(),
|
|
8
|
+
capture: (viewTag) => native.capture(viewTag),
|
|
9
|
+
dropCapture: () => native.dropCapture(),
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=ThemeRevealModule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeRevealModule.js","sourceRoot":"","sources":["../src/ThemeRevealModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAA;AAc1C,MAAM,MAAM,GAAG,mBAAmB,CAAoB,aAAa,CAAC,CAAA;AAEpE,eAAe;IACb,OAAO,EAAE,CAAC,OAAgB,EAA+B,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;IACnF,KAAK,EAAE,CAAC,UAAsB,EAAiB,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;IAC1E,MAAM,EAAE,GAAkB,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;IAC5C,MAAM,EAAE,GAAkB,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,CAAC,OAAgB,EAAoB,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;IACxE,WAAW,EAAE,GAAkB,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE;CACvD,CAAA","sourcesContent":["import { requireNativeModule } from 'expo'\n\nimport type { CapturePath, Transition } from './types'\n\ntype NativeThemeReveal = {\n // The capture path, or null when no overlay shows.\n prepare(viewTag: number | undefined): Promise<CapturePath | null>\n start(transition: Transition): Promise<void>\n cancel(): Promise<void>\n warmUp(): Promise<void>\n capture(viewTag: number | undefined): Promise<boolean>\n dropCapture(): Promise<void>\n}\n\nconst native = requireNativeModule<NativeThemeReveal>('ThemeReveal')\n\nexport default {\n prepare: (viewTag?: number): Promise<CapturePath | null> => native.prepare(viewTag),\n start: (transition: Transition): Promise<void> => native.start(transition),\n cancel: (): Promise<void> => native.cancel(),\n warmUp: (): Promise<void> => native.warmUp(),\n capture: (viewTag?: number): Promise<boolean> => native.capture(viewTag),\n dropCapture: (): Promise<void> => native.dropCapture(),\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { GestureResponderEvent } from 'react-native';
|
|
2
|
+
import type { CapturePath, Point, Shape, TransitionType, WipeDirection } from './types';
|
|
3
|
+
export type SettledInfo = {
|
|
4
|
+
transition: TransitionType | null;
|
|
5
|
+
capture: CapturePath | null;
|
|
6
|
+
prepareMs: number;
|
|
7
|
+
totalMs: number;
|
|
8
|
+
};
|
|
9
|
+
export type ThemeRevealOptions<T> = {
|
|
10
|
+
getTheme: () => T;
|
|
11
|
+
setTheme: (theme: T) => void;
|
|
12
|
+
shape?: Shape;
|
|
13
|
+
durations?: Partial<Record<TransitionType, number>>;
|
|
14
|
+
transition?: TransitionType;
|
|
15
|
+
wipeDirection?: WipeDirection;
|
|
16
|
+
onError?: (error: unknown) => void;
|
|
17
|
+
onSettled?: (info: SettledInfo) => void;
|
|
18
|
+
warmUp?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type SwitchOptions = {
|
|
21
|
+
transition?: TransitionType;
|
|
22
|
+
shape?: Shape;
|
|
23
|
+
wipeDirection?: WipeDirection;
|
|
24
|
+
};
|
|
25
|
+
export type PressCapture = {
|
|
26
|
+
onPressIn(event: GestureResponderEvent): void;
|
|
27
|
+
onPressOut(event?: GestureResponderEvent): void;
|
|
28
|
+
};
|
|
29
|
+
export type ThemeReveal<T> = {
|
|
30
|
+
setOrigin(point: Point, viewTag?: number): void;
|
|
31
|
+
capture(point: Point, viewTag?: number): void;
|
|
32
|
+
dropCapture(): void;
|
|
33
|
+
pressCapture: PressCapture;
|
|
34
|
+
switchTo(theme: T, options?: SwitchOptions): Promise<void>;
|
|
35
|
+
useStart(): void;
|
|
36
|
+
useHeldTheme(): T | null;
|
|
37
|
+
};
|
|
38
|
+
export declare function createThemeReveal<T>(options: ThemeRevealOptions<T>): ThemeReveal<T>;
|
|
39
|
+
//# sourceMappingURL=createThemeReveal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createThemeReveal.d.ts","sourceRoot":"","sources":["../src/createThemeReveal.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAMzD,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAc,cAAc,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEnG,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;IAEjC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI;IAClC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;IAC5B,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAA;IAGvC,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B,CAAA;AAID,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,CAAC,KAAK,EAAE,qBAAqB,GAAG,IAAI,CAAA;IAC7C,UAAU,CAAC,KAAK,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAA;CAChD,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/C,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7C,WAAW,IAAI,IAAI,CAAA;IACnB,YAAY,EAAE,YAAY,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1D,QAAQ,IAAI,IAAI,CAAA;IAChB,YAAY,IAAI,CAAC,GAAG,IAAI,CAAA;CACzB,CAAA;AAmCD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CA8KnF"}
|