Guard RTL overhang handling for fixed-line text (#58685)

Summary:
Pull Request resolved: https://github.com/react/react-native/pull/58685

Add Android regression coverage for exactly constrained RTL text that reserves start-side ink overhang while enforcing a maximum line count and ellipsizing overflow. Also cover the mixed-direction exclusion, where a shared right-side reservation must not be applied.

Changelog: [Internal]

___

Differential Revision: D121670747

fbshipit-source-id: 307461e4ef24d0dad90e609aa8dd7845efc70fa7
This commit is contained in:
Peter Abbondanzo
2026-09-25 15:46:27 -07:00
committed by meta-codesync[bot]
parent 447ccd1ddb
commit 5c87ca977a
@@ -7,6 +7,7 @@
package com.facebook.react.views.text
import android.annotation.SuppressLint
import android.graphics.RectF
import android.text.BoringLayout
import android.text.Layout
@@ -24,6 +25,7 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@SuppressLint("NewApi")
class TextLayoutManagerStartOverhangTest {
@Test
@@ -56,6 +58,64 @@ class TextLayoutManagerStartOverhangTest {
assertThat(TextLayoutManager.getRtlRightOverhang(layout)).isEqualTo(8)
}
@Test
@Config(sdk = [35])
fun `RTL overhang reservation preserves max lines and ellipsis`() {
val initialLayout = mock<Layout>()
whenever(initialLayout.lineCount).thenReturn(2)
whenever(initialLayout.width).thenReturn(LAYOUT_WIDTH.toInt())
whenever(initialLayout.getParagraphDirection(any())).thenReturn(Layout.DIR_RIGHT_TO_LEFT)
whenever(initialLayout.computeDrawingBoundingBox())
.thenReturn(RectF(10f, 0f, LAYOUT_WIDTH + 7.1f, 40f))
var rebuiltWidth = 0
val layout =
TextLayoutManager.adjustLayoutForRtlRightOverhang(
initialLayout,
LAYOUT_WIDTH.toInt(),
) { adjustedWidth ->
rebuiltWidth = adjustedWidth
createLayout(
YogaMeasureMode.EXACTLY,
text =
SpannableString(
listOf(
"\u200Ffirst paragraph",
"\u200Fsecond paragraph",
"\u200Fthird paragraph",
)
.joinToString("\n"),
),
layoutWidth = adjustedWidth.toFloat(),
ellipsizeMode = TextUtils.TruncateAt.END,
maxNumberOfLines = 2,
)
}
assertThat(rebuiltWidth).isEqualTo(192)
assertThat(layout.width).isEqualTo(rebuiltWidth)
assertThat(layout.lineCount).isEqualTo(2)
assertThat(layout.getEllipsisCount(layout.lineCount - 1)).isGreaterThan(0)
}
@Test
@Config(sdk = [35])
fun `mixed direction text does not reserve RTL right overhang`() {
val initialLayout = mock<Layout>()
whenever(initialLayout.lineCount).thenReturn(2)
whenever(initialLayout.width).thenReturn(200)
whenever(initialLayout.getParagraphDirection(0)).thenReturn(Layout.DIR_RIGHT_TO_LEFT)
whenever(initialLayout.getParagraphDirection(1)).thenReturn(Layout.DIR_LEFT_TO_RIGHT)
whenever(initialLayout.computeDrawingBoundingBox()).thenReturn(RectF(10f, 0f, 208f, 40f))
val layout =
TextLayoutManager.adjustLayoutForRtlRightOverhang(initialLayout, 200) {
throw AssertionError("Mixed-direction text must not be rebuilt")
}
assertThat(layout).isSameAs(initialLayout)
}
@Test
@Config(sdk = [34])
fun `EXACTLY mode remains supported before Android 15`() {
@@ -64,9 +124,15 @@ class TextLayoutManagerStartOverhangTest {
assertThat(layout.width).isEqualTo(LAYOUT_WIDTH.toInt())
}
private fun createLayout(widthMode: YogaMeasureMode): Layout {
val text = SpannableString("\u0622\u064a\u0629 \u0627\u0644\u0643\u0631\u0633\u064a")
val paint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 26f }
private fun createLayout(
widthMode: YogaMeasureMode,
text: SpannableString =
SpannableString("\u0622\u064a\u0629 \u0627\u0644\u0643\u0631\u0633\u064a"),
layoutWidth: Float = LAYOUT_WIDTH,
ellipsizeMode: TextUtils.TruncateAt? = null,
maxNumberOfLines: Int = 2,
paint: TextPaint = TextPaint(TextPaint.ANTI_ALIAS_FLAG).apply { textSize = 26f },
): Layout {
val method =
TextLayoutManager::class
.java
@@ -91,15 +157,15 @@ class TextLayoutManagerStartOverhangTest {
TextLayoutManager,
text,
null,
LAYOUT_WIDTH,
layoutWidth,
widthMode,
/* includeFontPadding = */ false,
/* textBreakStrategy = */ Layout.BREAK_STRATEGY_HIGH_QUALITY,
/* hyphenationFrequency = */ Layout.HYPHENATION_FREQUENCY_NONE,
Layout.Alignment.ALIGN_NORMAL,
/* justificationMode = */ 0,
/* ellipsizeMode = */ null,
/* maxNumberOfLines = */ 2,
/* ellipsizeMode = */ ellipsizeMode,
/* maxNumberOfLines = */ maxNumberOfLines,
paint,
) as Layout
}