Fix TextField empty line lineHeight (2)

When a line is empty (including empty text) StaticLayout
does not apply the LineHeightSpans.

This causes the line height to be different for an empty
line compared to a non empty line. The issue becomes more
visible within TextField .

This CL stores adjustment values to report the height and the
last line metrics correctly.

For the other solution option please see aosp/2170504

Performance

There are no performance differences for cases where text
is not empty or does  not end with "\n"

The below numbers are for the cases where text is empty or ends with "\n"

+--------+-----------+-----------+------+
|        | Before    | After     | Diff |
+--------+-----------+-----------+------+
| length | ns        | ns        | ns   |
| 0      | 115,835   | 180,161   | 56%  |
| 16     | 395,679   | 484,748   | 23%  |
| 32     | 513,235   | 590,783   | 15%  |
| 64     | 807,133   | 901,896   | 12%  |
| 128    | 1,330,733 | 1,421,371 | 7%   |
| 256    | 2,338,534 | 2,464,810 | 5%   |
+--------+-----------+-----------+------+

Test: Added test
Test: ./gradlew text:text:test
Test: ./gradlew text:text:cAT
Test: ./gradlew compose:ui:ui-text:test
Test: ./gradlew compose:ui:ui-text:cAT
Test: Treehugger

Bug: 236615813

