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.modifiers
18 
19 import androidx.compose.foundation.text.TEST_FONT_FAMILY
20 import androidx.compose.foundation.text.toIntPx
21 import androidx.compose.ui.geometry.Offset
22 import androidx.compose.ui.graphics.Canvas
23 import androidx.compose.ui.text.AnnotatedString
24 import androidx.compose.ui.text.SpanStyle
25 import androidx.compose.ui.text.TextPainter
26 import androidx.compose.ui.text.TextStyle
27 import androidx.compose.ui.text.font.createFontFamilyResolver
28 import androidx.compose.ui.unit.Constraints
29 import androidx.compose.ui.unit.Density
30 import androidx.compose.ui.unit.LayoutDirection
31 import androidx.compose.ui.unit.sp
32 import androidx.test.ext.junit.runners.AndroidJUnit4
33 import androidx.test.filters.SmallTest
34 import androidx.test.platform.app.InstrumentationRegistry
35 import com.google.common.truth.IntegerSubject
36 import com.google.common.truth.Truth.assertThat
37 import kotlin.math.floor
38 import org.junit.Test
39 import org.junit.runner.RunWith
40 
41 @RunWith(AndroidJUnit4::class)
42 @SmallTest
43 class TextLayoutResultIntegrationTest {
44 
45     private val fontFamily = TEST_FONT_FAMILY
46     private val density = Density(density = 1f)
47     private val context = InstrumentationRegistry.getInstrumentation().context
48     private val fontFamilyResolver = createFontFamilyResolver(context)
49     private val layoutDirection = LayoutDirection.Ltr
50 
51     @Test
width_getternull52     fun width_getter() {
53         with(density) {
54             val fontSize = 20.sp
55             val text = "Hello"
56             val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
57             val annotatedString = AnnotatedString(text, spanStyle)
58             val textDelegate =
59                 MultiParagraphLayoutCache(
60                         text = annotatedString,
61                         style = TextStyle.Default,
62                         fontFamilyResolver = fontFamilyResolver
63                     )
64                     .also { it.density = this }
65 
66             textDelegate.layoutWithConstraints(Constraints(0, 200), layoutDirection)
67             val layoutResult = textDelegate.textLayoutResult
68 
69             assertThat(layoutResult.size.width).isEqualTo((fontSize.toPx() * text.length).toIntPx())
70         }
71     }
72 
73     @Test
width_getter_with_small_widthnull74     fun width_getter_with_small_width() {
75         val text = "Hello"
76         val width = 80
77         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
78         val annotatedString = AnnotatedString(text, spanStyle)
79         val textDelegate =
80             MultiParagraphLayoutCache(
81                     text = annotatedString,
82                     style = TextStyle.Default,
83                     fontFamilyResolver = fontFamilyResolver
84                 )
85                 .also { it.density = density }
86 
87         textDelegate.layoutWithConstraints(Constraints(maxWidth = width), layoutDirection)
88         val layoutResult = textDelegate.textLayoutResult
89 
90         assertThat(layoutResult.size.width).isEqualTo(width)
91     }
92 
93     @Test
height_getternull94     fun height_getter() {
95         with(density) {
96             val fontSize = 20.sp
97             val spanStyle = SpanStyle(fontSize = fontSize, fontFamily = fontFamily)
98             val text = "hello"
99             val annotatedString = AnnotatedString(text, spanStyle)
100             val textDelegate =
101                 MultiParagraphLayoutCache(
102                         text = annotatedString,
103                         style = TextStyle.Default,
104                         fontFamilyResolver = fontFamilyResolver
105                     )
106                     .also { it.density = this }
107 
108             textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
109             val layoutResult = textDelegate.textLayoutResult
110 
111             assertThat(layoutResult.size.height).isEqualTo((fontSize.toPx()).toIntPx())
112         }
113     }
114 
115     @Test
layout_build_layoutResultnull116     fun layout_build_layoutResult() {
117         val textDelegate =
118             MultiParagraphLayoutCache(
119                     text = AnnotatedString("hello"),
120                     style = TextStyle.Default,
121                     fontFamilyResolver = fontFamilyResolver
122                 )
123                 .also { it.density = density }
124 
125         textDelegate.layoutWithConstraints(Constraints(0, 20), layoutDirection)
126         val layoutResult = textDelegate.textLayoutResult
127 
128         assertThat(layoutResult).isNotNull()
129     }
130 
IntegerSubjectnull131     private fun IntegerSubject.isZero() {
132         this.isEqualTo(0)
133     }
134 
135     @Test
getPositionForOffset_First_Characternull136     fun getPositionForOffset_First_Character() {
137         val text = "Hello"
138         val annotatedString =
139             AnnotatedString(text, SpanStyle(fontSize = 20.sp, fontFamily = fontFamily))
140 
141         val textDelegate =
142             MultiParagraphLayoutCache(
143                     text = annotatedString,
144                     style = TextStyle.Default,
145                     fontFamilyResolver = fontFamilyResolver
146                 )
147                 .also { it.density = density }
148         textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
149         val layoutResult = textDelegate.textLayoutResult
150 
151         val selection = layoutResult.getOffsetForPosition(Offset.Zero)
152 
153         assertThat(selection).isZero()
154     }
155 
156     @Test
getPositionForOffset_other_Characternull157     fun getPositionForOffset_other_Character() {
158         with(density) {
159             val fontSize = 20.sp
160             val characterIndex = 2 // Start from 0.
161             val text = "Hello"
162 
163             val annotatedString =
164                 AnnotatedString(text, SpanStyle(fontSize = fontSize, fontFamily = fontFamily))
165 
166             val textDelegate =
167                 MultiParagraphLayoutCache(
168                         text = annotatedString,
169                         style = TextStyle.Default,
170                         fontFamilyResolver = fontFamilyResolver
171                     )
172                     .also { it.density = this }
173             textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
174             val layoutResult = textDelegate.textLayoutResult
175 
176             val selection =
177                 layoutResult.getOffsetForPosition(
178                     position = Offset((fontSize.toPx() * characterIndex + 1), 0f)
179                 )
180 
181             assertThat(selection).isEqualTo(characterIndex)
182         }
183     }
184 
185     @Test
hasOverflowShaderFalsenull186     fun hasOverflowShaderFalse() {
187         val text = "Hello"
188         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
189         val annotatedString = AnnotatedString(text, spanStyle)
190         val textDelegate =
191             MultiParagraphLayoutCache(
192                     text = annotatedString,
193                     style = TextStyle.Default,
194                     fontFamilyResolver = fontFamilyResolver
195                 )
196                 .also { it.density = density }
197 
198         textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
199         val layoutResult = textDelegate.textLayoutResult
200 
201         assertThat(layoutResult.hasVisualOverflow).isFalse()
202 
203         // paint should not throw exception
204         TextPainter.paint(Canvas(android.graphics.Canvas()), layoutResult)
205     }
206 
207     @Test
didOverflowHeight_isTrue_when_maxLines_exceedednull208     fun didOverflowHeight_isTrue_when_maxLines_exceeded() {
209         val text = "HelloHellowHelloHello"
210         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
211         val annotatedString = AnnotatedString(text, spanStyle)
212         val maxLines = 3
213 
214         val textDelegate =
215             MultiParagraphLayoutCache(
216                     text = annotatedString,
217                     style = TextStyle.Default,
218                     fontFamilyResolver = fontFamilyResolver,
219                     maxLines = maxLines
220                 )
221                 .also { it.density = density }
222 
223         textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
224         // Tries to make 5 lines of text, which exceeds the given maxLines(3).
225         val maxWidth = textDelegate.maxIntrinsicWidth(LayoutDirection.Ltr) / 5
226         textDelegate.layoutWithConstraints(Constraints(maxWidth = maxWidth), layoutDirection)
227         val layoutResult = textDelegate.textLayoutResult
228 
229         assertThat(layoutResult.didOverflowHeight).isTrue()
230     }
231 
232     @Test
didOverflowHeight_isFalse_when_maxLines_notExceedednull233     fun didOverflowHeight_isFalse_when_maxLines_notExceeded() {
234         val text = "HelloHellowHelloHello"
235         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
236         val annotatedString = AnnotatedString(text, spanStyle)
237         val maxLines = 10
238 
239         val textDelegate =
240             MultiParagraphLayoutCache(
241                     text = annotatedString,
242                     style = TextStyle.Default,
243                     fontFamilyResolver = fontFamilyResolver,
244                     maxLines = maxLines
245                 )
246                 .also { it.density = density }
247 
248         textDelegate.layoutWithConstraints(Constraints.fixed(0, 0), LayoutDirection.Ltr)
249         // Tries to make 5 lines of text, which doesn't exceed the given maxLines(10).
250         val maxWidth = textDelegate.maxIntrinsicWidth(LayoutDirection.Ltr) / 5
251         textDelegate.layoutWithConstraints(Constraints(maxWidth = maxWidth), layoutDirection)
252         val layoutResult = textDelegate.textLayoutResult
253 
254         assertThat(layoutResult.didOverflowHeight).isFalse()
255     }
256 
257     @Test
didOverflowHeight_isTrue_when_maxHeight_exceedednull258     fun didOverflowHeight_isTrue_when_maxHeight_exceeded() {
259         val text = "HelloHellowHelloHello"
260         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
261         val annotatedString = AnnotatedString(text, spanStyle)
262 
263         val textDelegate =
264             MultiParagraphLayoutCache(
265                     text = annotatedString,
266                     style = TextStyle.Default,
267                     fontFamilyResolver = fontFamilyResolver,
268                 )
269                 .also { it.density = density }
270 
271         textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
272 
273         val maxIntrinsicsHeight = textDelegate.textLayoutResult.multiParagraph.height
274 
275         // Make maxHeight smaller than needed.
276         val maxHeight = floor(maxIntrinsicsHeight / 2).toInt()
277         textDelegate.layoutWithConstraints(Constraints(maxHeight = maxHeight), layoutDirection)
278         val layoutResult = textDelegate.textLayoutResult
279 
280         assertThat(layoutResult.didOverflowHeight).isTrue()
281     }
282 
283     @Test
didOverflowHeight_isFalse_when_maxHeight_notExceedednull284     fun didOverflowHeight_isFalse_when_maxHeight_notExceeded() {
285         val text = "HelloHellowHelloHello"
286         val spanStyle = SpanStyle(fontSize = 20.sp, fontFamily = fontFamily)
287         val annotatedString = AnnotatedString(text, spanStyle)
288 
289         val textDelegate =
290             MultiParagraphLayoutCache(
291                     text = annotatedString,
292                     style = TextStyle.Default,
293                     fontFamilyResolver = fontFamilyResolver,
294                 )
295                 .also { it.density = density }
296 
297         textDelegate.layoutWithConstraints(Constraints(), layoutDirection)
298         val maxIntrinsicsHeight = textDelegate.textLayoutResult.multiParagraph.height
299 
300         // Make max height larger than the needed.
301         val maxHeight = floor(maxIntrinsicsHeight * 2).toInt()
302         textDelegate.layoutWithConstraints(Constraints(maxHeight = maxHeight), layoutDirection)
303         val layoutResult = textDelegate.textLayoutResult
304 
305         assertThat(layoutResult.didOverflowHeight).isFalse()
306     }
307 }
308