1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.text
18 
19 import android.graphics.RectF
20 import android.os.Build
21 import android.text.Spannable
22 import android.text.SpannableString
23 import android.text.Spanned
24 import android.text.TextUtils
25 import androidx.annotation.IntRange
26 import androidx.annotation.VisibleForTesting
27 import androidx.compose.ui.geometry.Offset
28 import androidx.compose.ui.geometry.Rect
29 import androidx.compose.ui.geometry.Size
30 import androidx.compose.ui.graphics.BlendMode
31 import androidx.compose.ui.graphics.Brush
32 import androidx.compose.ui.graphics.Canvas
33 import androidx.compose.ui.graphics.Color
34 import androidx.compose.ui.graphics.Path
35 import androidx.compose.ui.graphics.Shadow
36 import androidx.compose.ui.graphics.asComposePath
37 import androidx.compose.ui.graphics.drawscope.DrawStyle
38 import androidx.compose.ui.graphics.nativeCanvas
39 import androidx.compose.ui.graphics.toAndroidRectF
40 import androidx.compose.ui.graphics.toComposeRect
41 import androidx.compose.ui.text.android.InternalPlatformTextApi
42 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_CENTER
43 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_LEFT
44 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_NORMAL
45 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_OPPOSITE
46 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_RIGHT
47 import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_BALANCED
48 import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_HIGH_QUALITY
49 import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_SIMPLE
50 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_ALIGNMENT
51 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_BREAK_STRATEGY
52 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_HYPHENATION_FREQUENCY
53 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_JUSTIFICATION_MODE
54 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER
55 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_STYLE
56 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_WORD_STYLE
57 import androidx.compose.ui.text.android.LayoutCompat.HYPHENATION_FREQUENCY_FULL
58 import androidx.compose.ui.text.android.LayoutCompat.HYPHENATION_FREQUENCY_FULL_FAST
59 import androidx.compose.ui.text.android.LayoutCompat.HYPHENATION_FREQUENCY_NONE
60 import androidx.compose.ui.text.android.LayoutCompat.JUSTIFICATION_MODE_INTER_WORD
61 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_LOOSE
62 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_NONE
63 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_NORMAL
64 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_STRICT
65 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_WORD_STYLE_NONE
66 import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_WORD_STYLE_PHRASE
67 import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_CHARACTER
68 import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_WORD
69 import androidx.compose.ui.text.android.TextLayout
70 import androidx.compose.ui.text.android.hasSpan
71 import androidx.compose.ui.text.android.selection.getWordEnd
72 import androidx.compose.ui.text.android.selection.getWordStart
73 import androidx.compose.ui.text.android.style.IndentationFixSpan
74 import androidx.compose.ui.text.android.style.PlaceholderSpan
75 import androidx.compose.ui.text.font.FontFamily
76 import androidx.compose.ui.text.internal.requirePrecondition
77 import androidx.compose.ui.text.platform.AndroidParagraphIntrinsics
78 import androidx.compose.ui.text.platform.AndroidTextPaint
79 import androidx.compose.ui.text.platform.extensions.setSpan
80 import androidx.compose.ui.text.platform.isIncludeFontPaddingEnabled
81 import androidx.compose.ui.text.platform.style.ShaderBrushSpan
82 import androidx.compose.ui.text.style.Hyphens
83 import androidx.compose.ui.text.style.LineBreak
84 import androidx.compose.ui.text.style.ResolvedTextDirection
85 import androidx.compose.ui.text.style.TextAlign
86 import androidx.compose.ui.text.style.TextDecoration
87 import androidx.compose.ui.text.style.TextOverflow
88 import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis
89 import androidx.compose.ui.text.style.TextOverflow.Companion.MiddleEllipsis
90 import androidx.compose.ui.text.style.TextOverflow.Companion.StartEllipsis
91 import androidx.compose.ui.unit.Constraints
92 import androidx.compose.ui.unit.Density
93 import androidx.compose.ui.unit.TextUnit
94 import androidx.compose.ui.unit.sp
95 import java.util.Locale as JavaLocale
96 
97 /** Android specific implementation for [Paragraph] */
98 // NOTE(text-perf-review): I see most of the APIs in this class just delegate to TextLayout or to
99 // AndroidParagraphIntrinsics. Should we consider just having one TextLayout class which
100 // implements Paragraph and ParagraphIntrinsics? it seems like all of these types are immutable
101 // and have similar sets of responsibilities.
102 @OptIn(InternalPlatformTextApi::class, ExperimentalTextApi::class)
103 internal class AndroidParagraph(
104     val paragraphIntrinsics: AndroidParagraphIntrinsics,
105     val maxLines: Int,
106     val overflow: TextOverflow,
107     val constraints: Constraints
108 ) : Paragraph {
109     constructor(
110         text: String,
111         style: TextStyle,
112         annotations: List<AnnotatedString.Range<out AnnotatedString.Annotation>>,
113         placeholders: List<AnnotatedString.Range<Placeholder>>,
114         maxLines: Int,
115         overflow: TextOverflow,
116         constraints: Constraints,
117         fontFamilyResolver: FontFamily.Resolver,
118         density: Density
119     ) : this(
120         paragraphIntrinsics =
121             AndroidParagraphIntrinsics(
122                 text = text,
123                 style = style,
124                 annotations = annotations,
125                 placeholders = placeholders,
126                 fontFamilyResolver = fontFamilyResolver,
127                 density = density
128             ),
129         maxLines = maxLines,
130         overflow = overflow,
131         constraints = constraints
132     )
133 
134     private val layout: TextLayout
135 
136     @VisibleForTesting internal val charSequence: CharSequence
137 
138     init {
139         requirePrecondition(constraints.minHeight == 0 && constraints.minWidth == 0) {
140             "Setting Constraints.minWidth and Constraints.minHeight is not supported, " +
141                 "these should be the default zero values instead."
142         }
143         requirePrecondition(maxLines >= 1) { "maxLines should be greater than 0" }
144 
145         val style = paragraphIntrinsics.style
146 
147         charSequence =
148             if (shouldAttachIndentationFixSpan(style, overflow == Ellipsis)) {
149                 // When letter spacing, align and ellipsize applied to text, the ellipsized line is
150                 // indented wrong. This function adds the IndentationFixSpan in order to fix the
151                 // issue
152                 // with best effort. b/228463206
153                 paragraphIntrinsics.charSequence.attachIndentationFixSpan()
154             } else {
155                 paragraphIntrinsics.charSequence
156             }
157 
158         val alignment = toLayoutAlign(style.textAlign)
159 
160         val justificationMode =
161             when (style.textAlign) {
162                 TextAlign.Justify -> JUSTIFICATION_MODE_INTER_WORD
163                 else -> DEFAULT_JUSTIFICATION_MODE
164             }
165 
166         val hyphens = toLayoutHyphenationFrequency(style.paragraphStyle.hyphens)
167 
168         val breakStrategy = toLayoutBreakStrategy(style.lineBreak.strategy)
169         val lineBreakStyle = toLayoutLineBreakStyle(style.lineBreak.strictness)
170         val lineBreakWordStyle = toLayoutLineBreakWordStyle(style.lineBreak.wordBreak)
171 
172         val ellipsize =
173             when (overflow) {
174                 Ellipsis -> TextUtils.TruncateAt.END
175                 MiddleEllipsis -> TextUtils.TruncateAt.MIDDLE
176                 StartEllipsis -> TextUtils.TruncateAt.START
177                 else -> null
178             }
179 
180         var firstLayout =
181             constructTextLayout(
182                 alignment = alignment,
183                 justificationMode = justificationMode,
184                 ellipsize = ellipsize,
185                 maxLines = maxLines,
186                 hyphens = hyphens,
187                 breakStrategy = breakStrategy,
188                 lineBreakStyle = lineBreakStyle,
189                 lineBreakWordStyle = lineBreakWordStyle
190             )
191 
192         // In case of start/middle ellipsis when the letter spacing is enabled and some of the
193         // characters are ellipsized away, we need to remeasure. This is because though
194         // internally ellipsized character are replaced with zero-width U+FEFF character, the
195         // letter spacing is still applied to each such character. It's been fixed on API 35
196         // where letter spacing won't be applied to some special characters including U+FEFF.
197         if (
198             Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM &&
199                 textPaint.letterSpacing != 0f &&
200                 (overflow == StartEllipsis || overflow == MiddleEllipsis) &&
201                 firstLayout.getLineEllipsisCount(0) > 0
202         ) {
203             val beforeEllipsis = firstLayout.getLineEllipsisOffset(0)
204             val afterEllipsis = beforeEllipsis + firstLayout.getLineEllipsisCount(0)
205             val newSpannable =
206                 TextUtils.concat(
207                     charSequence.subSequence(0, beforeEllipsis),
208                     Typography.ellipsis.toString(),
209                     charSequence.subSequence(afterEllipsis, charSequence.length)
210                 )
211             firstLayout =
212                 constructTextLayout(
213                     alignment = alignment,
214                     justificationMode = justificationMode,
215                     ellipsize = ellipsize,
216                     maxLines = maxLines,
217                     hyphens = hyphens,
218                     breakStrategy = breakStrategy,
219                     lineBreakStyle = lineBreakStyle,
220                     lineBreakWordStyle = lineBreakWordStyle,
221                     charSequence = newSpannable
222                 )
223         }
224 
225         // Ellipsize if there's not enough vertical space to fit all lines. Because this only makes
226         // sense for end ellipsis because start/middle only works for a single line.
227         if (overflow == Ellipsis && firstLayout.height > constraints.maxHeight && maxLines > 1) {
228             val calculatedMaxLines =
229                 firstLayout.numberOfLinesThatFitMaxHeight(constraints.maxHeight)
230             layout =
231                 if (calculatedMaxLines >= 0 && calculatedMaxLines != maxLines) {
232                     constructTextLayout(
233                         alignment = alignment,
234                         justificationMode = justificationMode,
235                         ellipsize = ellipsize,
236                         // When we can't fully fit even a single line, measure with one line anyway.
237                         // This will allow to have an ellipsis on that single line. If we measured
238                         // with 0 maxLines, it would measure all lines with no ellipsis even though
239                         // the first line might be partially visible
240                         maxLines = calculatedMaxLines.coerceAtLeast(1),
241                         hyphens = hyphens,
242                         breakStrategy = breakStrategy,
243                         lineBreakStyle = lineBreakStyle,
244                         lineBreakWordStyle = lineBreakWordStyle
245                     )
246                 } else {
247                     firstLayout
248                 }
249         } else {
250             layout = firstLayout
251         }
252 
253         // Brush is not fully realized on text until layout is complete and size information
254         // is known. Brush can now be applied to the overall textpaint and all the spans.
255         textPaint.setBrush(style.brush, Size(width, height), style.alpha)
256         val shaderBrushSpans = layout.getShaderBrushSpans()
257         if (shaderBrushSpans != null) {
258             for (shaderBrushSpan in shaderBrushSpans) {
259                 shaderBrushSpan.size = Size(width, height)
260             }
261         }
262     }
263 
264     override val width: Float
265         get() = constraints.maxWidth.toFloat()
266 
267     override val height: Float
268         get() = layout.height.toFloat()
269 
270     override val maxIntrinsicWidth: Float
271         get() = paragraphIntrinsics.maxIntrinsicWidth
272 
273     override val minIntrinsicWidth: Float
274         get() = paragraphIntrinsics.minIntrinsicWidth
275 
276     override val firstBaseline: Float
277         get() = getLineBaseline(0)
278 
279     override val lastBaseline: Float
280         get() = getLineBaseline(lineCount - 1)
281 
282     override val didExceedMaxLines: Boolean
283         get() = layout.didExceedMaxLines
284 
285     @VisibleForTesting
286     internal val textLocale: JavaLocale
287         get() = paragraphIntrinsics.textPaint.textLocale
288 
289     /**
290      * Resolved line count. If maxLines smaller than the real number of lines in the text, this
291      * property will return the minimum between the two
292      */
293     override val lineCount: Int
294         get() = layout.lineCount
295 
296     override val placeholderRects: List<Rect?> =
297         with(charSequence) {
298             if (this !is Spanned) return@with listOf()
299             getSpans(0, length, PlaceholderSpan::class.java).map { span ->
300                 val start = getSpanStart(span)
301                 val end = getSpanEnd(span)
302                 // The line index of the PlaceholderSpan. In the case where PlaceholderSpan is
303                 // truncated due to maxLines limitation. It will return the index of last line.
304                 val line = layout.getLineForOffset(start)
305                 val exceedsMaxLines = line >= maxLines
306                 val isPlaceholderSpanEllipsized =
307                     layout.getLineEllipsisCount(line) > 0 &&
308                         end > layout.getLineEllipsisOffset(line)
309                 val isPlaceholderSpanTruncated = end > layout.getLineEnd(line)
310                 // This Placeholder is ellipsized or truncated, return null instead.
311                 if (isPlaceholderSpanEllipsized || isPlaceholderSpanTruncated || exceedsMaxLines) {
312                     return@map null
313                 }
314 
315                 val direction = getBidiRunDirection(start)
316 
317                 val left =
318                     when (direction) {
319                         ResolvedTextDirection.Ltr -> getHorizontalPosition(start, true)
320                         ResolvedTextDirection.Rtl ->
321                             getHorizontalPosition(start, true) - span.widthPx
322                     }
323                 val right = left + span.widthPx
324 
325                 val top =
326                     with(layout) {
327                         when (span.verticalAlign) {
328                             PlaceholderSpan.ALIGN_ABOVE_BASELINE ->
329                                 getLineBaseline(line) - span.heightPx
330                             PlaceholderSpan.ALIGN_TOP -> getLineTop(line)
331                             PlaceholderSpan.ALIGN_BOTTOM -> getLineBottom(line) - span.heightPx
332                             PlaceholderSpan.ALIGN_CENTER ->
333                                 (getLineTop(line) + getLineBottom(line) - span.heightPx) / 2
334                             PlaceholderSpan.ALIGN_TEXT_TOP ->
335                                 span.fontMetrics.ascent + getLineBaseline(line)
336                             PlaceholderSpan.ALIGN_TEXT_BOTTOM ->
337                                 span.fontMetrics.descent + getLineBaseline(line) - span.heightPx
338                             PlaceholderSpan.ALIGN_TEXT_CENTER ->
339                                 with(span.fontMetrics) {
340                                     (ascent + descent - span.heightPx) / 2 + getLineBaseline(line)
341                                 }
342                             else -> throw IllegalStateException("unexpected verticalAlignment")
343                         }
344                     }
345 
346                 val bottom = top + span.heightPx
347 
348                 Rect(left, top, right, bottom)
349             }
350         }
351 
352     @VisibleForTesting
353     internal val textPaint: AndroidTextPaint
354         get() = paragraphIntrinsics.textPaint
355 
356     override fun getLineForVerticalPosition(vertical: Float): Int {
357         return layout.getLineForVertical(vertical.toInt())
358     }
359 
360     override fun getOffsetForPosition(position: Offset): Int {
361         val line = layout.getLineForVertical(position.y.toInt())
362         return layout.getOffsetForHorizontal(line, position.x)
363     }
364 
365     override fun getRangeForRect(
366         rect: Rect,
367         granularity: TextGranularity,
368         inclusionStrategy: TextInclusionStrategy
369     ): TextRange {
370         val range =
371             layout.getRangeForRect(
372                 rect = rect.toAndroidRectF(),
373                 granularity = granularity.toLayoutTextGranularity(),
374                 inclusionStrategy = { segmentBounds: RectF, area: RectF ->
375                     inclusionStrategy.isIncluded(
376                         segmentBounds.toComposeRect(),
377                         area.toComposeRect()
378                     )
379                 }
380             ) ?: return TextRange.Zero
381         return TextRange(range[0], range[1])
382     }
383 
384     /**
385      * Returns the bounding box as Rect of the character for given character offset. Rect includes
386      * the top, bottom, left and right of a character.
387      */
388     override fun getBoundingBox(offset: Int): Rect {
389         requirePrecondition(offset in charSequence.indices) {
390             "offset($offset) is out of bounds [0,${charSequence.length})"
391         }
392         val rectF = layout.getBoundingBox(offset)
393         return with(rectF) { Rect(left = left, top = top, right = right, bottom = bottom) }
394     }
395 
396     /**
397      * Fills the bounding boxes for characters provided in the [range] into [array]. The array is
398      * filled starting from [arrayStart] (inclusive). The coordinates are in local text layout
399      * coordinates.
400      *
401      * The returned information consists of left/right of a character; line top and bottom for the
402      * same character.
403      *
404      * For the grapheme consists of multiple code points, e.g. ligatures, combining marks, the first
405      * character has the total width and the remaining are returned as zero-width.
406      *
407      * The array divided into segments of four where each index in that segment represents left,
408      * top, right, bottom of the character.
409      *
410      * The size of the provided [array] should be greater or equal than the four times * [TextRange]
411      * length.
412      *
413      * The final order of characters in the [array] is from [TextRange.min] to [TextRange.max].
414      *
415      * @param range the [TextRange] representing the start and end indices in the [Paragraph].
416      * @param array the array to fill in the values. The array divided into segments of four where
417      *   each index in that segment represents left, top, right, bottom of the character.
418      * @param arrayStart the inclusive start index in the array where the function will start
419      *   filling in the values from
420      */
421     override fun fillBoundingBoxes(
422         range: TextRange,
423         array: FloatArray,
424         @IntRange(from = 0) arrayStart: Int
425     ) {
426         layout.fillBoundingBoxes(range.min, range.max, array, arrayStart)
427     }
428 
429     override fun getPathForRange(start: Int, end: Int): Path {
430         requirePrecondition(start in 0..end && end <= charSequence.length) {
431             "start($start) or end($end) is out of range [0..${charSequence.length}]," +
432                 " or start > end!"
433         }
434         val path = android.graphics.Path()
435         layout.getSelectionPath(start, end, path)
436         return path.asComposePath()
437     }
438 
439     override fun getCursorRect(offset: Int): Rect {
440         requirePrecondition(offset in 0..charSequence.length) {
441             "offset($offset) is out of bounds [0,${charSequence.length}]"
442         }
443         val horizontal = layout.getPrimaryHorizontal(offset)
444         val line = layout.getLineForOffset(offset)
445 
446         // The width of the cursor is not taken into account. The callers of this API should use
447         // rect.left to get the start X position and then adjust it according to the width if needed
448         return Rect(horizontal, layout.getLineTop(line), horizontal, layout.getLineBottom(line))
449     }
450 
451     override fun getWordBoundary(offset: Int): TextRange {
452         val wordIterator = layout.wordIterator
453         return TextRange(wordIterator.getWordStart(offset), wordIterator.getWordEnd(offset))
454     }
455 
456     override fun getLineLeft(lineIndex: Int): Float = layout.getLineLeft(lineIndex)
457 
458     override fun getLineRight(lineIndex: Int): Float = layout.getLineRight(lineIndex)
459 
460     override fun getLineTop(lineIndex: Int): Float = layout.getLineTop(lineIndex)
461 
462     internal fun getLineAscent(lineIndex: Int): Float = layout.getLineAscent(lineIndex)
463 
464     override fun getLineBaseline(lineIndex: Int): Float = layout.getLineBaseline(lineIndex)
465 
466     internal fun getLineDescent(lineIndex: Int): Float = layout.getLineDescent(lineIndex)
467 
468     override fun getLineBottom(lineIndex: Int): Float = layout.getLineBottom(lineIndex)
469 
470     override fun getLineHeight(lineIndex: Int): Float = layout.getLineHeight(lineIndex)
471 
472     override fun getLineWidth(lineIndex: Int): Float = layout.getLineWidth(lineIndex)
473 
474     override fun getLineStart(lineIndex: Int): Int = layout.getLineStart(lineIndex)
475 
476     override fun getLineEnd(lineIndex: Int, visibleEnd: Boolean): Int =
477         if (visibleEnd) {
478             layout.getLineVisibleEnd(lineIndex)
479         } else {
480             layout.getLineEnd(lineIndex)
481         }
482 
483     override fun isLineEllipsized(lineIndex: Int): Boolean = layout.isLineEllipsized(lineIndex)
484 
485     internal fun getLineEllipsisOffset(lineIndex: Int): Int =
486         layout.getLineEllipsisOffset(lineIndex)
487 
488     internal fun getLineEllipsisCount(lineIndex: Int): Int = layout.getLineEllipsisCount(lineIndex)
489 
490     override fun getLineForOffset(offset: Int): Int = layout.getLineForOffset(offset)
491 
492     override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
493         if (usePrimaryDirection) {
494             layout.getPrimaryHorizontal(offset)
495         } else {
496             layout.getSecondaryHorizontal(offset)
497         }
498 
499     override fun getParagraphDirection(offset: Int): ResolvedTextDirection {
500         val lineIndex = layout.getLineForOffset(offset)
501         val direction = layout.getParagraphDirection(lineIndex)
502         return if (direction == 1) ResolvedTextDirection.Ltr else ResolvedTextDirection.Rtl
503     }
504 
505     override fun getBidiRunDirection(offset: Int): ResolvedTextDirection {
506         return if (layout.isRtlCharAt(offset)) ResolvedTextDirection.Rtl
507         else ResolvedTextDirection.Ltr
508     }
509 
510     private fun TextLayout.getShaderBrushSpans(): Array<ShaderBrushSpan>? {
511         if (text !is Spanned) return null
512         if (!(text as Spanned).hasSpan(ShaderBrushSpan::class.java)) return null
513         val brushSpans = (text as Spanned).getSpans(0, text.length, ShaderBrushSpan::class.java)
514         return brushSpans
515     }
516 
517     private fun Spanned.hasSpan(clazz: Class<*>): Boolean {
518         return nextSpanTransition(-1, length, clazz) != length
519     }
520 
521     override fun paint(
522         canvas: Canvas,
523         color: Color,
524         shadow: Shadow?,
525         textDecoration: TextDecoration?
526     ) {
527         with(textPaint) {
528             setColor(color)
529             setShadow(shadow)
530             setTextDecoration(textDecoration)
531         }
532 
533         paint(canvas)
534     }
535 
536     override fun paint(
537         canvas: Canvas,
538         color: Color,
539         shadow: Shadow?,
540         textDecoration: TextDecoration?,
541         drawStyle: DrawStyle?,
542         blendMode: BlendMode
543     ) {
544         val currBlendMode = textPaint.blendMode
545         with(textPaint) {
546             setColor(color)
547             setShadow(shadow)
548             setTextDecoration(textDecoration)
549             setDrawStyle(drawStyle)
550             this.blendMode = blendMode
551         }
552 
553         paint(canvas)
554 
555         textPaint.blendMode = currBlendMode
556     }
557 
558     override fun paint(
559         canvas: Canvas,
560         brush: Brush,
561         alpha: Float,
562         shadow: Shadow?,
563         textDecoration: TextDecoration?,
564         drawStyle: DrawStyle?,
565         blendMode: BlendMode
566     ) {
567         val currBlendMode = textPaint.blendMode
568         with(textPaint) {
569             setBrush(brush, Size(width, height), alpha)
570             setShadow(shadow)
571             setTextDecoration(textDecoration)
572             setDrawStyle(drawStyle)
573             this.blendMode = blendMode
574         }
575 
576         paint(canvas)
577 
578         textPaint.blendMode = currBlendMode
579     }
580 
581     private fun paint(canvas: Canvas) {
582         val nativeCanvas = canvas.nativeCanvas
583         if (didExceedMaxLines) {
584             nativeCanvas.save()
585             nativeCanvas.clipRect(0f, 0f, width, height)
586         }
587         layout.paint(nativeCanvas)
588         if (didExceedMaxLines) {
589             nativeCanvas.restore()
590         }
591     }
592 
593     private fun constructTextLayout(
594         alignment: Int,
595         justificationMode: Int,
596         ellipsize: TextUtils.TruncateAt?,
597         maxLines: Int,
598         hyphens: Int,
599         breakStrategy: Int,
600         lineBreakStyle: Int,
601         lineBreakWordStyle: Int,
602         charSequence: CharSequence = this.charSequence,
603     ) =
604         TextLayout(
605             charSequence = charSequence,
606             width = width,
607             textPaint = textPaint,
608             ellipsize = ellipsize,
609             alignment = alignment,
610             textDirectionHeuristic = paragraphIntrinsics.textDirectionHeuristic,
611             lineSpacingMultiplier = DEFAULT_LINESPACING_MULTIPLIER,
612             maxLines = maxLines,
613             justificationMode = justificationMode,
614             layoutIntrinsics = paragraphIntrinsics.layoutIntrinsics,
615             includePadding = paragraphIntrinsics.style.isIncludeFontPaddingEnabled(),
616             fallbackLineSpacing = true,
617             hyphenationFrequency = hyphens,
618             breakStrategy = breakStrategy,
619             lineBreakStyle = lineBreakStyle,
620             lineBreakWordStyle = lineBreakWordStyle
621         )
622 }
623 
624 /** Converts [TextAlign] into [TextLayout] alignment constants. */
625 @OptIn(InternalPlatformTextApi::class)
toLayoutAlignnull626 private fun toLayoutAlign(align: TextAlign): Int =
627     when (align) {
628         TextAlign.Left -> ALIGN_LEFT
629         TextAlign.Right -> ALIGN_RIGHT
630         TextAlign.Center -> ALIGN_CENTER
631         TextAlign.Start -> ALIGN_NORMAL
632         TextAlign.End -> ALIGN_OPPOSITE
633         else -> DEFAULT_ALIGNMENT
634     }
635 
636 @OptIn(InternalPlatformTextApi::class)
toLayoutHyphenationFrequencynull637 private fun toLayoutHyphenationFrequency(hyphens: Hyphens): Int =
638     when (hyphens) {
639         Hyphens.Auto ->
640             if (Build.VERSION.SDK_INT <= 32) {
641                 HYPHENATION_FREQUENCY_FULL
642             } else {
643                 HYPHENATION_FREQUENCY_FULL_FAST
644             }
645         Hyphens.None -> HYPHENATION_FREQUENCY_NONE
646         else -> DEFAULT_HYPHENATION_FREQUENCY
647     }
648 
649 @OptIn(InternalPlatformTextApi::class)
toLayoutBreakStrategynull650 private fun toLayoutBreakStrategy(breakStrategy: LineBreak.Strategy): Int =
651     when (breakStrategy) {
652         LineBreak.Strategy.Simple -> BREAK_STRATEGY_SIMPLE
653         LineBreak.Strategy.HighQuality -> BREAK_STRATEGY_HIGH_QUALITY
654         LineBreak.Strategy.Balanced -> BREAK_STRATEGY_BALANCED
655         else -> DEFAULT_BREAK_STRATEGY
656     }
657 
658 @OptIn(InternalPlatformTextApi::class)
toLayoutLineBreakStylenull659 private fun toLayoutLineBreakStyle(lineBreakStrictness: LineBreak.Strictness): Int =
660     when (lineBreakStrictness) {
661         LineBreak.Strictness.Default -> LINE_BREAK_STYLE_NONE
662         LineBreak.Strictness.Loose -> LINE_BREAK_STYLE_LOOSE
663         LineBreak.Strictness.Normal -> LINE_BREAK_STYLE_NORMAL
664         LineBreak.Strictness.Strict -> LINE_BREAK_STYLE_STRICT
665         else -> DEFAULT_LINE_BREAK_STYLE
666     }
667 
668 @OptIn(InternalPlatformTextApi::class)
toLayoutLineBreakWordStylenull669 private fun toLayoutLineBreakWordStyle(lineBreakWordStyle: LineBreak.WordBreak): Int =
670     when (lineBreakWordStyle) {
671         LineBreak.WordBreak.Default -> LINE_BREAK_WORD_STYLE_NONE
672         LineBreak.WordBreak.Phrase -> LINE_BREAK_WORD_STYLE_PHRASE
673         else -> DEFAULT_LINE_BREAK_WORD_STYLE
674     }
675 
676 @OptIn(InternalPlatformTextApi::class)
numberOfLinesThatFitMaxHeightnull677 private fun TextLayout.numberOfLinesThatFitMaxHeight(maxHeight: Int): Int {
678     for (lineIndex in 0 until lineCount) {
679         if (getLineBottom(lineIndex) > maxHeight) return lineIndex
680     }
681     return lineCount
682 }
683 
shouldAttachIndentationFixSpannull684 private fun shouldAttachIndentationFixSpan(textStyle: TextStyle, ellipsis: Boolean) =
685     with(textStyle) {
686         ellipsis &&
687             (letterSpacing != 0.sp && letterSpacing != TextUnit.Unspecified) &&
688             (textAlign != TextAlign.Unspecified &&
689                 textAlign != TextAlign.Start &&
690                 textAlign != TextAlign.Justify)
691     }
692 
693 // this _will_ be called multiple times on the same ParagraphIntrinsics
attachIndentationFixSpannull694 private fun CharSequence.attachIndentationFixSpan(): CharSequence {
695     if (isEmpty()) return this
696     val spannable = this as? Spannable ?: SpannableString(this)
697     if (!spannable.hasSpan(IndentationFixSpan::class.java)) {
698         spannable.setSpan(IndentationFixSpan(), spannable.length - 1, spannable.length - 1)
699     }
700     return spannable
701 }
702 
TextGranularitynull703 private fun TextGranularity.toLayoutTextGranularity(): Int {
704     return when (this) {
705         TextGranularity.Character -> TEXT_GRANULARITY_CHARACTER
706         TextGranularity.Word -> TEXT_GRANULARITY_WORD
707         else -> TEXT_GRANULARITY_CHARACTER
708     }
709 }
710