Change-Id: I57f8b6632f569ad06e547cf6ec83df1bc42845c9
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
index 6d754cc8..c05380c 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphBenchmark.kt
@@ -52,7 +52,7 @@
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "length={0} type={1} alphabet={2}")
-        fun initParameters(): List<Array<Any>> = cartesian(
+        fun initParameters(): List<Array<Any?>> = cartesian(
             arrayOf(512),
             arrayOf(TextType.PlainText),
             arrayOf(Alphabet.Latin, Alphabet.Cjk)
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
new file mode 100644
index 0000000..9ac6a7a
--- /dev/null
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/ParagraphWithLineHeightBenchmark.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.benchmark
+
+import android.content.Context
+import android.util.TypedValue
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.ParagraphIntrinsics
+import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.createFontFamilyResolver
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.sp
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.math.ceil
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class ParagraphWithLineHeightBenchmark(
+    private val textLength: Int,
+    private val addNewLine: Boolean,
+    private val applyLineHeight: Boolean,
+    private val lineHeightStyle: LineHeightStyle?
+) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(
+            name = "length={0} newLine={1} applyLineHeight={2} lineHeightStyle={3}"
+        )
+        fun initParameters(): List<Array<Any?>> = cartesian(
+            arrayOf(16),
+            // add new line
+            arrayOf(true),
+            // apply line height
+            arrayOf(false, true),
+            arrayOf(LineHeightStyle.Default)
+        )
+    }
+
+    @get:Rule
+    val benchmarkRule = BenchmarkRule()
+
+    @get:Rule
+    val textBenchmarkRule = TextBenchmarkTestRule(Alphabet.Latin)
+
+    private lateinit var instrumentationContext: Context
+
+    // Width initialized in setup().
+    private var width: Float = 0f
+    private val fontSize = textBenchmarkRule.fontSizeSp.sp
+
+    @Before
+    fun setup() {
+        instrumentationContext = InstrumentationRegistry.getInstrumentation().context
+        width = TypedValue.applyDimension(
+            TypedValue.COMPLEX_UNIT_DIP,
+            textBenchmarkRule.widthDp,
+            instrumentationContext.resources.displayMetrics
+        )
+    }
+
+    private fun text(textGenerator: RandomTextGenerator): String {
+        return textGenerator.nextParagraph(textLength) + if (addNewLine) "\n" else ""
+    }
+
+    private fun paragraph(
+        text: String,
+        width: Float
+    ): Paragraph {
+        return Paragraph(
+            paragraphIntrinsics = paragraphIntrinsics(text),
+            constraints = Constraints(maxWidth = ceil(width).toInt())
+        )
+    }
+
+    private fun paragraphIntrinsics(text: String): ParagraphIntrinsics {
+        @Suppress("DEPRECATION")
+        val style = if (applyLineHeight) {
+            TextStyle(
+                fontSize = fontSize,
+                lineHeight = fontSize * 2,
+                lineHeightStyle = lineHeightStyle,
+                platformStyle = PlatformTextStyle(includeFontPadding = false)
+            )
+        } else {
+            TextStyle(
+                fontSize = fontSize,
+                lineHeightStyle = lineHeightStyle,
+                platformStyle = PlatformTextStyle(includeFontPadding = false)
+            )
+        }
+
+        return ParagraphIntrinsics(
+            text = text,
+            density = Density(density = instrumentationContext.resources.displayMetrics.density),
+            style = style,
+            fontFamilyResolver = createFontFamilyResolver(instrumentationContext)
+        )
+    }
+
+    @Test
+    fun construct() {
+        textBenchmarkRule.generator { textGenerator ->
+            benchmarkRule.measureRepeated {
+                val text = runWithTimingDisabled {
+                    // create a new paragraph and use a smaller width to get
+                    // some line breaking in the result
+                    text(textGenerator)
+                }
+
+                paragraph(text = text, width = width)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/TextMeasurerBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/TextMeasurerBenchmark.kt
index 9a08b973..5dbd7a9 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/TextMeasurerBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/TextMeasurerBenchmark.kt
@@ -55,7 +55,7 @@
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "length={0} type={1} alphabet={2}")
-        fun initParameters(): List<Array<Any>> = cartesian(
+        fun initParameters(): List<Array<Any?>> = cartesian(
             arrayOf(8, 32, 128, 512),
             arrayOf(TextType.PlainText, TextType.StyledText),
             arrayOf(Alphabet.Latin, Alphabet.Cjk)
diff --git a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/input/EditProcessorBenchmark.kt b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/input/EditProcessorBenchmark.kt
index 1cf9583..d9549db 100644
--- a/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/input/EditProcessorBenchmark.kt
+++ b/compose/ui/ui-text/benchmark/src/androidTest/java/androidx/compose/ui/text/benchmark/input/EditProcessorBenchmark.kt
@@ -62,7 +62,7 @@
 
         @JvmStatic
         @Parameterized.Parameters(name = "initText={0}, senario={1}")
-        fun initParameters(): List<Array<Any>> = cartesian(
+        fun initParameters(): List<Array<Any?>> = cartesian(
             arrayOf(
                 InitialText(longText, "Long Text"),
                 InitialText(shortText, "Short Text")
diff --git a/compose/ui/ui-text/benchmark/src/main/java/androidx/compose/ui/text/benchmark/TextBenchmarkHelper.kt b/compose/ui/ui-text/benchmark/src/main/java/androidx/compose/ui/text/benchmark/TextBenchmarkHelper.kt
index b89c433..c83992d0 100644
--- a/compose/ui/ui-text/benchmark/src/main/java/androidx/compose/ui/text/benchmark/TextBenchmarkHelper.kt
+++ b/compose/ui/ui-text/benchmark/src/main/java/androidx/compose/ui/text/benchmark/TextBenchmarkHelper.kt
@@ -218,7 +218,7 @@
 /**
  * Creates a cartesian product of the given arrays.
  */
-fun cartesian(vararg arrays: Array<Any>): List<Array<Any>> {
+fun cartesian(vararg arrays: Array<Any?>): List<Array<Any?>> {
     return arrays.fold(listOf(arrayOf())) { acc, list ->
         // add items from the current list
         // to each list that was accumulated
diff --git a/compose/ui/ui-text/lint-baseline.xml b/compose/ui/ui-text/lint-baseline.xml
index 04f778b..987febe 100644
--- a/compose/ui/ui-text/lint-baseline.xml
+++ b/compose/ui/ui-text/lint-baseline.xml
@@ -463,6 +463,15 @@
     <issue
         id="NullAnnotationGroup"
         message="Could not find associated group for annotation androidx.compose.ui.text.android.InternalPlatformTextApi, which is used in androidx.compose.ui."
+        errorLine1="@OptIn(InternalPlatformTextApi::class)"
+        errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="../../../text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt"/>
+    </issue>
+
+    <issue
+        id="NullAnnotationGroup"
+        message="Could not find associated group for annotation androidx.compose.ui.text.android.InternalPlatformTextApi, which is used in androidx.compose.ui."
         errorLine1="@InternalPlatformTextApi"
         errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
index 6b4b6d0..1004618 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationLineHeightStyleTest.kt
@@ -840,11 +840,55 @@
         }
     }
 
+    @Test
+    fun lastLineEmptyTextHasSameLineHeightAsNonEmptyText() {
+        assertEmptyLineMetrics("", "a")
+        assertEmptyLineMetrics("\n", "a\na")
+        assertEmptyLineMetrics("a\n", "a\na")
+        assertEmptyLineMetrics("\na", "a\na")
+        assertEmptyLineMetrics("\na\na", "a\na\na")
+        assertEmptyLineMetrics("a\na\n", "a\na\na")
+    }
+
+    private fun assertEmptyLineMetrics(textWithEmptyLine: String, textWithoutEmptyLine: String) {
+        val textStyle = TextStyle(
+            lineHeightStyle = LineHeightStyle(
+                trim = Trim.None,
+                alignment = Alignment.Proportional
+            ),
+            platformStyle = @Suppress("DEPRECATION") PlatformTextStyle(
+                includeFontPadding = false
+            )
+        )
+
+        val paragraphWithEmptyLastLine = simpleParagraph(
+            text = textWithEmptyLine,
+            style = textStyle
+        ) as AndroidParagraph
+
+        val otherParagraph = simpleParagraph(
+            text = textWithoutEmptyLine,
+            style = textStyle
+        ) as AndroidParagraph
+
+        with(paragraphWithEmptyLastLine) {
+            for (line in 0 until lineCount) {
+                assertThat(height).isEqualTo(otherParagraph.height)
+                assertThat(getLineTop(line)).isEqualTo(otherParagraph.getLineTop(line))
+                assertThat(getLineBottom(line)).isEqualTo(otherParagraph.getLineBottom(line))
+                assertThat(getLineHeight(line)).isEqualTo(otherParagraph.getLineHeight(line))
+                assertThat(getLineAscent(line)).isEqualTo(otherParagraph.getLineAscent(line))
+                assertThat(getLineDescent(line)).isEqualTo(otherParagraph.getLineDescent(line))
+                assertThat(getLineBaseline(line)).isEqualTo(otherParagraph.getLineBaseline(line))
+            }
+        }
+    }
+
     private fun singleLineParagraph(
         lineHeightTrim: Trim,
         lineHeightAlignment: Alignment,
+        text: String = "AAA"
     ): AndroidParagraph {
-        val text = "AAA"
         val textStyle = TextStyle(
             lineHeightStyle = LineHeightStyle(
                 trim = lineHeightTrim,
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
index 4238314..f07c68b 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/SpannableExtensions.android.kt
@@ -126,11 +126,14 @@
 ) {
     val resolvedLineHeight = resolveLineHeightInPx(lineHeight, contextFontSize, density)
     if (!resolvedLineHeight.isNaN()) {
+        // in order to handle empty lines (including empty text) better, change endIndex so that
+        // it won't apply trimLastLineBottom rule
+        val endIndex = if (isEmpty() || last() == '\n') length + 1 else length
         setSpan(
             span = LineHeightStyleSpan(
                 lineHeight = resolvedLineHeight,
                 startIndex = 0,
-                endIndex = length,
+                endIndex = endIndex,
                 trimFirstLineTop = lineHeightStyle.trim.isTrimFirstLineTop(),
                 trimLastLineBottom = lineHeightStyle.trim.isTrimLastLineBottom(),
                 topPercentage = lineHeightStyle.alignment.topPercentage
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index 05883d7a..0275f61 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -16,10 +16,12 @@
 package androidx.compose.ui.text.android
 
 import android.graphics.Canvas
+import android.graphics.Paint.FontMetricsInt
 import android.graphics.Path
 import android.graphics.RectF
 import android.text.BoringLayout
 import android.text.Layout
+import android.text.SpannableString
 import android.text.Spanned
 import android.text.StaticLayout
 import android.text.TextDirectionHeuristic
@@ -154,8 +156,26 @@
     @VisibleForTesting
     internal val bottomPadding: Int
 
+    /**
+     * When true the wrapped layout that was created is a BoringLayout.
+     */
     private val isBoringLayout: Boolean
 
+    /**
+     * When the last line of the text is empty, ParagraphStyle's are not applied. This becomes
+     * visible during edit operations when the text field is empty or user inputs an new line
+     * character. This layout contains the text layout that would be applied if the last line
+     * was not empty.
+     */
+    private val lastLineFontMetrics: FontMetricsInt?
+
+    /**
+     * Holds the difference in line height for the lastLineFontMetrics and the wrapped text layout.
+     */
+    private val lastLineExtra: Int
+
+    val lineHeightSpans: Array<LineHeightStyleSpan>
+
     init {
         val end = charSequence.length
         val frameworkTextDir = getTextDirectionHeuristic(textDirectionHeuristic)
@@ -251,9 +271,15 @@
             }
 
         val verticalPaddings = getVerticalPaddings()
-        val lineHeightPaddings = getLineHeightPaddings()
+
+        lineHeightSpans = getLineHeightSpans()
+        val lineHeightPaddings = getLineHeightPaddings(lineHeightSpans)
         topPadding = max(verticalPaddings.first, lineHeightPaddings.first)
         bottomPadding = max(verticalPaddings.second, lineHeightPaddings.second)
+
+        val lastLineMetricsPair = getLastLineMetrics(textPaint, frameworkTextDir, lineHeightSpans)
+        lastLineFontMetrics = lastLineMetricsPair.first
+        lastLineExtra = lastLineMetricsPair.second
     }
 
     private val layoutHelper by lazy(LazyThreadSafetyMode.NONE) { LayoutHelper(layout) }
@@ -266,7 +292,7 @@
             layout.getLineBottom(lineCount - 1)
         } else {
             layout.height
-        } + topPadding + bottomPadding
+        } + topPadding + bottomPadding + lastLineExtra
 
     fun getLineLeft(lineIndex: Int): Float = layout.getLineLeft(lineIndex)
 
@@ -288,6 +314,10 @@
      * Return the vertical position of the bottom of the line in pixels.
      */
     fun getLineBottom(line: Int): Float {
+        if (line == lineCount - 1 && lastLineFontMetrics != null) {
+            return layout.getLineBottom(line - 1).toFloat() + lastLineFontMetrics.bottom
+        }
+
         return topPadding +
             layout.getLineBottom(line).toFloat() +
             if (line == lineCount - 1) bottomPadding else 0
@@ -299,12 +329,24 @@
      *
      * @param line the line index starting from 0
      */
-    fun getLineAscent(line: Int): Float = layout.getLineAscent(line).toFloat()
+    fun getLineAscent(line: Int): Float {
+        return if (line == lineCount - 1 && lastLineFontMetrics != null) {
+            lastLineFontMetrics.ascent.toFloat()
+        } else {
+            layout.getLineAscent(line).toFloat()
+        }
+    }
 
     /**
      * Return the vertical position of the baseline of the line in pixels.
      */
-    fun getLineBaseline(line: Int): Float = topPadding + layout.getLineBaseline(line).toFloat()
+    fun getLineBaseline(line: Int): Float {
+        return topPadding + if (line == lineCount - 1 && lastLineFontMetrics != null) {
+            getLineTop(line) - lastLineFontMetrics.ascent
+        } else {
+            layout.getLineBaseline(line).toFloat()
+        }
+    }
 
     /**
      * Returns the descent of the line in the line coordinates. Baseline is considered to be 0,
@@ -312,7 +354,13 @@
      *
      * @param line the line index starting from 0
      */
-    fun getLineDescent(line: Int): Float = layout.getLineDescent(line).toFloat()
+    fun getLineDescent(line: Int): Float {
+        return if (line == lineCount - 1 && lastLineFontMetrics != null) {
+            lastLineFontMetrics.descent.toFloat()
+        } else {
+            layout.getLineDescent(line).toFloat()
+        }
+    }
 
     fun getLineHeight(lineIndex: Int): Float = getLineBottom(lineIndex) - getLineTop(lineIndex)
 
@@ -711,10 +759,10 @@
         // reuse the existing rect since there is single line
         firstLineTextBounds
     } else {
-        val line = layout.lineCount - 1
+        val line = lineCount - 1
         paint.getCharSequenceBounds(text, layout.getLineStart(line), layout.getLineEnd(line))
     }
-    val descent = layout.getLineDescent(layout.lineCount - 1)
+    val descent = layout.getLineDescent(lineCount - 1)
 
     // when textBounds.bottom is "lower" than descent, we need to add the difference into account
     // since includeFontPadding is false, descent is at the bottom of Layout
@@ -734,10 +782,11 @@
 private val EmptyPair = Pair(0, 0)
 
 @OptIn(InternalPlatformTextApi::class)
-private fun TextLayout.getLineHeightPaddings(): Pair<Int, Int> {
+private fun TextLayout.getLineHeightPaddings(
+    lineHeightSpans: Array<LineHeightStyleSpan>
+): Pair<Int, Int> {
     var firstAscentDiff = 0
     var lastDescentDiff = 0
-    val lineHeightSpans = getLineHeightSpans()
 
     for (span in lineHeightSpans) {
         if (span.firstAscentDiff < 0) {
@@ -756,6 +805,60 @@
 }
 
 @OptIn(InternalPlatformTextApi::class)
+private fun TextLayout.getLastLineMetrics(
+    textPaint: TextPaint,
+    frameworkTextDir: TextDirectionHeuristic,
+    lineHeightSpans: Array<LineHeightStyleSpan>
+): Pair<FontMetricsInt?, Int> {
+    val lastLine = lineCount - 1
+    // did not check for "\n" since the last line might include zero width characters
+    if (layout.getLineStart(lastLine) == layout.getLineEnd(lastLine) &&
+        lineHeightSpans.isNotEmpty()
+    ) {
+        val emptyText = SpannableString("\u200B")
+        val lineHeightSpan = lineHeightSpans.first()
+        val newLineHeightSpan = lineHeightSpan.copy(
+            startIndex = 0,
+            endIndex = emptyText.length,
+            trimFirstLineTop = if (lastLine != 0 && lineHeightSpan.trimLastLineBottom) {
+                false
+            } else {
+                lineHeightSpan.trimLastLineBottom
+            }
+        )
+
+        emptyText.setSpan(
+            newLineHeightSpan,
+            0,
+            emptyText.length,
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+        )
+
+        val tmpLayout = StaticLayoutFactory.create(
+            text = emptyText,
+            start = 0,
+            end = emptyText.length,
+            width = Int.MAX_VALUE,
+            paint = textPaint,
+            textDir = frameworkTextDir,
+            includePadding = includePadding,
+            useFallbackLineSpacing = fallbackLineSpacing
+        )
+
+        val lastLineFontMetrics = FontMetricsInt().apply {
+            ascent = tmpLayout.getLineAscent(0)
+            descent = tmpLayout.getLineDescent(0)
+            top = tmpLayout.getLineTop(0)
+            bottom = tmpLayout.getLineBottom(0)
+        }
+
+        val lastLineExtra = lastLineFontMetrics.bottom - getLineHeight(lastLine).toInt()
+        return Pair(lastLineFontMetrics, lastLineExtra)
+    }
+    return Pair(null, 0)
+}
+
+@OptIn(InternalPlatformTextApi::class)
 private fun TextLayout.getLineHeightSpans(): Array<LineHeightStyleSpan> {
     if (text !is Spanned) return emptyArray()
     val lineHeightStyleSpans = (text as Spanned).getSpans(
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt
index 24e2065..55e164a 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightStyleSpan.kt
@@ -49,8 +49,8 @@
     private val startIndex: Int,
     private val endIndex: Int,
     private val trimFirstLineTop: Boolean,
-    private val trimLastLineBottom: Boolean,
-    @IntRange(from = 0, to = 100) private val topPercentage: Int
+    val trimLastLineBottom: Boolean,
+    @IntRange(from = 0, to = 100) val topPercentage: Int
 ) : android.text.style.LineHeightSpan {
 
     private var firstAscent: Int = 0
@@ -125,6 +125,19 @@
         firstAscentDiff = fontMetricsInt.ascent - firstAscent
         lastDescentDiff = lastDescent - fontMetricsInt.descent
     }
+
+    internal fun copy(
+        startIndex: Int,
+        endIndex: Int,
+        trimFirstLineTop: Boolean = this.trimFirstLineTop
+    ) = LineHeightStyleSpan(
+        lineHeight = lineHeight,
+        startIndex = startIndex,
+        endIndex = endIndex,
+        trimFirstLineTop = trimFirstLineTop,
+        trimLastLineBottom = trimLastLineBottom,
+        topPercentage = topPercentage
+    )
 }
 
 internal fun FontMetricsInt.lineHeight(): Int = this.descent - this.ascent
\ No newline at end of file