Skip to content

Commit 0492557

Browse files
committed
Work around erroneous paragraph return values
1 parent 1daf0c5 commit 0492557

File tree

6 files changed

+41
-72
lines changed

6 files changed

+41
-72
lines changed
-65 Bytes
Loading
-62 Bytes
Loading

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Nodes.kt

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,39 @@ import androidx.compose.ui.node.LayoutNode
1313
import androidx.compose.ui.text.TextLayoutResult
1414
import kotlin.math.roundToInt
1515

16-
internal class ComposeTextLayout(
17-
internal val layout: TextLayoutResult,
18-
private val hasFillModifier: Boolean,
19-
) : TextLayout {
16+
internal class ComposeTextLayout(internal val layout: TextLayoutResult) : TextLayout {
2017
override val lineCount: Int
2118
get() = layout.lineCount
2219

2320
override val dominantTextColor: Int?
2421
get() = null
2522

26-
override fun getPrimaryHorizontal(line: Int, offset: Int): Float {
27-
val horizontalPos = layout.getHorizontalPosition(offset, usePrimaryDirection = true)
28-
// when there's no `fill` modifier on a Text composable, compose still thinks that there's
29-
// one and wrongly calculates horizontal position relative to node's start, not text's start
30-
// for some reason. This is only the case for single-line text (multiline works fien).
31-
// So we subtract line's left to get the correct position
32-
return if (!hasFillModifier && lineCount == 1) {
33-
horizontalPos - layout.getLineLeft(line)
34-
} else {
35-
horizontalPos
23+
/**
24+
* The paragraph may be laid out with a wider width (constraint maxWidth) than the actual node
25+
* (layout result size). When that happens, getLineLeft/getLineRight return positions in the
26+
* paragraph coordinate system, which don't match the node's bounds. In that case, text alignment
27+
* has no visible effect, so we fall back to using line width starting from x=0.
28+
*/
29+
private val paragraphWidthExceedsNode: Boolean
30+
get() = layout.multiParagraph.width > layout.size.width
31+
32+
override fun getLineLeft(line: Int): Float {
33+
if (paragraphWidthExceedsNode) {
34+
return 0f
3635
}
36+
return layout.getLineLeft(line)
3737
}
3838

39-
override fun getEllipsisCount(line: Int): Int = if (layout.isLineEllipsized(line)) 1 else 0
40-
41-
override fun getLineVisibleEnd(line: Int): Int = layout.getLineEnd(line, visibleEnd = true)
39+
override fun getLineRight(line: Int): Float {
40+
if (paragraphWidthExceedsNode) {
41+
return layout.multiParagraph.getLineWidth(line)
42+
}
43+
return layout.getLineRight(line)
44+
}
4245

4346
override fun getLineTop(line: Int): Int = layout.getLineTop(line).roundToInt()
4447

4548
override fun getLineBottom(line: Int): Int = layout.getLineBottom(line).roundToInt()
46-
47-
override fun getLineStart(line: Int): Int = layout.getLineStart(line)
4849
}
4950

5051
// TODO: probably most of the below we can do via bytecode instrumentation and speed up at runtime
@@ -92,46 +93,31 @@ internal fun Painter.isMaskable(): Boolean {
9293
!className.contains("Brush")
9394
}
9495

95-
internal data class TextAttributes(val color: Color?, val hasFillModifier: Boolean)
96-
9796
/**
9897
* This method is necessary to mask text in Compose.
9998
*
10099
* We heuristically look up for classes that have a [Text] modifier, usually they all have a `Text`
101100
* string in their name, e.g. TextStringSimpleElement or TextAnnotatedStringElement. We then get the
102101
* color from the modifier, to be able to mask it with the correct color.
103102
*
104-
* We also look up for classes that have a [Fill] modifier, usually they all have a `Fill` string in
105-
* their name, e.g. FillElement. This is necessary to workaround a Compose bug where single-line
106-
* text composable without a `fill` modifier still thinks that there's one and wrongly calculates
107-
* horizontal position.
108-
*
109103
* We also add special proguard rules to keep the `Text` class names and their `color` member.
110104
*/
111-
internal fun LayoutNode.findTextAttributes(): TextAttributes {
105+
internal fun LayoutNode.findTextColor(): Color? {
112106
val modifierInfos = getModifierInfo()
113-
var color: Color? = null
114-
var hasFillModifier = false
115107
for (index in modifierInfos.indices) {
116108
val modifier = modifierInfos[index].modifier
117109
val modifierClassName = modifier::class.java.name
118110
if (modifierClassName.contains("Text")) {
119-
color =
120-
try {
121-
(modifier::class
122-
.java
123-
.getDeclaredField("color")
124-
.apply { isAccessible = true }
125-
.get(modifier) as? ColorProducer)
126-
?.invoke()
127-
} catch (e: Throwable) {
128-
null
129-
}
130-
} else if (modifierClassName.contains("Fill")) {
131-
hasFillModifier = true
111+
return try {
112+
(modifier::class.java.getDeclaredField("color").apply { isAccessible = true }.get(modifier)
113+
as? ColorProducer)
114+
?.invoke()
115+
} catch (e: Throwable) {
116+
null
117+
}
132118
}
133119
}
134-
return TextAttributes(color, hasFillModifier)
120+
return null
135121
}
136122

137123
/**

sentry-android-replay/src/main/java/io/sentry/android/replay/util/TextLayout.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,11 @@ internal interface TextLayout {
1313
*/
1414
val dominantTextColor: Int?
1515

16-
fun getPrimaryHorizontal(line: Int, offset: Int): Float
16+
fun getLineLeft(line: Int): Float
1717

18-
fun getEllipsisCount(line: Int): Int
19-
20-
fun getLineVisibleEnd(line: Int): Int
18+
fun getLineRight(line: Int): Float
2119

2220
fun getLineTop(line: Int): Int
2321

2422
fun getLineBottom(line: Int): Int
25-
26-
fun getLineStart(line: Int): Int
2723
}

sentry-android-replay/src/main/java/io/sentry/android/replay/util/Views.kt

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,21 +128,14 @@ internal fun TextLayout?.getVisibleRects(
128128

129129
val rects = mutableListOf<Rect>()
130130
for (i in 0 until lineCount) {
131-
val lineStart = getPrimaryHorizontal(i, getLineStart(i)).toInt()
132-
val ellipsisCount = getEllipsisCount(i)
133-
val lineVisibleEnd = getLineVisibleEnd(i)
134-
var lineEnd =
135-
getPrimaryHorizontal(i, lineVisibleEnd - ellipsisCount + if (ellipsisCount > 0) 1 else 0)
136-
.toInt()
137-
if (lineEnd == 0 && lineVisibleEnd > 0) {
138-
// looks like the case for when emojis are present in text
139-
lineEnd = getPrimaryHorizontal(i, lineVisibleEnd - 1).toInt() + 1
140-
}
131+
val lineLeft = getLineLeft(i).toInt()
132+
val lineRight = getLineRight(i).toInt()
141133
val lineTop = getLineTop(i)
142134
val lineBottom = getLineBottom(i)
143135
val rect = Rect()
144-
rect.left = globalRect.left + paddingLeft + lineStart
145-
rect.right = rect.left + (lineEnd - lineStart)
136+
137+
rect.left = globalRect.left + paddingLeft + lineLeft
138+
rect.right = globalRect.left + paddingLeft + lineRight
146139
rect.top = globalRect.top + paddingTop + lineTop
147140
rect.bottom = rect.top + (lineBottom - lineTop)
148141

@@ -197,18 +190,13 @@ internal class AndroidTextLayout(private val layout: Layout) : TextLayout {
197190
return dominantColor?.toOpaque()
198191
}
199192

200-
override fun getPrimaryHorizontal(line: Int, offset: Int): Float =
201-
layout.getPrimaryHorizontal(offset)
193+
override fun getLineLeft(line: Int): Float = layout.getLineLeft(line)
202194

203-
override fun getEllipsisCount(line: Int): Int = layout.getEllipsisCount(line)
204-
205-
override fun getLineVisibleEnd(line: Int): Int = layout.getLineVisibleEnd(line)
195+
override fun getLineRight(line: Int): Float = layout.getLineRight(line)
206196

207197
override fun getLineTop(line: Int): Int = layout.getLineTop(line)
208198

209199
override fun getLineBottom(line: Int): Int = layout.getLineBottom(line)
210-
211-
override fun getLineStart(line: Int): Int = layout.getLineStart(line)
212200
}
213201

214202
internal fun View?.addOnDrawListenerSafe(listener: ViewTreeObserver.OnDrawListener) {

sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import io.sentry.android.replay.SentryReplayModifiers
2424
import io.sentry.android.replay.util.ComposeTextLayout
2525
import io.sentry.android.replay.util.boundsInWindow
2626
import io.sentry.android.replay.util.findPainter
27-
import io.sentry.android.replay.util.findTextAttributes
27+
import io.sentry.android.replay.util.findTextColor
2828
import io.sentry.android.replay.util.isMaskable
2929
import io.sentry.android.replay.util.toOpaque
3030
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode.GenericViewHierarchyNode
@@ -189,11 +189,10 @@ internal object ComposeViewHierarchyNode {
189189
?.action
190190
?.invoke(textLayoutResults)
191191

192-
val (color, hasFillModifier) = node.findTextAttributes()
193192
val textLayoutResult = textLayoutResults.firstOrNull()
194193
var textColor = textLayoutResult?.layoutInput?.style?.color
195194
if (textColor?.isUnspecified == true) {
196-
textColor = color
195+
textColor = node.findTextColor()
197196
}
198197
val isLaidOut = textLayoutResult?.layoutInput?.style?.fontSize != TextUnit.Unspecified
199198
// TODO: support editable text (currently there's a way to get @Composable's padding only
@@ -202,7 +201,7 @@ internal object ComposeViewHierarchyNode {
202201
TextViewHierarchyNode(
203202
layout =
204203
if (textLayoutResult != null && !isEditable && isLaidOut) {
205-
ComposeTextLayout(textLayoutResult, hasFillModifier)
204+
ComposeTextLayout(textLayoutResult)
206205
} else {
207206
null
208207
},

0 commit comments

Comments
 (0)