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.foundation.text
18
19 import androidx.compose.ui.geometry.Offset
20 import androidx.compose.ui.text.AnnotatedString
21 import androidx.compose.ui.text.Placeholder
22 import androidx.compose.ui.text.TextLayoutResult
23 import androidx.compose.ui.text.TextRange
24 import androidx.compose.ui.text.TextStyle
25 import androidx.compose.ui.text.font.FontFamily
26 import androidx.compose.ui.text.style.TextOverflow
27 import androidx.compose.ui.unit.Constraints
28 import androidx.compose.ui.unit.Density
29 import androidx.compose.ui.unit.LayoutDirection
30
31 /**
32 * Returns true if the this TextLayoutResult can be reused for given parameters.
33 *
34 * @param text a text to be used for computing text layout.
35 * @param style a text style to be used for computing text layout.
36 * @param placeholders a list of [Placeholder]s to be used for computing text layout.
37 * @param maxLines a maximum number of lines to be used for computing text layout.
38 * @param softWrap whether doing softwrap or not to be used for computing text layout.
39 * @param overflow an overflow type to be used for computing text layout.
40 * @param density a density to be used for computing text layout.
41 * @param layoutDirection a layout direction to be used for computing text layout.
42 * @param constraints a constraint to be used for computing text layout.
43 */
canReusenull44 internal fun TextLayoutResult.canReuse(
45 text: AnnotatedString,
46 style: TextStyle,
47 placeholders: List<AnnotatedString.Range<Placeholder>>,
48 maxLines: Int,
49 softWrap: Boolean,
50 overflow: TextOverflow,
51 density: Density,
52 layoutDirection: LayoutDirection,
53 fontFamilyResolver: FontFamily.Resolver,
54 constraints: Constraints
55 ): Boolean {
56
57 // NOTE(text-perf-review): might make sense to short-circuit instance equality here
58
59 // Check if this is created from the same parameter.
60 val layoutInput = this.layoutInput
61 if (multiParagraph.intrinsics.hasStaleResolvedFonts) {
62 // one of the resolved fonts has updated, and this MultiParagraph is no longer valid for
63 // measure or display
64 return false
65 }
66 if (
67 !(layoutInput.text == text &&
68 layoutInput.style.hasSameLayoutAffectingAttributes(style) &&
69 layoutInput.placeholders == placeholders &&
70 layoutInput.maxLines == maxLines &&
71 layoutInput.softWrap == softWrap &&
72 layoutInput.overflow == overflow &&
73 layoutInput.density == density &&
74 layoutInput.layoutDirection == layoutDirection &&
75 layoutInput.fontFamilyResolver == fontFamilyResolver)
76 ) {
77 return false
78 }
79
80 // Check the given constraints can produces the same result.
81 if (constraints.minWidth != layoutInput.constraints.minWidth) return false
82
83 if (!(softWrap || overflow == TextOverflow.Ellipsis)) {
84 // If width does not matter, we can result the same layout.
85 return true
86 }
87 return constraints.maxWidth == layoutInput.constraints.maxWidth &&
88 constraints.maxHeight == layoutInput.constraints.maxHeight
89 }
90
91 /** Returns whether the given pixel position is inside the selection. */
isPositionInsideSelectionnull92 internal fun TextLayoutResult.isPositionInsideSelection(
93 position: Offset,
94 selectionRange: TextRange?,
95 ): Boolean {
96 if ((selectionRange == null) || selectionRange.collapsed) return false
97
98 fun isOffsetSelectedAndContainsPosition(offset: Int) =
99 selectionRange.contains(offset) && getBoundingBox(offset).contains(position)
100
101 // getOffsetForPosition returns the index at which the cursor should be placed when the
102 // given position is clicked. This means that when position is to the right of the center of
103 // a glyph it will return the index of the next glyph. So we test both the index it returns
104 // and the previous index.
105 val offset = getOffsetForPosition(position)
106 return isOffsetSelectedAndContainsPosition(offset) ||
107 isOffsetSelectedAndContainsPosition(offset - 1)
108 }
109
110 /**
111 * Returns the text line height for the given offset. It also returns the last visible line height
112 * if the given offset is followed after the last visible character. If the text is empty, or if the
113 * requested line is out of the visible range, then returns zero.
114 *
115 * @param offset a character offset
116 * @return the line height for the given offset
117 */
getLineHeightnull118 internal fun TextLayoutResult.getLineHeight(offset: Int): Float {
119 if (offset < 0 || layoutInput.text.isEmpty()) return 0f
120
121 val line =
122 minOf(
123 multiParagraph.getLineForOffset(offset),
124 multiParagraph.maxLines - 1,
125 multiParagraph.lineCount - 1
126 )
127 val lineEnd = multiParagraph.getLineEnd(line)
128 if (offset > lineEnd) return 0f
129
130 return multiParagraph.getLineHeight(line)
131 }
132