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