1 /*
2  * Copyright 2019 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 androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.geometry.Rect
21 import androidx.compose.ui.graphics.Path
22 import androidx.compose.ui.text.font.Font
23 import androidx.compose.ui.text.font.FontFamily
24 import androidx.compose.ui.text.font.createFontFamilyResolver
25 import androidx.compose.ui.text.font.toFontFamily
26 import androidx.compose.ui.text.platform.SynchronizedObject
27 import androidx.compose.ui.text.platform.makeSynchronizedObject
28 import androidx.compose.ui.text.platform.synchronized
29 import androidx.compose.ui.text.style.ResolvedTextDirection
30 import androidx.compose.ui.text.style.TextOverflow
31 import androidx.compose.ui.unit.Constraints
32 import androidx.compose.ui.unit.Density
33 import androidx.compose.ui.unit.IntSize
34 import androidx.compose.ui.unit.LayoutDirection
35 
36 /** The data class which holds the set of parameters of the text layout computation. */
37 class TextLayoutInput
38 private constructor(
39     /** The text used for computing text layout. */
40     val text: AnnotatedString,
41 
42     /** The text layout used for computing this text layout. */
43     val style: TextStyle,
44 
45     /**
46      * A list of [Placeholder]s inserted into text layout that reserves space to embed icons or
47      * custom emojis. A list of bounding boxes will be returned in
48      * [TextLayoutResult.placeholderRects] that corresponds to this input.
49      *
50      * @see TextLayoutResult.placeholderRects
51      * @see MultiParagraph
52      * @see MultiParagraphIntrinsics
53      */
54     val placeholders: List<AnnotatedString.Range<Placeholder>>,
55 
56     /** The maxLines param used for computing this text layout. */
57     val maxLines: Int,
58 
59     /** The maxLines param used for computing this text layout. */
60     val softWrap: Boolean,
61 
62     /** The overflow param used for computing this text layout */
63     val overflow: TextOverflow,
64 
65     /** The density param used for computing this text layout. */
66     val density: Density,
67 
68     /** The layout direction used for computing this text layout. */
69     val layoutDirection: LayoutDirection,
70 
71     /**
72      * The font resource loader used for computing this text layout.
73      *
74      * This is no longer used.
75      *
76      * @see fontFamilyResolver
77      */
78     @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader?,
79 
80     /** The font resolver used for computing this text layout. */
81     val fontFamilyResolver: FontFamily.Resolver,
82 
83     /** The minimum width provided while calculating this text layout. */
84     val constraints: Constraints
85 ) {
86 
87     private var _developerSuppliedResourceLoader = resourceLoader
88     @Deprecated(
89         "Replaced with FontFamily.Resolver",
90         replaceWith = ReplaceWith("fontFamilyResolver"),
91     )
92     @Suppress("DEPRECATION")
93     val resourceLoader: Font.ResourceLoader
94         get() {
95             return _developerSuppliedResourceLoader
96                 ?: DeprecatedBridgeFontResourceLoader.from(fontFamilyResolver)
97         }
98 
99     @Deprecated(
100         "Font.ResourceLoader is replaced with FontFamily.Resolver",
101         replaceWith =
102             ReplaceWith(
103                 "TextLayoutInput(text, style, placeholders, " +
104                     "maxLines, softWrap, overflow, density, layoutDirection, fontFamilyResolver, " +
105                     "constraints"
106             )
107     )
108     @Suppress("DEPRECATION")
109     constructor(
110         text: AnnotatedString,
111         style: TextStyle,
112         placeholders: List<AnnotatedString.Range<Placeholder>>,
113         maxLines: Int,
114         softWrap: Boolean,
115         overflow: TextOverflow,
116         density: Density,
117         layoutDirection: LayoutDirection,
118         resourceLoader: Font.ResourceLoader,
119         constraints: Constraints
120     ) : this(
121         text,
122         style,
123         placeholders,
124         maxLines,
125         softWrap,
126         overflow,
127         density,
128         layoutDirection,
129         resourceLoader,
130         createFontFamilyResolver(resourceLoader),
131         constraints
132     )
133 
134     constructor(
135         text: AnnotatedString,
136         style: TextStyle,
137         placeholders: List<AnnotatedString.Range<Placeholder>>,
138         maxLines: Int,
139         softWrap: Boolean,
140         overflow: TextOverflow,
141         density: Density,
142         layoutDirection: LayoutDirection,
143         fontFamilyResolver: FontFamily.Resolver,
144         constraints: Constraints
145     ) : this(
146         text,
147         style,
148         placeholders,
149         maxLines,
150         softWrap,
151         overflow,
152         density,
153         layoutDirection,
154         @Suppress("DEPRECATION") null,
155         fontFamilyResolver,
156         constraints
157     )
158 
159     @Deprecated(
160         "Font.ResourceLoader is deprecated",
161         replaceWith =
162             ReplaceWith(
163                 "TextLayoutInput(text, style, placeholders," +
164                     " maxLines, softWrap, overFlow, density, layoutDirection, fontFamilyResolver, " +
165                     "constraints)"
166             )
167     )
168     // Unfortunately, there's no way to deprecate and add a parameter to a copy chain such that the
169     // resolution is valid.
170     //
171     // However, as this was never intended to be a public function we will not replace it. There is
172     // no use case for calling this method directly.
copynull173     fun copy(
174         text: AnnotatedString = this.text,
175         style: TextStyle = this.style,
176         placeholders: List<AnnotatedString.Range<Placeholder>> = this.placeholders,
177         maxLines: Int = this.maxLines,
178         softWrap: Boolean = this.softWrap,
179         overflow: TextOverflow = this.overflow,
180         density: Density = this.density,
181         layoutDirection: LayoutDirection = this.layoutDirection,
182         @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader = this.resourceLoader,
183         constraints: Constraints = this.constraints
184     ): TextLayoutInput {
185         return TextLayoutInput(
186             text = text,
187             style = style,
188             placeholders = placeholders,
189             maxLines = maxLines,
190             softWrap = softWrap,
191             overflow = overflow,
192             density = density,
193             layoutDirection = layoutDirection,
194             resourceLoader = resourceLoader,
195             fontFamilyResolver = fontFamilyResolver,
196             constraints = constraints
197         )
198     }
199 
equalsnull200     override fun equals(other: Any?): Boolean {
201         if (this === other) return true
202         if (other !is TextLayoutInput) return false
203 
204         if (text != other.text) return false
205         if (style != other.style) return false
206         if (placeholders != other.placeholders) return false
207         if (maxLines != other.maxLines) return false
208         if (softWrap != other.softWrap) return false
209         if (overflow != other.overflow) return false
210         if (density != other.density) return false
211         if (layoutDirection != other.layoutDirection) return false
212         if (fontFamilyResolver != other.fontFamilyResolver) return false
213         if (constraints != other.constraints) return false
214 
215         return true
216     }
217 
hashCodenull218     override fun hashCode(): Int {
219         var result = text.hashCode()
220         result = 31 * result + style.hashCode()
221         result = 31 * result + placeholders.hashCode()
222         result = 31 * result + maxLines
223         result = 31 * result + softWrap.hashCode()
224         result = 31 * result + overflow.hashCode()
225         result = 31 * result + density.hashCode()
226         result = 31 * result + layoutDirection.hashCode()
227         result = 31 * result + fontFamilyResolver.hashCode()
228         result = 31 * result + constraints.hashCode()
229         return result
230     }
231 
toStringnull232     override fun toString(): String {
233         return "TextLayoutInput(" +
234             "text=$text, " +
235             "style=$style, " +
236             "placeholders=$placeholders, " +
237             "maxLines=$maxLines, " +
238             "softWrap=$softWrap, " +
239             "overflow=$overflow, " +
240             "density=$density, " +
241             "layoutDirection=$layoutDirection, " +
242             "fontFamilyResolver=$fontFamilyResolver, " +
243             "constraints=$constraints" +
244             ")"
245     }
246 }
247 
248 @Suppress("DEPRECATION")
249 private class DeprecatedBridgeFontResourceLoader
250 private constructor(private val fontFamilyResolver: FontFamily.Resolver) : Font.ResourceLoader {
251     @Deprecated(
252         "Replaced by FontFamily.Resolver, this method should not be called",
253         ReplaceWith("FontFamily.Resolver.resolve(font, )"),
254     )
loadnull255     override fun load(font: Font): Any {
256         return fontFamilyResolver.resolve(font.toFontFamily(), font.weight, font.style).value
257     }
258 
259     companion object {
260         // In normal usage will  be a map of size 1.
261         //
262         // To fill this map with a large number of entries an app must:
263         //
264         // 1. Repeatedly change FontFamily.Resolver
265         // 2. Call the deprecated method getFontResourceLoader on TextLayoutInput
266         //
267         // If this map is found to be large in profiling of an app, please modify your code to not
268         // call getFontResourceLoader, and evaluate if FontFamily.Resolver is being correctly cached
269         // (via e.g. remember)
270         var cache = mutableMapOf<FontFamily.Resolver, Font.ResourceLoader>()
271         val lock: SynchronizedObject = makeSynchronizedObject()
272 
fromnull273         fun from(fontFamilyResolver: FontFamily.Resolver): Font.ResourceLoader {
274             synchronized(lock) {
275                 // the same resolver to return the same ResourceLoader
276                 cache[fontFamilyResolver]?.let {
277                     return it
278                 }
279 
280                 val deprecatedBridgeFontResourceLoader =
281                     DeprecatedBridgeFontResourceLoader(fontFamilyResolver)
282                 cache[fontFamilyResolver] = deprecatedBridgeFontResourceLoader
283                 return deprecatedBridgeFontResourceLoader
284             }
285         }
286     }
287 }
288 
289 /** The data class which holds text layout result. */
290 class TextLayoutResult
291 constructor(
292     /** The parameters used for computing this text layout result. */
293     val layoutInput: TextLayoutInput,
294 
295     /**
296      * The multi paragraph object.
297      *
298      * This is the result of the text layout computation.
299      */
300     val multiParagraph: MultiParagraph,
301 
302     /** The amount of space required to paint this text in Int. */
303     val size: IntSize
304 ) {
305     /** The distance from the top to the alphabetic baseline of the first line. */
306     val firstBaseline: Float = multiParagraph.firstBaseline
307 
308     /** The distance from the top to the alphabetic baseline of the last line. */
309     val lastBaseline: Float = multiParagraph.lastBaseline
310 
311     /** Returns true if the text is too tall and couldn't fit with given height. */
312     val didOverflowHeight: Boolean
313         get() = multiParagraph.didExceedMaxLines || size.height < multiParagraph.height
314 
315     /** Returns true if the text is too wide and couldn't fit with given width. */
316     val didOverflowWidth: Boolean
317         get() = size.width < multiParagraph.width
318 
319     /** Returns true if either vertical overflow or horizontal overflow happens. */
320     val hasVisualOverflow: Boolean
321         get() = didOverflowWidth || didOverflowHeight
322 
323     /**
324      * Returns a list of bounding boxes that is reserved for [TextLayoutInput.placeholders]. Each
325      * [Rect] in this list corresponds to the [Placeholder] passed to [TextLayoutInput.placeholders]
326      * and it will have the height and width specified in the [Placeholder]. It's guaranteed that
327      * [TextLayoutInput.placeholders] and [TextLayoutResult.placeholderRects] will have same length
328      * and order.
329      *
330      * @see TextLayoutInput.placeholders
331      * @see Placeholder
332      */
333     val placeholderRects: List<Rect?> = multiParagraph.placeholderRects
334 
335     /** Returns a number of lines of this text layout */
336     val lineCount: Int
337         get() = multiParagraph.lineCount
338 
339     /**
340      * Returns the start offset of the given line, inclusive.
341      *
342      * The start offset represents a position in text before the first character in the given line.
343      * For example, `getLineStart(1)` will return 4 for the text below
344      * <pre>
345      * ┌────┐
346      * │abcd│
347      * │efg │
348      * └────┘
349      * </pre>
350      *
351      * @param lineIndex the line number
352      * @return the start offset of the line
353      */
getLineStartnull354     fun getLineStart(lineIndex: Int): Int = multiParagraph.getLineStart(lineIndex)
355 
356     /**
357      * Returns the end offset of the given line.
358      *
359      * The end offset represents a position in text after the last character in the given line. For
360      * example, `getLineEnd(0)` will return 4 for the text below
361      * <pre>
362      * ┌────┐
363      * │abcd│
364      * │efg │
365      * └────┘
366      * </pre>
367      *
368      * Characters being ellipsized are treated as invisible characters. So that if visibleEnd is
369      * false, it will return line end including the ellipsized characters and vice versa.
370      *
371      * @param lineIndex the line number
372      * @param visibleEnd if true, the returned line end will not count trailing whitespaces or
373      *   linefeed characters. Otherwise, this function will return the logical line end. By default
374      *   it's false.
375      * @return an exclusive end offset of the line.
376      */
377     fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int =
378         multiParagraph.getLineEnd(lineIndex, visibleEnd)
379 
380     /**
381      * Returns true if the given line is ellipsized, otherwise returns false.
382      *
383      * @param lineIndex a 0 based line index
384      * @return true if the given line is ellipsized, otherwise false
385      */
386     fun isLineEllipsized(lineIndex: Int): Boolean = multiParagraph.isLineEllipsized(lineIndex)
387 
388     /**
389      * Returns the top y coordinate of the given line.
390      *
391      * @param lineIndex the line number
392      * @return the line top y coordinate
393      */
394     fun getLineTop(lineIndex: Int): Float = multiParagraph.getLineTop(lineIndex)
395 
396     /**
397      * Returns the distance in pixels from the top of the text layout to the alphabetic baseline of
398      * the line at index [lineIndex].
399      */
400     fun getLineBaseline(lineIndex: Int): Float = multiParagraph.getLineBaseline(lineIndex)
401 
402     /**
403      * Returns the bottom y coordinate of the given line.
404      *
405      * @param lineIndex the line number
406      * @return the line bottom y coordinate
407      */
408     fun getLineBottom(lineIndex: Int): Float = multiParagraph.getLineBottom(lineIndex)
409 
410     /**
411      * Returns the left x coordinate of the given line.
412      *
413      * @param lineIndex the line number
414      * @return the line left x coordinate
415      */
416     fun getLineLeft(lineIndex: Int): Float = multiParagraph.getLineLeft(lineIndex)
417 
418     /**
419      * Returns the right x coordinate of the given line.
420      *
421      * @param lineIndex the line number
422      * @return the line right x coordinate
423      */
424     fun getLineRight(lineIndex: Int): Float = multiParagraph.getLineRight(lineIndex)
425 
426     /**
427      * Returns the line number on which the specified text offset appears.
428      *
429      * If you ask for a position before 0, you get 0; if you ask for a position beyond the end of
430      * the text, you get the last line.
431      *
432      * @param offset a character offset
433      * @return the 0 origin line number.
434      */
435     fun getLineForOffset(offset: Int): Int = multiParagraph.getLineForOffset(offset)
436 
437     /**
438      * Returns line number closest to the given graphical vertical position.
439      *
440      * If you ask for a vertical position before 0, you get 0; if you ask for a vertical position
441      * beyond the last line, you get the last line.
442      *
443      * @param vertical the vertical position
444      * @return the 0 origin line number.
445      */
446     fun getLineForVerticalPosition(vertical: Float): Int =
447         multiParagraph.getLineForVerticalPosition(vertical)
448 
449     /**
450      * Get the horizontal position for the specified text [offset].
451      *
452      * Returns the relative distance from the text starting offset. For example, if the paragraph
453      * direction is Left-to-Right, this function returns positive value as a distance from the
454      * left-most edge. If the paragraph direction is Right-to-Left, this function returns negative
455      * value as a distance from the right-most edge.
456      *
457      * [usePrimaryDirection] argument is taken into account only when the offset is in the BiDi
458      * directional transition point. [usePrimaryDirection] is true means use the primary direction
459      * run's coordinate, and use the secondary direction's run's coordinate if false.
460      *
461      * @param offset a character offset
462      * @param usePrimaryDirection true for using the primary run's coordinate if the given offset is
463      *   in the BiDi directional transition point.
464      * @return the relative distance from the text starting edge.
465      * @see MultiParagraph.getHorizontalPosition
466      */
467     fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float =
468         multiParagraph.getHorizontalPosition(offset, usePrimaryDirection)
469 
470     /**
471      * Get the text direction of the paragraph containing the given offset.
472      *
473      * @param offset a character offset
474      * @return the paragraph direction
475      */
476     fun getParagraphDirection(offset: Int): ResolvedTextDirection =
477         multiParagraph.getParagraphDirection(offset)
478 
479     /**
480      * Get the text direction of the resolved BiDi run that the character at the given offset
481      * associated with.
482      *
483      * @param offset a character offset
484      * @return the direction of the BiDi run of the given character offset.
485      */
486     fun getBidiRunDirection(offset: Int): ResolvedTextDirection =
487         multiParagraph.getBidiRunDirection(offset)
488 
489     /**
490      * Returns the character offset closest to the given graphical position.
491      *
492      * @param position a graphical position in this text layout
493      * @return a character offset that is closest to the given graphical position.
494      */
495     fun getOffsetForPosition(position: Offset): Int = multiParagraph.getOffsetForPosition(position)
496 
497     /**
498      * Returns the bounding box of the character for given character offset.
499      *
500      * @param offset a character offset
501      * @return a bounding box for the character in pixels.
502      */
503     fun getBoundingBox(offset: Int): Rect = multiParagraph.getBoundingBox(offset)
504 
505     /**
506      * Returns the text range of the word at the given character offset.
507      *
508      * Characters not part of a word, such as spaces, symbols, and punctuation, have word breaks on
509      * both sides. In such cases, this method will return a text range that contains the given
510      * character offset.
511      *
512      * Word boundaries are defined more precisely in Unicode Standard Annex #29
513      * <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
514      */
515     fun getWordBoundary(offset: Int): TextRange = multiParagraph.getWordBoundary(offset)
516 
517     /**
518      * Returns the rectangle of the cursor area
519      *
520      * @param offset An character offset of the cursor
521      * @return a rectangle of cursor region
522      */
523     fun getCursorRect(offset: Int): Rect = multiParagraph.getCursorRect(offset)
524 
525     /**
526      * Returns path that enclose the given text range.
527      *
528      * @param start an inclusive start character offset
529      * @param end an exclusive end character offset
530      * @return a drawing path
531      */
532     fun getPathForRange(start: Int, end: Int): Path = multiParagraph.getPathForRange(start, end)
533 
534     fun copy(
535         layoutInput: TextLayoutInput = this.layoutInput,
536         size: IntSize = this.size
537     ): TextLayoutResult {
538         return TextLayoutResult(
539             layoutInput = layoutInput,
540             multiParagraph = multiParagraph,
541             size = size
542         )
543     }
544 
equalsnull545     override fun equals(other: Any?): Boolean {
546         if (this === other) return true
547         if (other !is TextLayoutResult) return false
548 
549         if (layoutInput != other.layoutInput) return false
550         if (multiParagraph != other.multiParagraph) return false
551         if (size != other.size) return false
552         if (firstBaseline != other.firstBaseline) return false
553         if (lastBaseline != other.lastBaseline) return false
554         if (placeholderRects != other.placeholderRects) return false
555 
556         return true
557     }
558 
hashCodenull559     override fun hashCode(): Int {
560         var result = layoutInput.hashCode()
561         result = 31 * result + multiParagraph.hashCode()
562         result = 31 * result + size.hashCode()
563         result = 31 * result + firstBaseline.hashCode()
564         result = 31 * result + lastBaseline.hashCode()
565         result = 31 * result + placeholderRects.hashCode()
566         return result
567     }
568 
toStringnull569     override fun toString(): String {
570         return "TextLayoutResult(" +
571             "layoutInput=$layoutInput, " +
572             "multiParagraph=$multiParagraph, " +
573             "size=$size, " +
574             "firstBaseline=$firstBaseline, " +
575             "lastBaseline=$lastBaseline, " +
576             "placeholderRects=$placeholderRects" +
577             ")"
578     }
579 }
580