-
-
Notifications
You must be signed in to change notification settings - Fork 468
Expand file tree
/
Copy pathNodes.kt
More file actions
204 lines (176 loc) · 7.73 KB
/
Nodes.kt
File metadata and controls
204 lines (176 loc) · 7.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") // to access internal vals and classes
package io.sentry.android.replay.util
import android.graphics.Rect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.text.TextLayoutResult
import kotlin.math.roundToInt
internal class ComposeTextLayout(internal val layout: TextLayoutResult) : TextLayout {
override val lineCount: Int
get() = layout.lineCount
override val dominantTextColor: Int?
get() = null
/**
* The paragraph may be laid out with a wider width (constraint maxWidth) than the actual node
* (layout result size). When that happens, getLineLeft/getLineRight return positions in the
* paragraph coordinate system, which don't match the node's bounds. In that case, text alignment
* has no visible effect, so we fall back to using line width starting from x=0.
*/
private val paragraphWidthExceedsNode: Boolean
get() = layout.multiParagraph.width > layout.size.width
override fun getLineLeft(line: Int): Float {
if (paragraphWidthExceedsNode) {
return 0f
}
return layout.getLineLeft(line)
}
override fun getLineRight(line: Int): Float {
if (paragraphWidthExceedsNode) {
return layout.multiParagraph.getLineWidth(line)
}
return layout.getLineRight(line)
}
override fun getLineTop(line: Int): Int = layout.getLineTop(line).roundToInt()
override fun getLineBottom(line: Int): Int = layout.getLineBottom(line).roundToInt()
}
// TODO: probably most of the below we can do via bytecode instrumentation and speed up at runtime
/**
* This method is necessary to mask images in Compose.
*
* We heuristically look up for classes that have a [Painter] modifier, usually they all have a
* `Painter` string in their name, e.g. PainterElement, PainterModifierNodeElement or
* ContentPainterModifier for Coil.
*
* That's not going to cover all cases, but probably 90%.
*
* We also add special proguard rules to keep the `Painter` class names and their `painter` member.
*/
internal fun LayoutNode.findPainter(): Painter? {
val modifierInfos = getModifierInfo()
for (index in modifierInfos.indices) {
val modifier = modifierInfos[index].modifier
if (modifier::class.java.name.contains("Painter")) {
return try {
modifier::class.java.getDeclaredField("painter").apply { isAccessible = true }.get(modifier)
as? Painter
} catch (e: Throwable) {
null
}
}
}
return null
}
/**
* We heuristically check the known classes that are coming from local assets usually:
* [androidx.compose.ui.graphics.vector.VectorPainter]
* [androidx.compose.ui.graphics.painter.ColorPainter]
* [androidx.compose.ui.graphics.painter.BrushPainter]
*
* In theory, [androidx.compose.ui.graphics.painter.BitmapPainter] can also come from local assets,
* but it can as well come from a network resource, so we preemptively mask it.
*/
internal fun Painter.isMaskable(): Boolean {
val className = this::class.java.name
return !className.contains("Vector") &&
!className.contains("Color") &&
!className.contains("Brush")
}
/**
* This method is necessary to mask text in Compose.
*
* We heuristically look up for classes that have a [Text] modifier, usually they all have a `Text`
* string in their name, e.g. TextStringSimpleElement or TextAnnotatedStringElement. We then get the
* color from the modifier, to be able to mask it with the correct color.
*
* We also add special proguard rules to keep the `Text` class names and their `color` member.
*/
internal fun LayoutNode.findTextColor(): Color? {
val modifierInfos = getModifierInfo()
for (index in modifierInfos.indices) {
val modifier = modifierInfos[index].modifier
val modifierClassName = modifier::class.java.name
if (modifierClassName.contains("Text")) {
return try {
(modifier::class.java.getDeclaredField("color").apply { isAccessible = true }.get(modifier)
as? ColorProducer)
?.invoke()
} catch (e: Throwable) {
null
}
}
}
return null
}
/**
* Returns the smaller of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.minOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private inline fun fastMinOf(a: Float, b: Float, c: Float, d: Float): Float =
minOf(a, minOf(b, minOf(c, d)))
/**
* Returns the largest of the given values. If any value is NaN, returns NaN. Preferred over
* `kotlin.comparisons.maxOf()` for 4 arguments as it avoids allocating an array because of the
* varargs.
*/
private inline fun fastMaxOf(a: Float, b: Float, c: Float, d: Float): Float =
maxOf(a, maxOf(b, maxOf(c, d)))
/**
* Returns this float value clamped in the inclusive range defined by [minimumValue] and
* [maximumValue]. Unlike [Float.coerceIn], the range is not validated: the caller must ensure that
* [minimumValue] is less than [maximumValue].
*/
private inline fun Float.fastCoerceIn(minimumValue: Float, maximumValue: Float) =
this.fastCoerceAtLeast(minimumValue).fastCoerceAtMost(maximumValue)
/** Ensures that this value is not less than the specified [minimumValue]. */
private inline fun Float.fastCoerceAtLeast(minimumValue: Float): Float =
if (this < minimumValue) minimumValue else this
/** Ensures that this value is not greater than the specified [maximumValue]. */
private inline fun Float.fastCoerceAtMost(maximumValue: Float): Float =
if (this > maximumValue) maximumValue else this
/**
* A faster copy of
* https://github.com/androidx/androidx/blob/fc7df0dd68466ac3bb16b1c79b7a73dd0bfdd4c1/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt#L187
*
* Since we traverse the tree from the root, we don't need to find it again from the leaf node and
* just pass it as an argument.
*
* @return boundaries of this layout relative to the window's origin.
*/
internal fun LayoutCoordinates.boundsInWindow(rootCoordinates: LayoutCoordinates?): Rect {
val root = rootCoordinates ?: findRootCoordinates()
val rootWidth = root.size.width.toFloat()
val rootHeight = root.size.height.toFloat()
// pass clipBounds explicitly to avoid the `localBoundingBoxOf$default` bridge that AGP 8.13's D8
// desugars inconsistently on minSdk < 24
val bounds = root.localBoundingBoxOf(this, true)
val boundsLeft = bounds.left.fastCoerceIn(0f, rootWidth)
val boundsTop = bounds.top.fastCoerceIn(0f, rootHeight)
val boundsRight = bounds.right.fastCoerceIn(0f, rootWidth)
val boundsBottom = bounds.bottom.fastCoerceIn(0f, rootHeight)
if (boundsLeft == boundsRight || boundsTop == boundsBottom) {
return Rect()
}
val topLeft = root.localToWindow(Offset(boundsLeft, boundsTop))
val topRight = root.localToWindow(Offset(boundsRight, boundsTop))
val bottomRight = root.localToWindow(Offset(boundsRight, boundsBottom))
val bottomLeft = root.localToWindow(Offset(boundsLeft, boundsBottom))
val topLeftX = topLeft.x
val topRightX = topRight.x
val bottomLeftX = bottomLeft.x
val bottomRightX = bottomRight.x
val left = fastMinOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
val right = fastMaxOf(topLeftX, topRightX, bottomLeftX, bottomRightX)
val topLeftY = topLeft.y
val topRightY = topRight.y
val bottomLeftY = bottomLeft.y
val bottomRightY = bottomRight.y
val top = fastMinOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
val bottom = fastMaxOf(topLeftY, topRightY, bottomLeftY, bottomRightY)
return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
}