1 /*
<lambda>null2  * 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.text.font.Font
20 import androidx.compose.ui.text.font.FontFamily
21 import androidx.compose.ui.text.font.createFontFamilyResolver
22 import androidx.compose.ui.text.internal.requirePrecondition
23 import androidx.compose.ui.text.style.TextDirection
24 import androidx.compose.ui.unit.Density
25 import androidx.compose.ui.util.fastAny
26 import androidx.compose.ui.util.fastFilteredMap
27 import androidx.compose.ui.util.fastMaxBy
28 
29 /**
30  * Calculates and provides the intrinsic width and height of text that contains [ParagraphStyle].
31  *
32  * @param annotatedString the text to be laid out
33  * @param style the [TextStyle] to be applied to the whole text
34  * @param placeholders a list of [Placeholder]s that specify ranges of text which will be skipped
35  *   during layout and replaced with [Placeholder]. It's required that the range of each
36  *   [Placeholder] doesn't cross paragraph boundary, otherwise [IllegalArgumentException] is thrown.
37  * @param density density of the device
38  * @param fontFamilyResolver [Font.ResourceLoader] to be used to load the font given in [SpanStyle]s
39  * @throws IllegalArgumentException if [ParagraphStyle.textDirection] is not set, or any of the
40  *   [placeholders] crosses paragraph boundary.
41  * @see MultiParagraph
42  * @see Placeholder
43  */
44 class MultiParagraphIntrinsics(
45     val annotatedString: AnnotatedString,
46     style: TextStyle,
47     val placeholders: List<AnnotatedString.Range<Placeholder>>,
48     density: Density,
49     fontFamilyResolver: FontFamily.Resolver
50 ) : ParagraphIntrinsics {
51 
52     @Suppress("DEPRECATION")
53     @Deprecated(
54         "Font.ResourceLoader is deprecated, call with fontFamilyResolver",
55         replaceWith =
56             ReplaceWith(
57                 "MultiParagraphIntrinsics(annotatedString, style, " +
58                     "placeholders, density, fontFamilyResolver)"
59             )
60     )
61     constructor(
62         annotatedString: AnnotatedString,
63         style: TextStyle,
64         placeholders: List<AnnotatedString.Range<Placeholder>>,
65         density: Density,
66         resourceLoader: Font.ResourceLoader
67     ) : this(
68         annotatedString,
69         style,
70         placeholders,
71         density,
72         createFontFamilyResolver(resourceLoader)
73     )
74 
75     // NOTE(text-perf-review): why are we using lazy here? Are there cases where these
76     // calculations aren't executed?
77     override val minIntrinsicWidth: Float by
78         lazy(LazyThreadSafetyMode.NONE) {
79             infoList.fastMaxBy { it.intrinsics.minIntrinsicWidth }?.intrinsics?.minIntrinsicWidth
80                 ?: 0f
81         }
82 
83     override val maxIntrinsicWidth: Float by
84         lazy(LazyThreadSafetyMode.NONE) {
85             infoList.fastMaxBy { it.intrinsics.maxIntrinsicWidth }?.intrinsics?.maxIntrinsicWidth
86                 ?: 0f
87         }
88 
89     /**
90      * [ParagraphIntrinsics] for each paragraph included in the [buildAnnotatedString]. For empty
91      * string there will be a single empty paragraph intrinsics info.
92      */
93     internal val infoList: List<ParagraphIntrinsicInfo>
94 
95     init {
96         val paragraphStyle = style.toParagraphStyle()
97         infoList =
98             annotatedString.mapEachParagraphStyle(paragraphStyle) {
99                 annotatedString,
100                 paragraphStyleItem ->
101                 val currentParagraphStyle =
102                     resolveTextDirection(paragraphStyleItem.item, paragraphStyle)
103 
104                 ParagraphIntrinsicInfo(
105                     intrinsics =
106                         ParagraphIntrinsics(
107                             text = annotatedString.text,
108                             style = style.merge(currentParagraphStyle),
109                             annotations = annotatedString.annotations ?: emptyList(),
110                             placeholders =
111                                 placeholders.getLocalPlaceholders(
112                                     paragraphStyleItem.start,
113                                     paragraphStyleItem.end
114                                 ),
115                             density = density,
116                             fontFamilyResolver = fontFamilyResolver
117                         ),
118                     startIndex = paragraphStyleItem.start,
119                     endIndex = paragraphStyleItem.end
120                 )
121             }
122     }
123 
124     override val hasStaleResolvedFonts: Boolean
125         get() = infoList.fastAny { it.intrinsics.hasStaleResolvedFonts }
126 
127     /**
128      * if the [style] does `not` have [TextDirection] set, it will return a new [ParagraphStyle]
129      * where [TextDirection] is set using the [defaultStyle]. Otherwise returns the same [style]
130      * object.
131      *
132      * @param style ParagraphStyle to be checked for [TextDirection]
133      * @param defaultStyle [ParagraphStyle] passed to [MultiParagraphIntrinsics] as the main style
134      */
135     private fun resolveTextDirection(
136         style: ParagraphStyle,
137         defaultStyle: ParagraphStyle
138     ): ParagraphStyle {
139         return if (style.textDirection != TextDirection.Unspecified) style
140         else style.copy(textDirection = defaultStyle.textDirection)
141     }
142 }
143 
Listnull144 private fun List<AnnotatedString.Range<Placeholder>>.getLocalPlaceholders(start: Int, end: Int) =
145     fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
<lambda>null146         requirePrecondition(start <= it.start && it.end <= end) {
147             "placeholder can not overlap with paragraph."
148         }
149         AnnotatedString.Range(it.item, it.start - start, it.end - start)
150     }
151 
152 internal data class ParagraphIntrinsicInfo(
153     val intrinsics: ParagraphIntrinsics,
154     val startIndex: Int,
155     val endIndex: Int
156 )
157